From 75f6db10bd1afeeab00bc9265ae7090a197f9e8f Mon Sep 17 00:00:00 2001 From: Kevin Delemme Date: Wed, 23 Oct 2024 09:03:42 -0400 Subject: [PATCH 01/14] fix(slo): Slices without any data event are considered good (#196942) --- .../__snapshots__/apm_transaction_duration.test.ts.snap | 4 ++-- .../__snapshots__/apm_transaction_error_rate.test.ts.snap | 4 ++-- .../transform_generators/__snapshots__/histogram.test.ts.snap | 4 ++-- .../__snapshots__/kql_custom.test.ts.snap | 4 ++-- .../__snapshots__/metric_custom.test.ts.snap | 2 +- .../services/transform_generators/apm_transaction_duration.ts | 4 ++-- .../transform_generators/apm_transaction_error_rate.ts | 4 ++-- .../slo/server/services/transform_generators/histogram.ts | 4 ++-- .../slo/server/services/transform_generators/kql_custom.ts | 4 ++-- .../slo/server/services/transform_generators/metric_custom.ts | 4 ++-- 10 files changed, 19 insertions(+), 19 deletions(-) diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/__snapshots__/apm_transaction_duration.test.ts.snap b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/__snapshots__/apm_transaction_duration.test.ts.snap index e5eba04f5305c..fe0c3a8904c05 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/__snapshots__/apm_transaction_duration.test.ts.snap +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/__snapshots__/apm_transaction_duration.test.ts.snap @@ -394,7 +394,7 @@ Object { "goodEvents": "slo.numerator.value", "totalEvents": "slo.denominator.value", }, - "script": "params.goodEvents / params.totalEvents >= 0.95 ? 1 : 0", + "script": "if (params.totalEvents == 0) { return 1 } else { return params.goodEvents / params.totalEvents >= 0.95 ? 1 : 0 }", }, }, "slo.numerator": Object { @@ -542,7 +542,7 @@ Object { "goodEvents": "slo.numerator.value", "totalEvents": "slo.denominator.value", }, - "script": "params.goodEvents / params.totalEvents > 0 ? 1 : 0", + "script": "if (params.totalEvents == 0) { return 1 } else { return params.goodEvents / params.totalEvents > 0 ? 1 : 0 }", }, }, "slo.numerator": Object { diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/__snapshots__/apm_transaction_error_rate.test.ts.snap b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/__snapshots__/apm_transaction_error_rate.test.ts.snap index 92c5b8c09385c..a081c2829f2bc 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/__snapshots__/apm_transaction_error_rate.test.ts.snap +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/__snapshots__/apm_transaction_error_rate.test.ts.snap @@ -358,7 +358,7 @@ Object { "goodEvents": "slo.numerator>_count", "totalEvents": "slo.denominator>_count", }, - "script": "params.goodEvents / params.totalEvents >= 0.95 ? 1 : 0", + "script": "if (params.totalEvents == 0) { return 1 } else { return params.goodEvents / params.totalEvents >= 0.95 ? 1 : 0 }", }, }, "slo.numerator": Object { @@ -493,7 +493,7 @@ Object { "goodEvents": "slo.numerator>_count", "totalEvents": "slo.denominator>_count", }, - "script": "params.goodEvents / params.totalEvents > 0 ? 1 : 0", + "script": "if (params.totalEvents == 0) { return 1 } else { return params.goodEvents / params.totalEvents > 0 ? 1 : 0 }", }, }, "slo.numerator": Object { diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/__snapshots__/histogram.test.ts.snap b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/__snapshots__/histogram.test.ts.snap index 260a9406e7674..43026be44a3bb 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/__snapshots__/histogram.test.ts.snap +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/__snapshots__/histogram.test.ts.snap @@ -134,7 +134,7 @@ Object { "goodEvents": "slo.numerator>value", "totalEvents": "slo.denominator>value", }, - "script": "params.goodEvents / params.totalEvents >= 0.95 ? 1 : 0", + "script": "if (params.totalEvents == 0) { return 1 } else { return params.goodEvents / params.totalEvents >= 0.95 ? 1 : 0 }", }, }, "slo.numerator": Object { @@ -263,7 +263,7 @@ Object { "goodEvents": "slo.numerator>value", "totalEvents": "slo.denominator>value", }, - "script": "params.goodEvents / params.totalEvents > 0 ? 1 : 0", + "script": "if (params.totalEvents == 0) { return 1 } else { return params.goodEvents / params.totalEvents > 0 ? 1 : 0 }", }, }, "slo.numerator": Object { diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/__snapshots__/kql_custom.test.ts.snap b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/__snapshots__/kql_custom.test.ts.snap index 26fb5dde0f784..95e1e3abf6aa5 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/__snapshots__/kql_custom.test.ts.snap +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/__snapshots__/kql_custom.test.ts.snap @@ -140,7 +140,7 @@ Object { "goodEvents": "slo.numerator>_count", "totalEvents": "slo.denominator>_count", }, - "script": "params.goodEvents / params.totalEvents >= 0.95 ? 1 : 0", + "script": "if (params.totalEvents == 0) { return 1 } else { return params.goodEvents / params.totalEvents >= 0.95 ? 1 : 0 }", }, }, "slo.numerator": Object { @@ -242,7 +242,7 @@ Object { "goodEvents": "slo.numerator>_count", "totalEvents": "slo.denominator>_count", }, - "script": "params.goodEvents / params.totalEvents > 0 ? 1 : 0", + "script": "if (params.totalEvents == 0) { return 1 } else { return params.goodEvents / params.totalEvents > 0 ? 1 : 0 }", }, }, "slo.numerator": Object { diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/__snapshots__/metric_custom.test.ts.snap b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/__snapshots__/metric_custom.test.ts.snap index c1317435fb97b..4b7dab85cec60 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/__snapshots__/metric_custom.test.ts.snap +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/__snapshots__/metric_custom.test.ts.snap @@ -181,7 +181,7 @@ Object { "goodEvents": "slo.numerator>value", "totalEvents": "slo.denominator>value", }, - "script": "params.goodEvents / params.totalEvents >= 0.95 ? 1 : 0", + "script": "if (params.totalEvents == 0) { return 1 } else { return params.goodEvents / params.totalEvents >= 0.95 ? 1 : 0 }", }, }, "slo.numerator": Object { diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/apm_transaction_duration.ts b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/apm_transaction_duration.ts index 85496cb91f6e4..b349a5affeceb 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/apm_transaction_duration.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/apm_transaction_duration.ts @@ -185,9 +185,9 @@ export class ApmTransactionDurationTransformGenerator extends TransformGenerator goodEvents: 'slo.numerator.value', totalEvents: 'slo.denominator.value', }, - script: `params.goodEvents / params.totalEvents ${getTimesliceTargetComparator( + script: `if (params.totalEvents == 0) { return 1 } else { return params.goodEvents / params.totalEvents ${getTimesliceTargetComparator( slo.objective.timesliceTarget! - )} ${slo.objective.timesliceTarget} ? 1 : 0`, + )} ${slo.objective.timesliceTarget} ? 1 : 0 }`, }, }, }), diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/apm_transaction_error_rate.ts b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/apm_transaction_error_rate.ts index 91b4af3a07aff..3aa0d4507e8a4 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/apm_transaction_error_rate.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/apm_transaction_error_rate.ts @@ -168,9 +168,9 @@ export class ApmTransactionErrorRateTransformGenerator extends TransformGenerato goodEvents: 'slo.numerator>_count', totalEvents: 'slo.denominator>_count', }, - script: `params.goodEvents / params.totalEvents ${getTimesliceTargetComparator( + script: `if (params.totalEvents == 0) { return 1 } else { return params.goodEvents / params.totalEvents ${getTimesliceTargetComparator( slo.objective.timesliceTarget! - )} ${slo.objective.timesliceTarget} ? 1 : 0`, + )} ${slo.objective.timesliceTarget} ? 1 : 0 }`, }, }, }), diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/histogram.ts b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/histogram.ts index c7c97639b92a8..b19f9a48e70f0 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/histogram.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/histogram.ts @@ -101,9 +101,9 @@ export class HistogramTransformGenerator extends TransformGenerator { goodEvents: 'slo.numerator>value', totalEvents: 'slo.denominator>value', }, - script: `params.goodEvents / params.totalEvents ${getTimesliceTargetComparator( + script: `if (params.totalEvents == 0) { return 1 } else { return params.goodEvents / params.totalEvents ${getTimesliceTargetComparator( slo.objective.timesliceTarget! - )} ${slo.objective.timesliceTarget} ? 1 : 0`, + )} ${slo.objective.timesliceTarget} ? 1 : 0 }`, }, }, }), diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/kql_custom.ts b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/kql_custom.ts index 1924bc1a4c344..0082e13968c80 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/kql_custom.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/kql_custom.ts @@ -94,9 +94,9 @@ export class KQLCustomTransformGenerator extends TransformGenerator { goodEvents: 'slo.numerator>_count', totalEvents: 'slo.denominator>_count', }, - script: `params.goodEvents / params.totalEvents ${getTimesliceTargetComparator( + script: `if (params.totalEvents == 0) { return 1 } else { return params.goodEvents / params.totalEvents ${getTimesliceTargetComparator( slo.objective.timesliceTarget! - )} ${slo.objective.timesliceTarget} ? 1 : 0`, + )} ${slo.objective.timesliceTarget} ? 1 : 0 }`, }, }, }), diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/metric_custom.ts b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/metric_custom.ts index 5717b05a466de..e96f252d5ed84 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/metric_custom.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/metric_custom.ts @@ -105,9 +105,9 @@ export class MetricCustomTransformGenerator extends TransformGenerator { goodEvents: 'slo.numerator>value', totalEvents: 'slo.denominator>value', }, - script: `params.goodEvents / params.totalEvents ${getTimesliceTargetComparator( + script: `if (params.totalEvents == 0) { return 1 } else { return params.goodEvents / params.totalEvents ${getTimesliceTargetComparator( slo.objective.timesliceTarget! - )} ${slo.objective.timesliceTarget} ? 1 : 0`, + )} ${slo.objective.timesliceTarget} ? 1 : 0 }`, }, }, }), From d081884cb5eeba64a4d035c1bd01a5f809db0204 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Luis=20Gonz=C3=A1lez?= Date: Wed, 23 Oct 2024 15:31:01 +0200 Subject: [PATCH 02/14] [Search][Connectors] Choose connector selectable custom icon (#197068) ## Summary This PR shows the connectors selected icon when choosing one from the `` component. At the same time the selectedConnector was not updated when clearing the selection. Now it updated this state and the related UI elements like the left doc links and the footer last block changed their state based on this. ![CleanShot 2024-10-21 at 15 48 58](https://github.com/user-attachments/assets/ac76c44a-7562-4f5a-adf0-4a41d70bad46) - [x] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [x] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) --------- Co-authored-by: Elastic Machine --- .../choose_connector_selectable.tsx | 133 +++++++++--------- 1 file changed, 63 insertions(+), 70 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/components/choose_connector_selectable.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/components/choose_connector_selectable.tsx index 6c5505a22f81e..7019fcbb71e3f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/components/choose_connector_selectable.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/components/choose_connector_selectable.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { useActions, useValues } from 'kea'; @@ -13,14 +13,16 @@ import { EuiBadge, EuiFlexItem, EuiIcon, - EuiInputPopover, - EuiSelectable, - EuiSelectableOption, + EuiComboBox, + EuiComboBoxOptionOption, + EuiFlexGroup, + EuiText, useEuiTheme, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import connectorLogo from '../../../../../../assets/images/connector.svg'; import { KibanaLogic } from '../../../../../shared/kibana'; import { NewConnectorLogic } from '../../../new_index/method_connector/new_connector_logic'; import { SelfManagePreference } from '../create_connector'; @@ -36,9 +38,39 @@ export const ChooseConnectorSelectable: React.FC selfManaged, }) => { const { euiTheme } = useEuiTheme(); - const [isOpen, setIsOpen] = useState(false); + const [selectedOption, setSelectedOption] = useState>>( + [] + ); + const renderOption = ( + option: EuiComboBoxOptionOption, + searchValue: string, + contentClassName: string + ) => { + const { _append, key, label, _prepend } = option as EuiComboBoxOptionOption & { + _append: JSX.Element[]; + _prepend: JSX.Element; + }; + return ( + + + {_prepend} + + + {label} + + + + {_append} + + ); + }; const [selectableOptions, selectableSetOptions] = useState< - Array> + Array> >([]); const { connectorTypes } = useValues(KibanaLogic); const allConnectors = useMemo( @@ -50,9 +82,9 @@ export const ChooseConnectorSelectable: React.FC const getInitialOptions = () => { return allConnectors.map((connector, key) => { - const append: JSX.Element[] = []; + const _append: JSX.Element[] = []; if (connector.isTechPreview) { - append.push( + _append.push( {i18n.translate( 'xpack.enterpriseSearch.createConnector.chooseConnectorSelectable.thechPreviewBadgeLabel', @@ -62,7 +94,7 @@ export const ChooseConnectorSelectable: React.FC ); } if (connector.isBeta) { - append.push( + _append.push( {i18n.translate( 'xpack.enterpriseSearch.createConnector.chooseConnectorSelectable.BetaBadgeLabel', @@ -74,7 +106,7 @@ export const ChooseConnectorSelectable: React.FC ); } if (selfManaged === 'native' && !connector.isNative) { - append.push( + _append.push( {i18n.translate( 'xpack.enterpriseSearch.createConnector.chooseConnectorSelectable.OnlySelfManagedBadgeLabel', @@ -85,12 +117,11 @@ export const ChooseConnectorSelectable: React.FC ); } - return { - append, + _append, + _prepend: , key: key.toString(), label: connector.name, - prepend: , }; }); }; @@ -100,73 +131,35 @@ export const ChooseConnectorSelectable: React.FC useEffect(() => { selectableSetOptions(initialOptions); }, [selfManaged]); - const [searchValue, setSearchValue] = useState(''); - - const openPopover = useCallback(() => { - setIsOpen(true); - }, []); - const closePopover = useCallback(() => { - setIsOpen(false); - }, []); return ( - } + singleSelection + fullWidth + placeholder={i18n.translate( + 'xpack.enterpriseSearch.createConnector.chooseConnectorSelectable.placeholder.text', + { defaultMessage: 'Choose a data source' } + )} options={selectableOptions} - onChange={(newOptions, _, changedOption) => { - selectableSetOptions(newOptions); - closePopover(); - if (changedOption.checked === 'on') { - const keySelected = Number(changedOption.key); - setSelectedConnector(allConnectors[keySelected]); - setSearchValue(allConnectors[keySelected].name); - } else { + selectedOptions={selectedOption} + onChange={(selectedItem) => { + setSelectedOption(selectedItem); + if (selectedItem.length === 0) { setSelectedConnector(null); - setSearchValue(''); + return; } + const keySelected = Number(selectedItem[0].key); + setSelectedConnector(allConnectors[keySelected]); }} - listProps={{ - isVirtualized: true, - rowHeight: Number(euiTheme.base * 3), - showIcons: false, - }} - singleSelection - searchable - searchProps={{ - fullWidth: true, - isClearable: true, - onChange: (value) => { - if (value !== selectedConnector?.name) { - setSearchValue(value); - } - }, - onClick: openPopover, - onFocus: openPopover, - placeholder: i18n.translate( - 'xpack.enterpriseSearch.createConnector.chooseConnectorSelectable.placeholder.text', - { defaultMessage: 'Choose a data source' } - ), - value: searchValue, - }} - > - {(list, search) => ( - - {list} - - )} - + renderOption={renderOption} + rowHeight={(euiTheme.base / 2) * 5} + /> ); }; From 0a0c3fa796ad8d046c2cbc08a7553d5547cd1f6b Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Wed, 23 Oct 2024 15:31:53 +0200 Subject: [PATCH 03/14] [ES|QL] Replace the comments with multiline ones (#197391) ## Summary Due to this bug https://github.com/elastic/kibana/issues/191866 and for better user experience i am changing the comments to multiline ones --- .../src/autocomplete/recommended_queries/templates.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/recommended_queries/templates.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/recommended_queries/templates.ts index 6a3226c9dcc8f..5e8d217491925 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/recommended_queries/templates.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/recommended_queries/templates.ts @@ -32,7 +32,7 @@ export const getRecommendedQueries = ({ defaultMessage: 'Count aggregation', } ), - queryString: `${fromCommand}\n | STATS count = COUNT(*) // you can group by a field using the BY operator`, + queryString: `${fromCommand}\n | STATS count = COUNT(*) /* you can group by a field using the BY operator */`, }, ...(timeField ? [ @@ -49,7 +49,7 @@ export const getRecommendedQueries = ({ defaultMessage: 'Sort by time', } ), - queryString: `${fromCommand}\n | SORT ${timeField} // Data is not sorted by default`, + queryString: `${fromCommand}\n | SORT ${timeField} /* Data is not sorted by default */`, }, { label: i18n.translate( @@ -64,7 +64,7 @@ export const getRecommendedQueries = ({ defaultMessage: 'Count aggregation over time', } ), - queryString: `${fromCommand}\n | EVAL buckets = DATE_TRUNC(5 minute, ${timeField}) | STATS count = COUNT(*) BY buckets // try out different intervals`, + queryString: `${fromCommand}\n | EVAL buckets = DATE_TRUNC(5 minute, ${timeField}) | STATS count = COUNT(*) BY buckets /* try out different intervals */`, }, ] : []), @@ -98,7 +98,7 @@ export const getRecommendedQueries = ({ defaultMessage: 'Count aggregation over time', } ), - queryString: `${fromCommand}\n | WHERE ${timeField} <=?_tend and ${timeField} >?_tstart\n | STATS count = COUNT(*) BY \`Over time\` = BUCKET(${timeField}, 50, ?_tstart, ?_tend) // ?_tstart and ?_tend take the values of the time picker`, + queryString: `${fromCommand}\n | WHERE ${timeField} <=?_tend and ${timeField} >?_tstart\n | STATS count = COUNT(*) BY \`Over time\` = BUCKET(${timeField}, 50, ?_tstart, ?_tend) /* ?_tstart and ?_tend take the values of the time picker */`, }, { label: i18n.translate( @@ -113,7 +113,7 @@ export const getRecommendedQueries = ({ defaultMessage: 'Event rate over time', } ), - queryString: `${fromCommand}\n | STATS count = COUNT(*), min_timestamp = MIN(${timeField}) // MIN(dateField) finds the earliest timestamp in the dataset.\n | EVAL event_rate = count / DATE_DIFF("seconds", min_timestamp, NOW()) // Calculates the event rate by dividing the total count of events by the time difference (in seconds) between the earliest event and the current time.\n | KEEP event_rate`, + queryString: `${fromCommand}\n | STATS count = COUNT(*), min_timestamp = MIN(${timeField}) /* MIN(dateField) finds the earliest timestamp in the dataset. */ \n | EVAL event_rate = count / DATE_DIFF("seconds", min_timestamp, NOW()) /* Calculates the event rate by dividing the total count of events by the time difference (in seconds) between the earliest event and the current time. */\n | KEEP event_rate`, }, { label: i18n.translate( From 4568240e6420318be7f8d948c5d3e1c88b963abe Mon Sep 17 00:00:00 2001 From: Sergi Romeu Date: Wed, 23 Oct 2024 15:32:55 +0200 Subject: [PATCH 04/14] [APM][e2e] Skip transaction_details Cypress test (#197388) ## Summary Relates to #197386 This PR skips the failing test, eventually we will need to fix it and the work will be handled on the parent issue. --- .../e2e/transaction_details/transaction_details.cy.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/observability_solution/apm/ftr_e2e/cypress/e2e/transaction_details/transaction_details.cy.ts b/x-pack/plugins/observability_solution/apm/ftr_e2e/cypress/e2e/transaction_details/transaction_details.cy.ts index 404bc5d2492ee..38ced9a6587ee 100644 --- a/x-pack/plugins/observability_solution/apm/ftr_e2e/cypress/e2e/transaction_details/transaction_details.cy.ts +++ b/x-pack/plugins/observability_solution/apm/ftr_e2e/cypress/e2e/transaction_details/transaction_details.cy.ts @@ -34,7 +34,8 @@ describe('Transaction details', () => { cy.loginAsViewerUser(); }); - it('shows transaction name and transaction charts', () => { + // skipping this as it´s been failing a lot lately, more information here https://github.com/elastic/kibana/issues/197386 + it.skip('shows transaction name and transaction charts', () => { cy.intercept('GET', '/internal/apm/services/opbeans-java/transactions/charts/latency?*').as( 'transactionLatencyRequest' ); @@ -106,8 +107,8 @@ describe('Transaction details', () => { ); cy.contains('Create SLO'); }); - - it('shows top errors table', () => { + // skipping this as it´s been failing a lot lately, more information here https://github.com/elastic/kibana/issues/197386 + it.skip('shows top errors table', () => { cy.visitKibana( `/app/apm/services/opbeans-java/transactions/view?${new URLSearchParams({ ...timeRange, From 5f4742c3c6f03053fc8bfe23caeb4d687103e0d3 Mon Sep 17 00:00:00 2001 From: Tre Date: Wed, 23 Oct 2024 14:46:11 +0100 Subject: [PATCH 05/14] [FTR][Ownership] Assign osquery, search profiler, etc (#197037) ## Summary Assign test files to small number of reviewers ## Assignment Reasons Assigned osquery due to https://github.com/elastic/kibana/blob/608cc70be56fa63cb68a93d480e545fa95c0846a/x-pack/plugins/osquery/kibana.jsonc#L4 Assigned search profiler due to https://github.com/elastic/kibana/blob/main/x-pack/plugins/searchprofiler/kibana.jsonc#L4 Assigned slos due to https://github.com/elastic/kibana/blob/main/x-pack/plugins/observability_solution/slo/kibana.jsonc#L4 Assigned stats due to https://github.com/elastic/kibana/blob/main/src/plugins/usage_collection/kibana.jsonc#L4 Assigned status due to https://github.com/elastic/kibana/blob/10ec204128776930c48376a848fe20b1301569f9/packages/core/status/core-status-server-internal/kibana.jsonc#L4 Assigned telemetry due to https://github.com/elastic/kibana/blob/main/src/plugins/telemetry/kibana.jsonc#L4 Contributes to: https://github.com/elastic/kibana/issues/194817 --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .github/CODEOWNERS | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index d78ad640d625e..b7a69b94d1c26 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1206,6 +1206,7 @@ x-pack/test_serverless/**/test_suites/observability/ai_assistant @elastic/obs-ai /x-pack/test/functional/apps/infra/logs @elastic/obs-ux-logs-team # Observability UX management team +/x-pack/test/api_integration/apis/slos @elastic/obs-ux-management-team /x-pack/test/accessibility/apps/group1/uptime.ts @elastic/obs-ux-management-team /x-pack/test/accessibility/apps/group3/observability.ts @elastic/obs-ux-management-team /x-pack/packages/observability/alert_details @elastic/obs-ux-management-team @@ -1392,6 +1393,9 @@ x-pack/test/api_integration/deployment_agnostic/services/ @elastic/appex-qa x-pack/test/**/deployment_agnostic/ @elastic/appex-qa #temporarily to monitor tests migration # Core +/x-pack/test/api_integration/apis/telemetry @elastic/kibana-core +/x-pack/test/api_integration/apis/status @elastic/kibana-core +/x-pack/test/api_integration/apis/stats @elastic/kibana-core /x-pack/test/api_integration/apis/kibana/stats @elastic/kibana-core /x-pack/test_serverless/functional/test_suites/security/config.saved_objects_management.ts @elastic/kibana-core /config/ @elastic/kibana-core @@ -1532,6 +1536,7 @@ x-pack/test/api_integration/apis/management/index_management/inference_endpoints /x-pack/test/functional_search/ @elastic/search-kibana # Management Experience - Deployment Management +/x-pack/test/api_integration/apis/searchprofiler @elastic/kibana-management /x-pack/test/api_integration/apis/console @elastic/kibana-management /x-pack/test_serverless/**/test_suites/common/index_management/ @elastic/kibana-management /x-pack/test_serverless/**/test_suites/common/management/index_management/ @elastic/kibana-management @@ -1835,6 +1840,7 @@ x-pack/test/security_solution_cypress/cypress/tasks/expandable_flyout @elastic/ /x-pack/plugins/security_solution/public/common/components/threat_match @elastic/security-detection-engine ## Security Solution sub teams - security-defend-workflows +/x-pack/test/api_integration/apis/osquery @elastic/security-defend-workflows /x-pack/plugins/security_solution/public/management/ @elastic/security-defend-workflows /x-pack/plugins/security_solution/public/common/lib/endpoint/ @elastic/security-defend-workflows /x-pack/plugins/security_solution/public/common/components/endpoint/ @elastic/security-defend-workflows From e1c0cef15dd7f771a621db0230bf4cddcf10815b Mon Sep 17 00:00:00 2001 From: Drew Tate Date: Wed, 23 Oct 2024 07:49:38 -0600 Subject: [PATCH 06/14] [ES|QL] Create validation errors for unknown parameters (#197334) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Follow-up from https://github.com/elastic/kibana/pull/195989. We discussed as a team and decided to show validation errors when an unknown variable is used as an argument to subsequent functions. **Before** Screenshot 2024-10-22 at 1 41 08 PM **After** Screenshot 2024-10-22 at 1 41 29 PM --- .../src/shared/helpers.ts | 2 +- .../validation/__tests__/functions.test.ts | 396 ++++++++++-------- 2 files changed, 226 insertions(+), 172 deletions(-) diff --git a/packages/kbn-esql-validation-autocomplete/src/shared/helpers.ts b/packages/kbn-esql-validation-autocomplete/src/shared/helpers.ts index 31c2c01a11404..2392a44814997 100644 --- a/packages/kbn-esql-validation-autocomplete/src/shared/helpers.ts +++ b/packages/kbn-esql-validation-autocomplete/src/shared/helpers.ts @@ -465,7 +465,7 @@ export function checkFunctionArgMatchesDefinition( const wrappedTypes: Array<(typeof validHit)['type']> = Array.isArray(validHit.type) ? validHit.type : [validHit.type]; - return wrappedTypes.some((ct) => ct === argType || ct === 'null' || ct === 'unknown'); + return wrappedTypes.some((ct) => ct === argType || ct === 'null'); } if (arg.type === 'inlineCast') { const lowerArgType = argType?.toLowerCase(); diff --git a/packages/kbn-esql-validation-autocomplete/src/validation/__tests__/functions.test.ts b/packages/kbn-esql-validation-autocomplete/src/validation/__tests__/functions.test.ts index a3934c1a35627..ec0fbe5395334 100644 --- a/packages/kbn-esql-validation-autocomplete/src/validation/__tests__/functions.test.ts +++ b/packages/kbn-esql-validation-autocomplete/src/validation/__tests__/functions.test.ts @@ -505,67 +505,41 @@ describe('function validation', () => { ['Invalid option ["foo"] for test. Supported options: ["ASC", "DESC"].'] ); }); - }); - describe('command/option support', () => { - it('validates command support', async () => { + it('validates values of type unknown', async () => { setTestFunctions([ { - name: 'eval_fn', + name: 'test1', type: 'eval', description: '', supportedCommands: ['eval'], signatures: [ { - params: [], - returnType: 'keyword', - }, - ], - }, - { - name: 'stats_fn', - type: 'agg', - description: '', - supportedCommands: ['stats'], - signatures: [ - { - params: [], - returnType: 'keyword', - }, - ], - }, - { - name: 'row_fn', - type: 'eval', - description: '', - supportedCommands: ['row'], - signatures: [ - { - params: [], + params: [{ name: 'arg1', type: 'keyword' }], returnType: 'keyword', }, ], }, { - name: 'where_fn', + name: 'test2', type: 'eval', description: '', - supportedCommands: ['where'], + supportedCommands: ['eval'], signatures: [ { - params: [], + params: [{ name: 'arg1', type: 'keyword' }], returnType: 'keyword', }, ], }, { - name: 'sort_fn', + name: 'test3', type: 'eval', description: '', - supportedCommands: ['sort'], + supportedCommands: ['eval'], signatures: [ { - params: [], + params: [{ name: 'arg1', type: 'long' }], returnType: 'keyword', }, ], @@ -573,155 +547,235 @@ describe('function validation', () => { ]); const { expectErrors } = await setup(); + await expectErrors( + `FROM a_index + | EVAL foo = TEST1(1.) + | EVAL TEST2(foo) + | EVAL TEST3(foo)`, + [ + 'Argument of [test1] must be [keyword], found value [1.] type [double]', + 'Argument of [test2] must be [keyword], found value [foo] type [unknown]', + 'Argument of [test3] must be [long], found value [foo] type [unknown]', + ] + ); + }); - await expectErrors('FROM a_index | EVAL EVAL_FN()', []); - await expectErrors('FROM a_index | SORT SORT_FN()', []); - await expectErrors('FROM a_index | STATS STATS_FN()', []); - await expectErrors('ROW ROW_FN()', []); - await expectErrors('FROM a_index | WHERE WHERE_FN()', []); + describe('command/option support', () => { + it('validates command support', async () => { + setTestFunctions([ + { + name: 'eval_fn', + type: 'eval', + description: '', + supportedCommands: ['eval'], + signatures: [ + { + params: [], + returnType: 'keyword', + }, + ], + }, + { + name: 'stats_fn', + type: 'agg', + description: '', + supportedCommands: ['stats'], + signatures: [ + { + params: [], + returnType: 'keyword', + }, + ], + }, + { + name: 'row_fn', + type: 'eval', + description: '', + supportedCommands: ['row'], + signatures: [ + { + params: [], + returnType: 'keyword', + }, + ], + }, + { + name: 'where_fn', + type: 'eval', + description: '', + supportedCommands: ['where'], + signatures: [ + { + params: [], + returnType: 'keyword', + }, + ], + }, + { + name: 'sort_fn', + type: 'eval', + description: '', + supportedCommands: ['sort'], + signatures: [ + { + params: [], + returnType: 'keyword', + }, + ], + }, + ]); - await expectErrors('FROM a_index | EVAL SORT_FN()', [ - 'EVAL does not support function sort_fn', - ]); - await expectErrors('FROM a_index | SORT STATS_FN()', [ - 'SORT does not support function stats_fn', - ]); - await expectErrors('FROM a_index | STATS ROW_FN()', [ - 'At least one aggregation function required in [STATS], found [ROW_FN()]', - 'STATS does not support function row_fn', - ]); - await expectErrors('ROW WHERE_FN()', ['ROW does not support function where_fn']); - await expectErrors('FROM a_index | WHERE EVAL_FN()', [ - 'WHERE does not support function eval_fn', - ]); - }); + const { expectErrors } = await setup(); - it('validates option support', async () => { - setTestFunctions([ - { - name: 'supports_by_option', - type: 'eval', - description: '', - supportedCommands: ['eval'], - supportedOptions: ['by'], - signatures: [ - { - params: [], - returnType: 'keyword', - }, - ], - }, - { - name: 'does_not_support_by_option', - type: 'eval', - description: '', - supportedCommands: ['eval'], - supportedOptions: [], - signatures: [ - { - params: [], - returnType: 'keyword', - }, - ], - }, + await expectErrors('FROM a_index | EVAL EVAL_FN()', []); + await expectErrors('FROM a_index | SORT SORT_FN()', []); + await expectErrors('FROM a_index | STATS STATS_FN()', []); + await expectErrors('ROW ROW_FN()', []); + await expectErrors('FROM a_index | WHERE WHERE_FN()', []); - { - name: 'agg_fn', - type: 'agg', - description: '', - supportedCommands: ['stats'], - supportedOptions: [], - signatures: [ - { - params: [], - returnType: 'keyword', - }, - ], - }, - ]); + await expectErrors('FROM a_index | EVAL SORT_FN()', [ + 'EVAL does not support function sort_fn', + ]); + await expectErrors('FROM a_index | SORT STATS_FN()', [ + 'SORT does not support function stats_fn', + ]); + await expectErrors('FROM a_index | STATS ROW_FN()', [ + 'At least one aggregation function required in [STATS], found [ROW_FN()]', + 'STATS does not support function row_fn', + ]); + await expectErrors('ROW WHERE_FN()', ['ROW does not support function where_fn']); + await expectErrors('FROM a_index | WHERE EVAL_FN()', [ + 'WHERE does not support function eval_fn', + ]); + }); - const { expectErrors } = await setup(); + it('validates option support', async () => { + setTestFunctions([ + { + name: 'supports_by_option', + type: 'eval', + description: '', + supportedCommands: ['eval'], + supportedOptions: ['by'], + signatures: [ + { + params: [], + returnType: 'keyword', + }, + ], + }, + { + name: 'does_not_support_by_option', + type: 'eval', + description: '', + supportedCommands: ['eval'], + supportedOptions: [], + signatures: [ + { + params: [], + returnType: 'keyword', + }, + ], + }, - await expectErrors('FROM a_index | STATS AGG_FN() BY SUPPORTS_BY_OPTION()', []); - await expectErrors('FROM a_index | STATS AGG_FN() BY DOES_NOT_SUPPORT_BY_OPTION()', [ - 'STATS BY does not support function does_not_support_by_option', - ]); + { + name: 'agg_fn', + type: 'agg', + description: '', + supportedCommands: ['stats'], + supportedOptions: [], + signatures: [ + { + params: [], + returnType: 'keyword', + }, + ], + }, + ]); + + const { expectErrors } = await setup(); + + await expectErrors('FROM a_index | STATS AGG_FN() BY SUPPORTS_BY_OPTION()', []); + await expectErrors('FROM a_index | STATS AGG_FN() BY DOES_NOT_SUPPORT_BY_OPTION()', [ + 'STATS BY does not support function does_not_support_by_option', + ]); + }); }); - }); - describe('nested functions', () => { - it('supports deep nesting', async () => { - setTestFunctions([ - { - name: 'test', - type: 'eval', - description: '', - supportedCommands: ['eval'], - signatures: [ - { - params: [{ name: 'arg1', type: 'keyword' }], - returnType: 'integer', - }, - ], - }, - { - name: 'test2', - type: 'eval', - description: '', - supportedCommands: ['eval'], - signatures: [ - { - params: [{ name: 'arg1', type: 'integer' }], - returnType: 'keyword', - }, - ], - }, - ]); + describe('nested functions', () => { + it('supports deep nesting', async () => { + setTestFunctions([ + { + name: 'test', + type: 'eval', + description: '', + supportedCommands: ['eval'], + signatures: [ + { + params: [{ name: 'arg1', type: 'keyword' }], + returnType: 'integer', + }, + ], + }, + { + name: 'test2', + type: 'eval', + description: '', + supportedCommands: ['eval'], + signatures: [ + { + params: [{ name: 'arg1', type: 'integer' }], + returnType: 'keyword', + }, + ], + }, + ]); - const { expectErrors } = await setup(); + const { expectErrors } = await setup(); - await expectErrors('FROM a_index | EVAL TEST(TEST2(TEST(TEST2(1))))', []); - }); + await expectErrors('FROM a_index | EVAL TEST(TEST2(TEST(TEST2(1))))', []); + }); - it("doesn't allow nested aggregation functions", async () => { - setTestFunctions([ - { - name: 'agg_fn', - type: 'agg', - description: '', - supportedCommands: ['stats'], - signatures: [ - { - params: [{ name: 'arg1', type: 'keyword' }], - returnType: 'keyword', - }, - ], - }, - { - name: 'scalar_fn', - type: 'eval', - description: '', - supportedCommands: ['stats'], - signatures: [ - { - params: [{ name: 'arg1', type: 'keyword' }], - returnType: 'keyword', - }, - ], - }, - ]); + it("doesn't allow nested aggregation functions", async () => { + setTestFunctions([ + { + name: 'agg_fn', + type: 'agg', + description: '', + supportedCommands: ['stats'], + signatures: [ + { + params: [{ name: 'arg1', type: 'keyword' }], + returnType: 'keyword', + }, + ], + }, + { + name: 'scalar_fn', + type: 'eval', + description: '', + supportedCommands: ['stats'], + signatures: [ + { + params: [{ name: 'arg1', type: 'keyword' }], + returnType: 'keyword', + }, + ], + }, + ]); - const { expectErrors } = await setup(); + const { expectErrors } = await setup(); - await expectErrors('FROM a_index | STATS AGG_FN(AGG_FN(""))', [ - 'Aggregate function\'s parameters must be an attribute, literal or a non-aggregation function; found [AGG_FN("")] of type [keyword]', - ]); - // @TODO — enable this test when we have fixed this bug - // await expectErrors('FROM a_index | STATS AGG_FN(SCALAR_FN(AGG_FN("")))', [ - // 'No nested aggregation functions.', - // ]); - }); + await expectErrors('FROM a_index | STATS AGG_FN(AGG_FN(""))', [ + 'Aggregate function\'s parameters must be an attribute, literal or a non-aggregation function; found [AGG_FN("")] of type [keyword]', + ]); + // @TODO — enable this test when we have fixed this bug + // await expectErrors('FROM a_index | STATS AGG_FN(SCALAR_FN(AGG_FN("")))', [ + // 'No nested aggregation functions.', + // ]); + }); - // @TODO — test function aliases + // @TODO — test function aliases + }); }); }); From 1a13fad289a157d962076bffcffd8aa661fc1bcc Mon Sep 17 00:00:00 2001 From: Pablo Machado Date: Wed, 23 Oct 2024 16:13:02 +0200 Subject: [PATCH 07/14] [SecuritySolution] Change Entity Store default transform interval to 30 seconds (#197419) ## Summary Change Entity Store default transform interval to 30 seconds --- .../server/lib/entity_analytics/entity_store/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/constants.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/constants.ts index addf432f20398..796932d79b364 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/constants.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/constants.ts @@ -9,7 +9,7 @@ import type { EngineStatus } from '../../../../common/api/entity_analytics'; export const DEFAULT_LOOKBACK_PERIOD = '24h'; -export const DEFAULT_INTERVAL = '15s'; +export const DEFAULT_INTERVAL = '30s'; export const ENGINE_STATUS: Record, EngineStatus> = { INSTALLING: 'installing', From 9656621fcc8f6f9a615b0a27d45db9722e047a10 Mon Sep 17 00:00:00 2001 From: Juan Pablo Djeredjian Date: Wed, 23 Oct 2024 16:44:13 +0200 Subject: [PATCH 08/14] [Security Solution] Fix `DataSource` payload creation during rule upgrade with `MERGED` pick_version (#197262) ## Summary The PR https://github.com/elastic/kibana/pull/191439 enhanced the `/upgrade/_perform` API contract and functionality to allow the users of the endpoint to upgrade rules to their `MERGED` version. However, a bug slipped in, where the two different types of `DataSource` (`type: index_patterns` or `type: data_view_id`) weren't properly handled and would cause, in some cases, a rule payload to be created having both an `index` and `data_view` field, causing upgrade to fail. This PR fixes the issue by handling these two field in a specific way, checking what the `DataSource` diffable field's type is, and setting the other field to `undefined`. ### Checklist Delete any items that are not applicable to this PR. - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#_add_your_labels) - [ ] This will appear in the **Release Notes** and follow the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --- .../diffable_rule_fields_mappings.ts | 21 ++++++++++ ...e_perform_prebuilt_rules.all_rules_mode.ts | 38 +++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/diffable_rule_fields_mappings.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/diffable_rule_fields_mappings.ts index d56747f9db264..8a796b5db1e28 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/diffable_rule_fields_mappings.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/diffable_rule_fields_mappings.ts @@ -7,6 +7,8 @@ import { get } from 'lodash'; import type { RuleSchedule, + DataSourceIndexPatterns, + DataSourceDataView, InlineKqlQuery, ThreeWayDiff, DiffableRuleTypes, @@ -195,6 +197,10 @@ export const transformDiffableFieldValues = ( } else if (fieldName === 'saved_id' && isInlineQuery(diffableFieldValue)) { // saved_id should be set only for rules with SavedKqlQuery, undefined otherwise return { type: 'TRANSFORMED_FIELD', value: undefined }; + } else if (fieldName === 'data_view_id' && isDataSourceIndexPatterns(diffableFieldValue)) { + return { type: 'TRANSFORMED_FIELD', value: undefined }; + } else if (fieldName === 'index' && isDataSourceDataView(diffableFieldValue)) { + return { type: 'TRANSFORMED_FIELD', value: undefined }; } return { type: 'NON_TRANSFORMED_FIELD' }; @@ -209,3 +215,18 @@ function isInlineQuery(value: unknown): value is InlineKqlQuery { typeof value === 'object' && value !== null && 'type' in value && value.type === 'inline_query' ); } + +function isDataSourceIndexPatterns(value: unknown): value is DataSourceIndexPatterns { + return ( + typeof value === 'object' && + value !== null && + 'type' in value && + value.type === 'index_patterns' + ); +} + +function isDataSourceDataView(value: unknown): value is DataSourceDataView { + return ( + typeof value === 'object' && value !== null && 'type' in value && value.type === 'data_view' + ); +} diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/upgrade_perform_prebuilt_rules.all_rules_mode.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/upgrade_perform_prebuilt_rules.all_rules_mode.ts index 2d0fe71e7d5d4..e24e517c93459 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/upgrade_perform_prebuilt_rules.all_rules_mode.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/upgrade_perform_prebuilt_rules.all_rules_mode.ts @@ -17,6 +17,9 @@ import { ThreatMatchRule, FIELDS_TO_UPGRADE_TO_CURRENT_VERSION, ModeEnum, + AllFieldsDiff, + DataSourceIndexPatterns, + QueryRule, } from '@kbn/security-solution-plugin/common/api/detection_engine'; import { PrebuiltRuleAsset } from '@kbn/security-solution-plugin/server/lib/detection_engine/prebuilt_rules'; import { FtrProviderContext } from '../../../../../../ftr_provider_context'; @@ -246,6 +249,41 @@ export default ({ getService }: FtrProviderContext): void => { expect(installedRule.tags).toEqual(reviewRuleResponseMap.get(ruleId)?.tags); } }); + + it('correctly upgrades rules with DataSource diffs to their MERGED versions', async () => { + await createHistoricalPrebuiltRuleAssetSavedObjects(es, [queryRule]); + await installPrebuiltRules(es, supertest); + + const targetObject = cloneDeep(queryRule); + targetObject['security-rule'].version += 1; + targetObject['security-rule'].name = TARGET_NAME; + targetObject['security-rule'].tags = TARGET_TAGS; + targetObject['security-rule'].index = ['auditbeat-*']; + await createHistoricalPrebuiltRuleAssetSavedObjects(es, [targetObject]); + + const reviewResponse = await reviewPrebuiltRulesToUpgrade(supertest); + const ruleDiffFields = reviewResponse.rules[0].diff.fields as AllFieldsDiff; + + const performUpgradeResponse = await performUpgradePrebuiltRules(es, supertest, { + mode: ModeEnum.ALL_RULES, + pick_version: 'MERGED', + }); + + expect(performUpgradeResponse.summary.succeeded).toEqual(1); + + const installedRules = await getInstalledRules(supertest); + const installedRule = installedRules.data[0] as QueryRule; + + expect(installedRule.name).toEqual(ruleDiffFields.name.merged_version); + expect(installedRule.tags).toEqual(ruleDiffFields.tags.merged_version); + + // Check that the updated rules has an `index` field which equals the output of the diff algorithm + // for the DataSource diffable field, and that the data_view_id is correspondingly set to undefined. + expect(installedRule.index).toEqual( + (ruleDiffFields.data_source.merged_version as DataSourceIndexPatterns).index_patterns + ); + expect(installedRule.data_view_id).toBe(undefined); + }); }); describe('edge cases and unhappy paths', () => { From ab021a52668203d19fb2651fcada0720da9c5120 Mon Sep 17 00:00:00 2001 From: Pablo Machado Date: Wed, 23 Oct 2024 16:52:16 +0200 Subject: [PATCH 09/14] [SecuritySolution] Add tooltip to entities table (#197430) ## Summary Add a tooltip to the entities table. ![Screenshot 2024-10-23 at 14 58 52](https://github.com/user-attachments/assets/4766e3bd-9e88-4c9a-9d8f-653c56de6fdd) --- .../components/entity_store/entities_list.tsx | 26 ++++++++++--------- .../components/paginated_table/index.tsx | 4 +++ 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/entities_list.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/entities_list.tsx index fc821bead61f0..1b7c77ceda123 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/entities_list.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/entities_list.tsx @@ -6,9 +6,8 @@ */ import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { EuiFilterGroup, EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; +import { EuiFilterGroup, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { noop } from 'lodash/fp'; -import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; import { useErrorToast } from '../../../common/hooks/use_error_toast'; import type { CriticalityLevels } from '../../../../common/constants'; @@ -116,16 +115,19 @@ export const EntitiesList: React.FC = () => { activePage={(data?.page ?? 1) - 1} columns={columns} headerCount={data?.total ?? 0} - headerTitle={ - -

- -

-
- } + titleSize="s" + headerTitle={i18n.translate( + 'xpack.securitySolution.entityAnalytics.entityStore.entitiesList.tableTitle', + { + defaultMessage: 'Entities', + } + )} + headerTooltip={i18n.translate( + 'xpack.securitySolution.entityAnalytics.entityStore.entitiesList.tableTooltip', + { + defaultMessage: 'Entity data can take a couple of minutes to appear', + } + )} limit={limit} loading={isLoading || isRefetching} isInspect={false} diff --git a/x-pack/plugins/security_solution/public/explore/components/paginated_table/index.tsx b/x-pack/plugins/security_solution/public/explore/components/paginated_table/index.tsx index 82cde8a24f153..7fe4f0c0110b0 100644 --- a/x-pack/plugins/security_solution/public/explore/components/paginated_table/index.tsx +++ b/x-pack/plugins/security_solution/public/explore/components/paginated_table/index.tsx @@ -9,6 +9,7 @@ import type { EuiBasicTableProps, EuiGlobalToastListToast as Toast, EuiTableRowCellProps, + EuiTitleSize, } from '@elastic/eui'; import { EuiBasicTable, @@ -107,6 +108,7 @@ export interface BasicTableProps { dataTestSubj?: string; headerCount: number; headerFilters?: string | React.ReactNode; + titleSize?: EuiTitleSize; headerSupplement?: React.ReactElement; headerTitle: string | React.ReactElement; headerTooltip?: string; @@ -148,6 +150,7 @@ const PaginatedTableComponent: FC = ({ dataTestSubj = DEFAULT_DATA_TEST_SUBJ, headerCount, headerFilters, + titleSize, headerSupplement, headerTitle, headerTooltip, @@ -277,6 +280,7 @@ const PaginatedTableComponent: FC = ({ Date: Wed, 23 Oct 2024 07:55:49 -0700 Subject: [PATCH 10/14] [ResponseOps] Prepare the connector `create` HTTP API for versioning (#194879) Towards https://github.com/elastic/response-ops-team/issues/125 ## Summary Preparing the `POST ${BASE_ACTION_API_PATH}/connector/{id?}` HTTP API for versioning --- x-pack/plugins/actions/common/index.ts | 1 + .../routes/connector/apis/create/index.ts | 21 +++ .../connector/apis/create/schemas}/latest.ts | 2 +- .../connector/apis/create/schemas/v1.ts | 32 ++++ .../connector/apis/create/types/latest.ts | 8 + .../routes/connector/apis/create/types/v1.ts | 12 ++ .../server/actions_client/actions_client.ts | 176 +----------------- .../connector/methods/create/create.ts | 170 +++++++++++++++++ .../connector/methods/create/index.ts | 8 + .../connector/methods/create/types/index.ts | 8 + .../connector/methods/create/types/types.ts | 22 +++ .../transforms => common_transforms}/index.ts | 4 +- .../transform_connector_response/latest.ts | 8 + .../transform_connector_response/v1.ts | 6 +- .../{ => connector/create}/create.test.ts | 27 +-- .../server/routes/connector/create/create.ts | 58 ++++++ .../server/routes/connector/create/index.ts | 8 + .../connector/create/transforms/index.ts | 10 + .../transform_connector_body/latest.ts | 8 + .../transforms/transform_connector_body/v1.ts | 21 +++ .../server/routes/connector/get/get.ts | 4 +- .../plugins/actions/server/routes/create.ts | 98 ---------- x-pack/plugins/actions/server/routes/index.ts | 4 +- .../endpoint/common/connectors_services.ts | 7 +- 24 files changed, 427 insertions(+), 296 deletions(-) create mode 100644 x-pack/plugins/actions/common/routes/connector/apis/create/index.ts rename x-pack/plugins/actions/{server/routes/connector/get/transforms/transform_connector_response => common/routes/connector/apis/create/schemas}/latest.ts (82%) create mode 100644 x-pack/plugins/actions/common/routes/connector/apis/create/schemas/v1.ts create mode 100644 x-pack/plugins/actions/common/routes/connector/apis/create/types/latest.ts create mode 100644 x-pack/plugins/actions/common/routes/connector/apis/create/types/v1.ts create mode 100644 x-pack/plugins/actions/server/application/connector/methods/create/create.ts create mode 100644 x-pack/plugins/actions/server/application/connector/methods/create/index.ts create mode 100644 x-pack/plugins/actions/server/application/connector/methods/create/types/index.ts create mode 100644 x-pack/plugins/actions/server/application/connector/methods/create/types/types.ts rename x-pack/plugins/actions/server/routes/connector/{get/transforms => common_transforms}/index.ts (55%) create mode 100644 x-pack/plugins/actions/server/routes/connector/common_transforms/transform_connector_response/latest.ts rename x-pack/plugins/actions/server/routes/connector/{get/transforms => common_transforms}/transform_connector_response/v1.ts (73%) rename x-pack/plugins/actions/server/routes/{ => connector/create}/create.test.ts (84%) create mode 100644 x-pack/plugins/actions/server/routes/connector/create/create.ts create mode 100644 x-pack/plugins/actions/server/routes/connector/create/index.ts create mode 100644 x-pack/plugins/actions/server/routes/connector/create/transforms/index.ts create mode 100644 x-pack/plugins/actions/server/routes/connector/create/transforms/transform_connector_body/latest.ts create mode 100644 x-pack/plugins/actions/server/routes/connector/create/transforms/transform_connector_body/v1.ts delete mode 100644 x-pack/plugins/actions/server/routes/create.ts diff --git a/x-pack/plugins/actions/common/index.ts b/x-pack/plugins/actions/common/index.ts index b56f6c61238c2..336ff003962a0 100644 --- a/x-pack/plugins/actions/common/index.ts +++ b/x-pack/plugins/actions/common/index.ts @@ -15,6 +15,7 @@ export * from './mustache_template'; export * from './validate_email_addresses'; export * from './connector_feature_config'; export * from './execution_log_types'; +export * from './validate_empty_strings'; export const BASE_ACTION_API_PATH = '/api/actions'; export const INTERNAL_BASE_ACTION_API_PATH = '/internal/actions'; diff --git a/x-pack/plugins/actions/common/routes/connector/apis/create/index.ts b/x-pack/plugins/actions/common/routes/connector/apis/create/index.ts new file mode 100644 index 0000000000000..a40970eadc099 --- /dev/null +++ b/x-pack/plugins/actions/common/routes/connector/apis/create/index.ts @@ -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. + */ + +export { + createConnectorRequestParamsSchema, + createConnectorRequestBodySchema, +} from './schemas/latest'; +export type { CreateConnectorRequestParams, CreateConnectorRequestBody } from './types/latest'; + +export { + createConnectorRequestParamsSchema as createConnectorRequestParamsSchemaV1, + createConnectorRequestBodySchema as createConnectorRequestBodySchemaV1, +} from './schemas/v1'; +export type { + CreateConnectorRequestParams as CreateConnectorRequestParamsV1, + CreateConnectorRequestBody as CreateConnectorRequestBodyV1, +} from './types/v1'; diff --git a/x-pack/plugins/actions/server/routes/connector/get/transforms/transform_connector_response/latest.ts b/x-pack/plugins/actions/common/routes/connector/apis/create/schemas/latest.ts similarity index 82% rename from x-pack/plugins/actions/server/routes/connector/get/transforms/transform_connector_response/latest.ts rename to x-pack/plugins/actions/common/routes/connector/apis/create/schemas/latest.ts index 8a64b2c81df0c..25300c97a6d2e 100644 --- a/x-pack/plugins/actions/server/routes/connector/get/transforms/transform_connector_response/latest.ts +++ b/x-pack/plugins/actions/common/routes/connector/apis/create/schemas/latest.ts @@ -5,4 +5,4 @@ * 2.0. */ -export { transformGetConnectorResponse } from './v1'; +export * from './v1'; diff --git a/x-pack/plugins/actions/common/routes/connector/apis/create/schemas/v1.ts b/x-pack/plugins/actions/common/routes/connector/apis/create/schemas/v1.ts new file mode 100644 index 0000000000000..ec74ba1a9279f --- /dev/null +++ b/x-pack/plugins/actions/common/routes/connector/apis/create/schemas/v1.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 { schema } from '@kbn/config-schema'; +import { validateEmptyStrings } from '../../../../../validate_empty_strings'; + +export const createConnectorRequestParamsSchema = schema.maybe( + schema.object({ + id: schema.maybe(schema.string({ meta: { description: 'An identifier for the connector.' } })), + }) +); + +export const createConnectorRequestBodySchema = schema.object({ + name: schema.string({ + validate: validateEmptyStrings, + meta: { description: 'The display name for the connector.' }, + }), + connector_type_id: schema.string({ + validate: validateEmptyStrings, + meta: { description: 'The type of connector.' }, + }), + config: schema.recordOf(schema.string(), schema.any({ validate: validateEmptyStrings }), { + defaultValue: {}, + }), + secrets: schema.recordOf(schema.string(), schema.any({ validate: validateEmptyStrings }), { + defaultValue: {}, + }), +}); diff --git a/x-pack/plugins/actions/common/routes/connector/apis/create/types/latest.ts b/x-pack/plugins/actions/common/routes/connector/apis/create/types/latest.ts new file mode 100644 index 0000000000000..25300c97a6d2e --- /dev/null +++ b/x-pack/plugins/actions/common/routes/connector/apis/create/types/latest.ts @@ -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 * from './v1'; diff --git a/x-pack/plugins/actions/common/routes/connector/apis/create/types/v1.ts b/x-pack/plugins/actions/common/routes/connector/apis/create/types/v1.ts new file mode 100644 index 0000000000000..2166ee5a712a6 --- /dev/null +++ b/x-pack/plugins/actions/common/routes/connector/apis/create/types/v1.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. + */ + +import type { TypeOf } from '@kbn/config-schema'; +import { createConnectorRequestParamsSchemaV1, createConnectorRequestBodySchemaV1 } from '..'; + +export type CreateConnectorRequestParams = TypeOf; +export type CreateConnectorRequestBody = TypeOf; diff --git a/x-pack/plugins/actions/server/actions_client/actions_client.ts b/x-pack/plugins/actions/server/actions_client/actions_client.ts index edad072acbca6..de029ed2acf54 100644 --- a/x-pack/plugins/actions/server/actions_client/actions_client.ts +++ b/x-pack/plugins/actions/server/actions_client/actions_client.ts @@ -14,9 +14,7 @@ import { compact, uniq } from 'lodash'; import { IScopedClusterClient, SavedObjectsClientContract, - SavedObjectAttributes, KibanaRequest, - SavedObjectsUtils, Logger, } from '@kbn/core/server'; import { AuditLogger } from '@kbn/security-plugin/server'; @@ -29,6 +27,7 @@ import { get } from '../application/connector/methods/get'; import { getAll, getAllSystemConnectors } from '../application/connector/methods/get_all'; import { update } from '../application/connector/methods/update'; import { listTypes } from '../application/connector/methods/list_types'; +import { create } from '../application/connector/methods/create'; import { execute } from '../application/connector/methods/execute'; import { GetGlobalExecutionKPIParams, @@ -36,15 +35,7 @@ import { IExecutionLogResult, } from '../../common'; import { ActionTypeRegistry } from '../action_type_registry'; -import { - validateConfig, - validateSecrets, - ActionExecutorContract, - validateConnector, - ActionExecutionSource, - parseDate, - tryCatch, -} from '../lib'; +import { ActionExecutorContract, ActionExecutionSource, parseDate } from '../lib'; import { ActionResult, RawAction, @@ -94,20 +85,11 @@ import { import { connectorFromSavedObject, isConnectorDeprecated } from '../application/connector/lib'; import { ListTypesParams } from '../application/connector/methods/list_types/types'; import { ConnectorUpdateParams } from '../application/connector/methods/update/types'; -import { ConnectorUpdate } from '../application/connector/methods/update/types/types'; +import { ConnectorCreateParams } from '../application/connector/methods/create/types'; import { isPreconfigured } from '../lib/is_preconfigured'; import { isSystemAction } from '../lib/is_system_action'; import { ConnectorExecuteParams } from '../application/connector/methods/execute/types'; -interface Action extends ConnectorUpdate { - actionTypeId: string; -} - -export interface CreateOptions { - action: Action; - options?: { id?: string }; -} - export interface ConstructorOptions { logger: Logger; kibanaIndices: string[]; @@ -187,156 +169,10 @@ export class ActionsClient { * Create an action */ public async create({ - action: { actionTypeId, name, config, secrets }, + action, options, - }: CreateOptions): Promise { - const id = options?.id || SavedObjectsUtils.generateId(); - - try { - await this.context.authorization.ensureAuthorized({ - operation: 'create', - actionTypeId, - }); - } catch (error) { - this.context.auditLogger?.log( - connectorAuditEvent({ - action: ConnectorAuditAction.CREATE, - savedObject: { type: 'action', id }, - error, - }) - ); - throw error; - } - - const foundInMemoryConnector = this.context.inMemoryConnectors.find( - (connector) => connector.id === id - ); - - if ( - this.context.actionTypeRegistry.isSystemActionType(actionTypeId) || - foundInMemoryConnector?.isSystemAction - ) { - throw Boom.badRequest( - i18n.translate('xpack.actions.serverSideErrors.systemActionCreationForbidden', { - defaultMessage: 'System action creation is forbidden. Action type: {actionTypeId}.', - values: { - actionTypeId, - }, - }) - ); - } - - if (foundInMemoryConnector?.isPreconfigured) { - throw Boom.badRequest( - i18n.translate('xpack.actions.serverSideErrors.predefinedIdConnectorAlreadyExists', { - defaultMessage: 'This {id} already exists in a preconfigured action.', - values: { - id, - }, - }) - ); - } - - const actionType = this.context.actionTypeRegistry.get(actionTypeId); - const configurationUtilities = this.context.actionTypeRegistry.getUtils(); - const validatedActionTypeConfig = validateConfig(actionType, config, { - configurationUtilities, - }); - const validatedActionTypeSecrets = validateSecrets(actionType, secrets, { - configurationUtilities, - }); - if (actionType.validate?.connector) { - validateConnector(actionType, { config, secrets }); - } - this.context.actionTypeRegistry.ensureActionTypeEnabled(actionTypeId); - - const hookServices: HookServices = { - scopedClusterClient: this.context.scopedClusterClient, - }; - - if (actionType.preSaveHook) { - try { - await actionType.preSaveHook({ - connectorId: id, - config, - secrets, - logger: this.context.logger, - request: this.context.request, - services: hookServices, - isUpdate: false, - }); - } catch (error) { - this.context.auditLogger?.log( - connectorAuditEvent({ - action: ConnectorAuditAction.CREATE, - savedObject: { type: 'action', id }, - error, - }) - ); - throw error; - } - } - - this.context.auditLogger?.log( - connectorAuditEvent({ - action: ConnectorAuditAction.CREATE, - savedObject: { type: 'action', id }, - outcome: 'unknown', - }) - ); - - const result = await tryCatch( - async () => - await this.context.unsecuredSavedObjectsClient.create( - 'action', - { - actionTypeId, - name, - isMissingSecrets: false, - config: validatedActionTypeConfig as SavedObjectAttributes, - secrets: validatedActionTypeSecrets as SavedObjectAttributes, - }, - { id } - ) - ); - - const wasSuccessful = !(result instanceof Error); - const label = `connectorId: "${id}"; type: ${actionTypeId}`; - const tags = ['post-save-hook', id]; - - if (actionType.postSaveHook) { - try { - await actionType.postSaveHook({ - connectorId: id, - config, - secrets, - logger: this.context.logger, - request: this.context.request, - services: hookServices, - isUpdate: false, - wasSuccessful, - }); - } catch (err) { - this.context.logger.error(`postSaveHook create error for ${label}: ${err.message}`, { - tags, - }); - } - } - - if (!wasSuccessful) { - throw result; - } - - return { - id: result.id, - actionTypeId: result.attributes.actionTypeId, - isMissingSecrets: result.attributes.isMissingSecrets, - name: result.attributes.name, - config: result.attributes.config, - isPreconfigured: false, - isSystemAction: false, - isDeprecated: isConnectorDeprecated(result.attributes), - }; + }: Omit): Promise { + return create({ context: this.context, action, options }); } /** diff --git a/x-pack/plugins/actions/server/application/connector/methods/create/create.ts b/x-pack/plugins/actions/server/application/connector/methods/create/create.ts new file mode 100644 index 0000000000000..3032b38cb36f2 --- /dev/null +++ b/x-pack/plugins/actions/server/application/connector/methods/create/create.ts @@ -0,0 +1,170 @@ +/* + * 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 Boom from '@hapi/boom'; +import { i18n } from '@kbn/i18n'; +import { SavedObjectAttributes, SavedObjectsUtils } from '@kbn/core/server'; +import { ConnectorCreateParams } from './types'; +import { ConnectorAuditAction, connectorAuditEvent } from '../../../../lib/audit_events'; +import { validateConfig, validateConnector, validateSecrets } from '../../../../lib'; +import { isConnectorDeprecated } from '../../lib'; +import { HookServices, ActionResult } from '../../../../types'; +import { tryCatch } from '../../../../lib'; + +export async function create({ + context, + action: { actionTypeId, name, config, secrets }, + options, +}: ConnectorCreateParams): Promise { + const id = options?.id || SavedObjectsUtils.generateId(); + + try { + await context.authorization.ensureAuthorized({ + operation: 'create', + actionTypeId, + }); + } catch (error) { + context.auditLogger?.log( + connectorAuditEvent({ + action: ConnectorAuditAction.CREATE, + savedObject: { type: 'action', id }, + error, + }) + ); + throw error; + } + + const foundInMemoryConnector = context.inMemoryConnectors.find( + (connector) => connector.id === id + ); + + if ( + context.actionTypeRegistry.isSystemActionType(actionTypeId) || + foundInMemoryConnector?.isSystemAction + ) { + throw Boom.badRequest( + i18n.translate('xpack.actions.serverSideErrors.systemActionCreationForbidden', { + defaultMessage: 'System action creation is forbidden. Action type: {actionTypeId}.', + values: { + actionTypeId, + }, + }) + ); + } + + if (foundInMemoryConnector?.isPreconfigured) { + throw Boom.badRequest( + i18n.translate('xpack.actions.serverSideErrors.predefinedIdConnectorAlreadyExists', { + defaultMessage: 'This {id} already exists in a preconfigured action.', + values: { + id, + }, + }) + ); + } + + const actionType = context.actionTypeRegistry.get(actionTypeId); + const configurationUtilities = context.actionTypeRegistry.getUtils(); + const validatedActionTypeConfig = validateConfig(actionType, config, { + configurationUtilities, + }); + const validatedActionTypeSecrets = validateSecrets(actionType, secrets, { + configurationUtilities, + }); + if (actionType.validate?.connector) { + validateConnector(actionType, { config, secrets }); + } + context.actionTypeRegistry.ensureActionTypeEnabled(actionTypeId); + + const hookServices: HookServices = { + scopedClusterClient: context.scopedClusterClient, + }; + + if (actionType.preSaveHook) { + try { + await actionType.preSaveHook({ + connectorId: id, + config, + secrets, + logger: context.logger, + request: context.request, + services: hookServices, + isUpdate: false, + }); + } catch (error) { + context.auditLogger?.log( + connectorAuditEvent({ + action: ConnectorAuditAction.CREATE, + savedObject: { type: 'action', id }, + error, + }) + ); + throw error; + } + } + + context.auditLogger?.log( + connectorAuditEvent({ + action: ConnectorAuditAction.CREATE, + savedObject: { type: 'action', id }, + outcome: 'unknown', + }) + ); + + const result = await tryCatch( + async () => + await context.unsecuredSavedObjectsClient.create( + 'action', + { + actionTypeId, + name, + isMissingSecrets: false, + config: validatedActionTypeConfig as SavedObjectAttributes, + secrets: validatedActionTypeSecrets as SavedObjectAttributes, + }, + { id } + ) + ); + + const wasSuccessful = !(result instanceof Error); + const label = `connectorId: "${id}"; type: ${actionTypeId}`; + const tags = ['post-save-hook', id]; + + if (actionType.postSaveHook) { + try { + await actionType.postSaveHook({ + connectorId: id, + config, + secrets, + logger: context.logger, + request: context.request, + services: hookServices, + isUpdate: false, + wasSuccessful, + }); + } catch (err) { + context.logger.error(`postSaveHook create error for ${label}: ${err.message}`, { + tags, + }); + } + } + + if (!wasSuccessful) { + throw result; + } + + return { + id: result.id, + actionTypeId: result.attributes.actionTypeId, + isMissingSecrets: result.attributes.isMissingSecrets, + name: result.attributes.name, + config: result.attributes.config, + isPreconfigured: false, + isSystemAction: false, + isDeprecated: isConnectorDeprecated(result.attributes), + }; +} diff --git a/x-pack/plugins/actions/server/application/connector/methods/create/index.ts b/x-pack/plugins/actions/server/application/connector/methods/create/index.ts new file mode 100644 index 0000000000000..97fcd17a79499 --- /dev/null +++ b/x-pack/plugins/actions/server/application/connector/methods/create/index.ts @@ -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 { create } from './create'; diff --git a/x-pack/plugins/actions/server/application/connector/methods/create/types/index.ts b/x-pack/plugins/actions/server/application/connector/methods/create/types/index.ts new file mode 100644 index 0000000000000..707a3fd770f2d --- /dev/null +++ b/x-pack/plugins/actions/server/application/connector/methods/create/types/index.ts @@ -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 type { ConnectorCreateParams } from './types'; diff --git a/x-pack/plugins/actions/server/application/connector/methods/create/types/types.ts b/x-pack/plugins/actions/server/application/connector/methods/create/types/types.ts new file mode 100644 index 0000000000000..2dc68d59ebe47 --- /dev/null +++ b/x-pack/plugins/actions/server/application/connector/methods/create/types/types.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 { SavedObjectAttributes } from '@kbn/core/server'; +import { ActionsClientContext } from '../../../../../actions_client'; + +export interface ConnectorCreate { + actionTypeId: string; + name: string; + config: SavedObjectAttributes; + secrets: SavedObjectAttributes; +} + +export interface ConnectorCreateParams { + context: ActionsClientContext; + action: ConnectorCreate; + options?: { id?: string }; +} diff --git a/x-pack/plugins/actions/server/routes/connector/get/transforms/index.ts b/x-pack/plugins/actions/server/routes/connector/common_transforms/index.ts similarity index 55% rename from x-pack/plugins/actions/server/routes/connector/get/transforms/index.ts rename to x-pack/plugins/actions/server/routes/connector/common_transforms/index.ts index 44960ddfeec0d..0e26b4ee295a6 100644 --- a/x-pack/plugins/actions/server/routes/connector/get/transforms/index.ts +++ b/x-pack/plugins/actions/server/routes/connector/common_transforms/index.ts @@ -5,6 +5,6 @@ * 2.0. */ -export { transformGetConnectorResponse } from './transform_connector_response/latest'; +export { transformConnectorResponse } from './transform_connector_response/latest'; -export { transformGetConnectorResponse as transformGetConnectorResponseV1 } from './transform_connector_response/v1'; +export { transformConnectorResponse as transformConnectorResponseV1 } from './transform_connector_response/v1'; diff --git a/x-pack/plugins/actions/server/routes/connector/common_transforms/transform_connector_response/latest.ts b/x-pack/plugins/actions/server/routes/connector/common_transforms/transform_connector_response/latest.ts new file mode 100644 index 0000000000000..99b38410e71a8 --- /dev/null +++ b/x-pack/plugins/actions/server/routes/connector/common_transforms/transform_connector_response/latest.ts @@ -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 { transformConnectorResponse } from './v1'; diff --git a/x-pack/plugins/actions/server/routes/connector/get/transforms/transform_connector_response/v1.ts b/x-pack/plugins/actions/server/routes/connector/common_transforms/transform_connector_response/v1.ts similarity index 73% rename from x-pack/plugins/actions/server/routes/connector/get/transforms/transform_connector_response/v1.ts rename to x-pack/plugins/actions/server/routes/connector/common_transforms/transform_connector_response/v1.ts index ab6f6332280d1..08b32a10e7e45 100644 --- a/x-pack/plugins/actions/server/routes/connector/get/transforms/transform_connector_response/v1.ts +++ b/x-pack/plugins/actions/server/routes/connector/common_transforms/transform_connector_response/v1.ts @@ -5,10 +5,10 @@ * 2.0. */ -import { ConnectorResponseV1 } from '../../../../../../common/routes/connector/response'; -import { Connector } from '../../../../../application/connector/types'; +import { ConnectorResponseV1 } from '../../../../../common/routes/connector/response'; +import { Connector } from '../../../../application/connector/types'; -export const transformGetConnectorResponse = ({ +export const transformConnectorResponse = ({ actionTypeId, isPreconfigured, isMissingSecrets, diff --git a/x-pack/plugins/actions/server/routes/create.test.ts b/x-pack/plugins/actions/server/routes/connector/create/create.test.ts similarity index 84% rename from x-pack/plugins/actions/server/routes/create.test.ts rename to x-pack/plugins/actions/server/routes/connector/create/create.test.ts index 7d9c7fd1a5899..0f97015bd4f01 100644 --- a/x-pack/plugins/actions/server/routes/create.test.ts +++ b/x-pack/plugins/actions/server/routes/connector/create/create.test.ts @@ -5,15 +5,16 @@ * 2.0. */ -import { createActionRoute, bodySchema } from './create'; +import { createConnectorRoute } from './create'; import { httpServiceMock } from '@kbn/core/server/mocks'; -import { licenseStateMock } from '../lib/license_state.mock'; -import { mockHandlerArguments } from './legacy/_mock_handler_arguments'; -import { verifyAccessAndContext } from './verify_access_and_context'; +import { licenseStateMock } from '../../../lib/license_state.mock'; +import { mockHandlerArguments } from '../../legacy/_mock_handler_arguments'; +import { verifyAccessAndContext } from '../../verify_access_and_context'; import { omit } from 'lodash'; -import { actionsClientMock } from '../actions_client/actions_client.mock'; +import { actionsClientMock } from '../../../actions_client/actions_client.mock'; +import { createConnectorRequestBodySchemaV1 } from '../../../../common/routes/connector/apis/create'; -jest.mock('./verify_access_and_context', () => ({ +jest.mock('../../verify_access_and_context', () => ({ verifyAccessAndContext: jest.fn(), })); @@ -22,12 +23,12 @@ beforeEach(() => { (verifyAccessAndContext as jest.Mock).mockImplementation((license, handler) => handler); }); -describe('createActionRoute', () => { +describe('createConnectorRoute', () => { it('creates an action with proper parameters', async () => { const licenseState = licenseStateMock.create(); const router = httpServiceMock.createRouter(); - createActionRoute(router, licenseState); + createConnectorRoute(router, licenseState); const [config, handler] = router.post.mock.calls[0]; @@ -103,7 +104,7 @@ describe('createActionRoute', () => { const licenseState = licenseStateMock.create(); const router = httpServiceMock.createRouter(); - createActionRoute(router, licenseState); + createConnectorRoute(router, licenseState); const [, handler] = router.post.mock.calls[0]; @@ -144,7 +145,7 @@ describe('createActionRoute', () => { throw new Error('OMG'); }); - createActionRoute(router, licenseState); + createConnectorRoute(router, licenseState); const [, handler] = router.post.mock.calls[0]; @@ -182,8 +183,8 @@ describe('createActionRoute', () => { config: { foo: ' ' }, secrets: {}, }; - expect(() => bodySchema.validate(body)).toThrowErrorMatchingInlineSnapshot( - `"[config.foo]: value '' is not valid"` - ); + expect(() => + createConnectorRequestBodySchemaV1.validate(body) + ).toThrowErrorMatchingInlineSnapshot(`"[config.foo]: value '' is not valid"`); }); }); diff --git a/x-pack/plugins/actions/server/routes/connector/create/create.ts b/x-pack/plugins/actions/server/routes/connector/create/create.ts new file mode 100644 index 0000000000000..cd5073506c03f --- /dev/null +++ b/x-pack/plugins/actions/server/routes/connector/create/create.ts @@ -0,0 +1,58 @@ +/* + * 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 { IRouter } from '@kbn/core/server'; +import { ActionsRequestHandlerContext } from '../../../types'; +import { ILicenseState } from '../../../lib'; +import { BASE_ACTION_API_PATH } from '../../../../common'; +import { verifyAccessAndContext } from '../../verify_access_and_context'; +import { connectorResponseSchemaV1 } from '../../../../common/routes/connector/response'; +import { transformConnectorResponseV1 } from '../common_transforms'; +import { + createConnectorRequestParamsSchemaV1, + createConnectorRequestBodySchemaV1, +} from '../../../../common/routes/connector/apis/create'; +import { transformCreateConnectorBodyV1 } from './transforms'; + +export const createConnectorRoute = ( + router: IRouter, + licenseState: ILicenseState +) => { + router.post( + { + path: `${BASE_ACTION_API_PATH}/connector/{id?}`, + options: { + access: 'public', + summary: 'Create a connector', + tags: ['oas-tag:connectors'], + }, + validate: { + request: { + params: createConnectorRequestParamsSchemaV1, + body: createConnectorRequestBodySchemaV1, + }, + response: { + 200: { + description: 'Indicates a successful call.', + body: () => connectorResponseSchemaV1, + }, + }, + }, + }, + router.handleLegacyErrors( + verifyAccessAndContext(licenseState, async function (context, req, res) { + const actionsClient = (await context.actions).getActionsClient(); + const action = transformCreateConnectorBodyV1(req.body); + return res.ok({ + body: transformConnectorResponseV1( + await actionsClient.create({ action, options: req.params }) + ), + }); + }) + ) + ); +}; diff --git a/x-pack/plugins/actions/server/routes/connector/create/index.ts b/x-pack/plugins/actions/server/routes/connector/create/index.ts new file mode 100644 index 0000000000000..47f4f6a55eb71 --- /dev/null +++ b/x-pack/plugins/actions/server/routes/connector/create/index.ts @@ -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 { createConnectorRoute } from './create'; diff --git a/x-pack/plugins/actions/server/routes/connector/create/transforms/index.ts b/x-pack/plugins/actions/server/routes/connector/create/transforms/index.ts new file mode 100644 index 0000000000000..76f6e098f0564 --- /dev/null +++ b/x-pack/plugins/actions/server/routes/connector/create/transforms/index.ts @@ -0,0 +1,10 @@ +/* + * 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 { transformCreateConnectorBody } from './transform_connector_body/latest'; + +export { transformCreateConnectorBody as transformCreateConnectorBodyV1 } from './transform_connector_body/v1'; diff --git a/x-pack/plugins/actions/server/routes/connector/create/transforms/transform_connector_body/latest.ts b/x-pack/plugins/actions/server/routes/connector/create/transforms/transform_connector_body/latest.ts new file mode 100644 index 0000000000000..4584455ddf5c2 --- /dev/null +++ b/x-pack/plugins/actions/server/routes/connector/create/transforms/transform_connector_body/latest.ts @@ -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 { transformCreateConnectorBody } from './v1'; diff --git a/x-pack/plugins/actions/server/routes/connector/create/transforms/transform_connector_body/v1.ts b/x-pack/plugins/actions/server/routes/connector/create/transforms/transform_connector_body/v1.ts new file mode 100644 index 0000000000000..07d7a5e7afa4c --- /dev/null +++ b/x-pack/plugins/actions/server/routes/connector/create/transforms/transform_connector_body/v1.ts @@ -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 { ConnectorCreateParams } from '../../../../../application/connector/methods/create/types'; +import { CreateConnectorRequestBodyV1 } from '../../../../../../common/routes/connector/apis/create'; + +export const transformCreateConnectorBody = ({ + connector_type_id: actionTypeId, + name, + config, + secrets, +}: CreateConnectorRequestBodyV1): ConnectorCreateParams['action'] => ({ + actionTypeId, + name, + config, + secrets, +}); diff --git a/x-pack/plugins/actions/server/routes/connector/get/get.ts b/x-pack/plugins/actions/server/routes/connector/get/get.ts index 686b54655c892..eaab31594ba1b 100644 --- a/x-pack/plugins/actions/server/routes/connector/get/get.ts +++ b/x-pack/plugins/actions/server/routes/connector/get/get.ts @@ -11,7 +11,7 @@ import { GetConnectorParamsV1, } from '../../../../common/routes/connector/apis/get'; import { connectorResponseSchemaV1 } from '../../../../common/routes/connector/response'; -import { transformGetConnectorResponseV1 } from './transforms'; +import { transformConnectorResponseV1 } from '../common_transforms'; import { ILicenseState } from '../../../lib'; import { BASE_ACTION_API_PATH } from '../../../../common'; import { ActionsRequestHandlerContext } from '../../../types'; @@ -46,7 +46,7 @@ export const getConnectorRoute = ( const actionsClient = (await context.actions).getActionsClient(); const { id }: GetConnectorParamsV1 = req.params; return res.ok({ - body: transformGetConnectorResponseV1(await actionsClient.get({ id })), + body: transformConnectorResponseV1(await actionsClient.get({ id })), }); }) ) diff --git a/x-pack/plugins/actions/server/routes/create.ts b/x-pack/plugins/actions/server/routes/create.ts deleted file mode 100644 index 25962701918a5..0000000000000 --- a/x-pack/plugins/actions/server/routes/create.ts +++ /dev/null @@ -1,98 +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 { schema } from '@kbn/config-schema'; -import { IRouter } from '@kbn/core/server'; -import { ActionResult, ActionsRequestHandlerContext } from '../types'; -import { ILicenseState } from '../lib'; -import { BASE_ACTION_API_PATH, RewriteRequestCase, RewriteResponseCase } from '../../common'; -import { verifyAccessAndContext } from './verify_access_and_context'; -import { CreateOptions } from '../actions_client'; -import { connectorResponseSchemaV1 } from '../../common/routes/connector/response'; -import { validateEmptyStrings } from '../../common/validate_empty_strings'; - -export const bodySchema = schema.object({ - name: schema.string({ - validate: validateEmptyStrings, - meta: { description: 'The display name for the connector.' }, - }), - connector_type_id: schema.string({ - validate: validateEmptyStrings, - meta: { description: 'The type of connector.' }, - }), - config: schema.recordOf(schema.string(), schema.any({ validate: validateEmptyStrings }), { - defaultValue: {}, - }), - secrets: schema.recordOf(schema.string(), schema.any({ validate: validateEmptyStrings }), { - defaultValue: {}, - }), -}); - -const rewriteBodyReq: RewriteRequestCase = ({ - connector_type_id: actionTypeId, - name, - config, - secrets, -}) => ({ actionTypeId, name, config, secrets }); -const rewriteBodyRes: RewriteResponseCase = ({ - actionTypeId, - isPreconfigured, - isDeprecated, - isMissingSecrets, - isSystemAction, - ...res -}) => ({ - ...res, - connector_type_id: actionTypeId, - is_preconfigured: isPreconfigured, - is_deprecated: isDeprecated, - is_missing_secrets: isMissingSecrets, - is_system_action: isSystemAction, -}); - -export const createActionRoute = ( - router: IRouter, - licenseState: ILicenseState -) => { - router.post( - { - path: `${BASE_ACTION_API_PATH}/connector/{id?}`, - options: { - access: 'public', - summary: 'Create a connector', - tags: ['oas-tag:connectors'], - }, - validate: { - request: { - params: schema.maybe( - schema.object({ - id: schema.maybe( - schema.string({ meta: { description: 'An identifier for the connector.' } }) - ), - }) - ), - body: bodySchema, - }, - response: { - 200: { - description: 'Indicates a successful call.', - body: () => connectorResponseSchemaV1, - }, - }, - }, - }, - router.handleLegacyErrors( - verifyAccessAndContext(licenseState, async function (context, req, res) { - const actionsClient = (await context.actions).getActionsClient(); - const action = rewriteBodyReq(req.body); - return res.ok({ - body: rewriteBodyRes(await actionsClient.create({ action, options: req.params })), - }); - }) - ) - ); -}; diff --git a/x-pack/plugins/actions/server/routes/index.ts b/x-pack/plugins/actions/server/routes/index.ts index 5ea804d1ce47e..39efe6619176b 100644 --- a/x-pack/plugins/actions/server/routes/index.ts +++ b/x-pack/plugins/actions/server/routes/index.ts @@ -13,7 +13,7 @@ import { listTypesRoute } from './connector/list_types'; import { listTypesWithSystemRoute } from './connector/list_types_system'; import { ILicenseState } from '../lib'; import { ActionsRequestHandlerContext } from '../types'; -import { createActionRoute } from './create'; +import { createConnectorRoute } from './connector/create'; import { deleteConnectorRoute } from './connector/delete'; import { executeConnectorRoute } from './connector/execute'; import { getConnectorRoute } from './connector/get'; @@ -36,7 +36,7 @@ export function defineRoutes(opts: RouteOptions) { defineLegacyRoutes(router, licenseState, usageCounter); - createActionRoute(router, licenseState); + createConnectorRoute(router, licenseState); deleteConnectorRoute(router, licenseState); getConnectorRoute(router, licenseState); getAllConnectorsRoute(router, licenseState); diff --git a/x-pack/plugins/security_solution/scripts/endpoint/common/connectors_services.ts b/x-pack/plugins/security_solution/scripts/endpoint/common/connectors_services.ts index 3fb2971f1236d..62ce76437c1d6 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/common/connectors_services.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/common/connectors_services.ts @@ -7,8 +7,7 @@ import type { KbnClient } from '@kbn/test'; import type { AllConnectorsResponseV1 } from '@kbn/actions-plugin/common/routes/connector/response'; -import type { bodySchema } from '@kbn/actions-plugin/server/routes/create'; -import type { TypeOf } from '@kbn/config-schema'; +import type { CreateConnectorRequestBodyV1 } from '@kbn/actions-plugin/common/routes/connector/apis/create'; import type { Connector } from '@kbn/actions-plugin/server/application/connector/types'; import { catchAxiosErrorFormatAndThrow } from '../../../common/endpoint/format_axios_error'; @@ -46,8 +45,6 @@ export const fetchConnectorByType = async ( } }; -type CreateConnectorBody = TypeOf; - /** * Creates a connector in the stack * @param kbnClient @@ -55,7 +52,7 @@ type CreateConnectorBody = TypeOf; */ export const createConnector = async ( kbnClient: KbnClient, - createPayload: CreateConnectorBody + createPayload: CreateConnectorRequestBodyV1 ): Promise => { return kbnClient .request({ From 629edc03da0c91df34b101098d822436a6682f1a Mon Sep 17 00:00:00 2001 From: Robert Oskamp Date: Wed, 23 Oct 2024 17:11:22 +0200 Subject: [PATCH 11/14] Support Kibana URL parts with stripped default port (#197418) ## Summary This PR adds support for getting Kibana URL parts with stripped default port. ### Details * Adds method `getUrlPartsWithStrippedDefaultPort` to `kbnTestConfig` * Can be used when asserting URLs where the browser strips the default port --- packages/kbn-test/kbn_test_config.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/packages/kbn-test/kbn_test_config.ts b/packages/kbn-test/kbn_test_config.ts index 1b96946bf2781..1faa3291f590e 100644 --- a/packages/kbn-test/kbn_test_config.ts +++ b/packages/kbn-test/kbn_test_config.ts @@ -54,4 +54,21 @@ export const kbnTestConfig = new (class KbnTestConfig { password, }; } + + /** + * Use to get `port:undefined` for assertions if the port is default for the + * used protocol and thus would be stripped by the browser + */ + getUrlPartsWithStrippedDefaultPort(user: UserAuth = kibanaTestUser): UrlParts { + const urlParts = this.getUrlParts(user); + + if ( + (urlParts.protocol === 'http' && urlParts.port === 80) || + (urlParts.protocol === 'https' && urlParts.port === 443) + ) { + urlParts.port = undefined; + } + + return urlParts; + } })(); From ea4ce57141019606b148016f667dd7a0cf98ff8c Mon Sep 17 00:00:00 2001 From: Liam Thompson <32779855+leemthompo@users.noreply.github.com> Date: Wed, 23 Oct 2024 17:30:13 +0200 Subject: [PATCH 12/14] [ML] Update allocations tooltip to clarify that it's per node (#197099) Clarifies text to mention nodes, conditional on being not-serverless --- .../nodes_overview/allocated_models.tsx | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/ml/public/application/memory_usage/nodes_overview/allocated_models.tsx b/x-pack/plugins/ml/public/application/memory_usage/nodes_overview/allocated_models.tsx index bce892a26b678..4dd04780e2e92 100644 --- a/x-pack/plugins/ml/public/application/memory_usage/nodes_overview/allocated_models.tsx +++ b/x-pack/plugins/ml/public/application/memory_usage/nodes_overview/allocated_models.tsx @@ -24,6 +24,7 @@ import type { NodeDeploymentStatsResponse, } from '../../../../common/types/trained_models'; import { useFieldFormatter } from '../../contexts/kibana/use_field_formatter'; +import { useEnabledFeatures } from '../../contexts/ml'; interface AllocatedModelsProps { models: NodeDeploymentStatsResponse['allocated_models']; @@ -38,6 +39,7 @@ export const AllocatedModels: FC = ({ const dateFormatter = useFieldFormatter(FIELD_FORMAT_IDS.DATE); const durationFormatter = useFieldFormatter(FIELD_FORMAT_IDS.DURATION); const euiTheme = useEuiTheme(); + const { showNodeInfo } = useEnabledFeatures(); const columns: Array> = [ { @@ -105,9 +107,20 @@ export const AllocatedModels: FC = ({ width: '8%', name: ( {i18n.translate('xpack.ml.trainedModels.nodesList.modelsList.allocationHeader', { From d15358496b8cbc153d494f9a8f70c9400ebb1d66 Mon Sep 17 00:00:00 2001 From: seanrathier Date: Wed, 23 Oct 2024 11:46:26 -0400 Subject: [PATCH 13/14] [Cloud Security] Handle critical errors returned from the Agentless API when creating and deleting agentless agents (#195997) --- x-pack/plugins/fleet/server/errors/index.ts | 10 + .../services/agents/agentless_agent.test.ts | 542 +++++++++++++++++- .../server/services/agents/agentless_agent.ts | 438 ++++++++++---- .../fleet/server/services/utils/agentless.ts | 2 +- 4 files changed, 857 insertions(+), 135 deletions(-) diff --git a/x-pack/plugins/fleet/server/errors/index.ts b/x-pack/plugins/fleet/server/errors/index.ts index 2f9b42799075f..ac6eca6b3b97d 100644 --- a/x-pack/plugins/fleet/server/errors/index.ts +++ b/x-pack/plugins/fleet/server/errors/index.ts @@ -57,6 +57,16 @@ export class AgentlessAgentCreateError extends FleetError { super(`Error creating agentless agent in Fleet, ${message}`); } } +export class AgentlessAgentDeleteError extends FleetError { + constructor(message: string) { + super(`Error deleting agentless agent in Fleet, ${message}`); + } +} +export class AgentlessAgentConfigError extends FleetError { + constructor(message: string) { + super(`Error validating Agentless API configuration in Fleet, ${message}`); + } +} export class AgentlessPolicyExistsRequestError extends AgentPolicyError { constructor(message: string) { diff --git a/x-pack/plugins/fleet/server/services/agents/agentless_agent.test.ts b/x-pack/plugins/fleet/server/services/agents/agentless_agent.test.ts index e7db96812749b..42f19d0de85bf 100644 --- a/x-pack/plugins/fleet/server/services/agents/agentless_agent.test.ts +++ b/x-pack/plugins/fleet/server/services/agents/agentless_agent.test.ts @@ -12,7 +12,7 @@ import type { Logger } from '@kbn/core/server'; import type { AxiosError } from 'axios'; import axios from 'axios'; -import { AgentlessAgentCreateError } from '../../errors'; +import { AgentlessAgentConfigError } from '../../errors'; import type { AgentPolicy, NewAgentPolicy } from '../../types'; import { appContextService } from '../app_context'; @@ -88,11 +88,23 @@ describe('Agentless Agent service', () => { jest.resetAllMocks(); }); - it('should throw AgentlessAgentCreateError if agentless policy does not support_agentless', async () => { + it('should throw AgentlessAgentConfigError if agentless policy does not support_agentless', async () => { const soClient = getAgentPolicyCreateMock(); // ignore unrelated unique name constraint const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isCloudEnabled: true } as any); + jest.spyOn(appContextService, 'getConfig').mockReturnValue({ + agentless: { + enabled: true, + api: { + url: 'http://api.agentless.com/api/v1/ess', + tls: { + certificate: '/path/to/cert', + key: '/path/to/key', + }, + }, + }, + } as any); await expect( agentlessAgentService.createAgentlessAgent(esClient, soClient, { @@ -102,16 +114,31 @@ describe('Agentless Agent service', () => { supports_agentless: false, } as AgentPolicy) ).rejects.toThrowError( - new AgentlessAgentCreateError('Agentless agent policy does not have agentless enabled') + new AgentlessAgentConfigError( + 'Agentless agent policy does not have supports_agentless enabled' + ) ); }); - it('should throw AgentlessAgentCreateError if cloud is not enabled', async () => { + it('should throw AgentlessAgentConfigError if cloud and serverless is not enabled', async () => { const soClient = getAgentPolicyCreateMock(); // ignore unrelated unique name constraint const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; - jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isCloudEnabled: false } as any); - + jest + .spyOn(appContextService, 'getCloud') + .mockReturnValue({ isCloudEnabled: false, isServerlessEnabled: false } as any); + jest.spyOn(appContextService, 'getConfig').mockReturnValue({ + agentless: { + enabled: true, + api: { + url: 'http://api.agentless.com/api/v1/ess', + tls: { + certificate: '/path/to/cert', + key: '/path/to/key', + }, + }, + }, + } as any); await expect( agentlessAgentService.createAgentlessAgent(esClient, soClient, { id: 'mocked', @@ -119,10 +146,14 @@ describe('Agentless Agent service', () => { namespace: 'default', supports_agentless: true, } as AgentPolicy) - ).rejects.toThrowError(new AgentlessAgentCreateError('missing agentless configuration')); + ).rejects.toThrowError( + new AgentlessAgentConfigError( + 'Agentless agents are only supported in cloud deployment and serverless projects' + ) + ); }); - it('should throw AgentlessAgentCreateError if agentless configuration is not found', async () => { + it('should throw AgentlessAgentConfigError if agentless configuration is not found', async () => { const soClient = getAgentPolicyCreateMock(); // ignore unrelated unique name constraint const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; @@ -136,14 +167,18 @@ describe('Agentless Agent service', () => { namespace: 'default', supports_agentless: true, } as AgentPolicy) - ).rejects.toThrowError(new AgentlessAgentCreateError('missing agentless configuration')); + ).rejects.toThrowError( + new AgentlessAgentConfigError('missing Agentless API configuration in Kibana') + ); }); - it('should throw AgentlessAgentCreateError if fleet hosts are not found', async () => { + + it('should throw AgentlessAgentConfigError if fleet hosts are not found', async () => { const soClient = getAgentPolicyCreateMock(); // ignore unrelated unique name constraint const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; jest.spyOn(appContextService, 'getConfig').mockReturnValue({ agentless: { + enabled: true, api: { url: 'http://api.agentless.com/api/v1/ess', tls: { @@ -172,15 +207,16 @@ describe('Agentless Agent service', () => { namespace: 'default', supports_agentless: true, } as AgentPolicy) - ).rejects.toThrowError(new AgentlessAgentCreateError('missing Fleet server host')); + ).rejects.toThrowError(new AgentlessAgentConfigError('missing default Fleet server host')); }); - it('should throw AgentlessAgentCreateError if enrollment tokens are not found', async () => { + it('should throw AgentlessAgentConfigError if enrollment tokens are not found', async () => { const soClient = getAgentPolicyCreateMock(); // ignore unrelated unique name constraint const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; jest.spyOn(appContextService, 'getConfig').mockReturnValue({ agentless: { + enabled: true, api: { url: 'http://api.agentless.com/api/v1/ess', tls: { @@ -212,7 +248,487 @@ describe('Agentless Agent service', () => { namespace: 'default', supports_agentless: true, } as AgentPolicy) - ).rejects.toThrowError(new AgentlessAgentCreateError('missing Fleet enrollment token')); + ).rejects.toThrowError(new AgentlessAgentConfigError('missing Fleet enrollment token')); + }); + + it('should throw an error and log and error when the Agentless API returns a status not handled and not in the 2xx series', async () => { + const soClient = getAgentPolicyCreateMock(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + jest.spyOn(appContextService, 'getConfig').mockReturnValue({ + agentless: { + enabled: true, + api: { + url: 'http://api.agentless.com', + tls: { + certificate: '/path/to/cert', + key: '/path/to/key', + ca: '/path/to/ca', + }, + }, + }, + } as any); + jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isCloudEnabled: true } as any); + mockedListFleetServerHosts.mockResolvedValue({ + items: [ + { + id: 'mocked-fleet-server-id', + host: 'http://fleetserver:8220', + active: true, + is_default: true, + host_urls: ['http://fleetserver:8220'], + }, + ], + } as any); + mockedListEnrollmentApiKeys.mockResolvedValue({ + items: [ + { + id: 'mocked-fleet-enrollment-token-id', + policy_id: 'mocked-policy-id', + api_key: 'mocked-api-key', + }, + ], + } as any); + // Force axios to throw an AxiosError to simulate an error response + (axios as jest.MockedFunction).mockRejectedValueOnce({ + response: { + status: 999, + data: { + message: 'This is a fake error status that is never to be handled handled', + }, + }, + } as AxiosError); + + await expect( + agentlessAgentService.createAgentlessAgent(esClient, soClient, { + id: 'mocked-agentless-agent-policy-id', + name: 'agentless agent policy', + namespace: 'default', + supports_agentless: true, + } as AgentPolicy) + ).rejects.toThrowError(); + + // Assert that the error is logged + expect(mockedLogger.error).toHaveBeenCalledTimes(1); + }); + + it('should throw an error and log and error when the Agentless API returns status 500', async () => { + const soClient = getAgentPolicyCreateMock(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + jest.spyOn(appContextService, 'getConfig').mockReturnValue({ + agentless: { + enabled: true, + api: { + url: 'http://api.agentless.com', + tls: { + certificate: '/path/to/cert', + key: '/path/to/key', + ca: '/path/to/ca', + }, + }, + }, + } as any); + jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isCloudEnabled: true } as any); + mockedListFleetServerHosts.mockResolvedValue({ + items: [ + { + id: 'mocked-fleet-server-id', + host: 'http://fleetserver:8220', + active: true, + is_default: true, + host_urls: ['http://fleetserver:8220'], + }, + ], + } as any); + mockedListEnrollmentApiKeys.mockResolvedValue({ + items: [ + { + id: 'mocked-fleet-enrollment-token-id', + policy_id: 'mocked-policy-id', + api_key: 'mocked-api-key', + }, + ], + } as any); + // Force axios to throw an AxiosError to simulate an error response + (axios as jest.MockedFunction).mockRejectedValueOnce({ + response: { + status: 500, + data: { + message: 'Internal Server Error', + }, + }, + } as AxiosError); + + await expect( + agentlessAgentService.createAgentlessAgent(esClient, soClient, { + id: 'mocked-agentless-agent-policy-id', + name: 'agentless agent policy', + namespace: 'default', + supports_agentless: true, + } as AgentPolicy) + ).rejects.toThrowError(); + + // Assert that the error is logged + expect(mockedLogger.error).toHaveBeenCalledTimes(1); + }); + + it('should throw an error and log and error when the Agentless API returns status 429', async () => { + const soClient = getAgentPolicyCreateMock(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + jest.spyOn(appContextService, 'getConfig').mockReturnValue({ + agentless: { + enabled: true, + api: { + url: 'http://api.agentless.com', + tls: { + certificate: '/path/to/cert', + key: '/path/to/key', + ca: '/path/to/ca', + }, + }, + }, + } as any); + jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isCloudEnabled: true } as any); + mockedListFleetServerHosts.mockResolvedValue({ + items: [ + { + id: 'mocked-fleet-server-id', + host: 'http://fleetserver:8220', + active: true, + is_default: true, + host_urls: ['http://fleetserver:8220'], + }, + ], + } as any); + mockedListEnrollmentApiKeys.mockResolvedValue({ + items: [ + { + id: 'mocked-fleet-enrollment-token-id', + policy_id: 'mocked-policy-id', + api_key: 'mocked-api-key', + }, + ], + } as any); + // Force axios to throw an AxiosError to simulate an error response + (axios as jest.MockedFunction).mockRejectedValueOnce({ + response: { + status: 429, + data: { + message: 'Limit exceeded', + }, + }, + } as AxiosError); + + await expect( + agentlessAgentService.createAgentlessAgent(esClient, soClient, { + id: 'mocked-agentless-agent-policy-id', + name: 'agentless agent policy', + namespace: 'default', + supports_agentless: true, + } as AgentPolicy) + ).rejects.toThrowError(); + + // Assert that the error is logged + expect(mockedLogger.error).toHaveBeenCalledTimes(1); + }); + + it('should throw an error and log and error when the Agentless API returns status 408', async () => { + const soClient = getAgentPolicyCreateMock(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + jest.spyOn(appContextService, 'getConfig').mockReturnValue({ + agentless: { + enabled: true, + api: { + url: 'http://api.agentless.com', + tls: { + certificate: '/path/to/cert', + key: '/path/to/key', + ca: '/path/to/ca', + }, + }, + }, + } as any); + jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isCloudEnabled: true } as any); + mockedListFleetServerHosts.mockResolvedValue({ + items: [ + { + id: 'mocked-fleet-server-id', + host: 'http://fleetserver:8220', + active: true, + is_default: true, + host_urls: ['http://fleetserver:8220'], + }, + ], + } as any); + mockedListEnrollmentApiKeys.mockResolvedValue({ + items: [ + { + id: 'mocked-fleet-enrollment-token-id', + policy_id: 'mocked-policy-id', + api_key: 'mocked-api-key', + }, + ], + } as any); + // Force axios to throw an AxiosError to simulate an error response + (axios as jest.MockedFunction).mockRejectedValueOnce({ + response: { + status: 408, + data: { + message: 'Request timed out', + }, + }, + } as AxiosError); + + await expect( + agentlessAgentService.createAgentlessAgent(esClient, soClient, { + id: 'mocked-agentless-agent-policy-id', + name: 'agentless agent policy', + namespace: 'default', + supports_agentless: true, + } as AgentPolicy) + ).rejects.toThrowError(); + + // Assert that the error is logged + expect(mockedLogger.error).toBeCalledTimes(1); + }); + + it('should throw an error and log and error when the Agentless API returns status 404', async () => { + const soClient = getAgentPolicyCreateMock(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + jest.spyOn(appContextService, 'getConfig').mockReturnValue({ + agentless: { + enabled: true, + api: { + url: 'http://api.agentless.com', + tls: { + certificate: '/path/to/cert', + key: '/path/to/key', + ca: '/path/to/ca', + }, + }, + }, + } as any); + jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isCloudEnabled: true } as any); + mockedListFleetServerHosts.mockResolvedValue({ + items: [ + { + id: 'mocked-fleet-server-id', + host: 'http://fleetserver:8220', + active: true, + is_default: true, + host_urls: ['http://fleetserver:8220'], + }, + ], + } as any); + mockedListEnrollmentApiKeys.mockResolvedValue({ + items: [ + { + id: 'mocked-fleet-enrollment-token-id', + policy_id: 'mocked-policy-id', + api_key: 'mocked-api-key', + }, + ], + } as any); + // Force axios to throw an AxiosError to simulate an error response + (axios as jest.MockedFunction).mockRejectedValueOnce({ + response: { + status: 404, + data: { + message: 'Not Found', + }, + }, + } as AxiosError); + + await expect( + agentlessAgentService.createAgentlessAgent(esClient, soClient, { + id: 'mocked-agentless-agent-policy-id', + name: 'agentless agent policy', + namespace: 'default', + supports_agentless: true, + } as AgentPolicy) + ).rejects.toThrowError(); + + // Assert that the error is logged + expect(mockedLogger.error).toBeCalledTimes(1); + }); + + it('should throw an error and log and error when the Agentless API returns status 403', async () => { + const soClient = getAgentPolicyCreateMock(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + jest.spyOn(appContextService, 'getConfig').mockReturnValue({ + agentless: { + enabled: true, + api: { + url: 'http://api.agentless.com', + tls: { + certificate: '/path/to/cert', + key: '/path/to/key', + ca: '/path/to/ca', + }, + }, + }, + } as any); + jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isCloudEnabled: true } as any); + mockedListFleetServerHosts.mockResolvedValue({ + items: [ + { + id: 'mocked-fleet-server-id', + host: 'http://fleetserver:8220', + active: true, + is_default: true, + host_urls: ['http://fleetserver:8220'], + }, + ], + } as any); + mockedListEnrollmentApiKeys.mockResolvedValue({ + items: [ + { + id: 'mocked-fleet-enrollment-token-id', + policy_id: 'mocked-policy-id', + api_key: 'mocked-api-key', + }, + ], + } as any); + // Force axios to throw an AxiosError to simulate an error response + (axios as jest.MockedFunction).mockRejectedValueOnce({ + response: { + status: 403, + data: { + message: 'Forbidden', + }, + }, + } as AxiosError); + + await expect( + agentlessAgentService.createAgentlessAgent(esClient, soClient, { + id: 'mocked-agentless-agent-policy-id', + name: 'agentless agent policy', + namespace: 'default', + supports_agentless: true, + } as AgentPolicy) + ).rejects.toThrowError(); + + // Assert that the error is logged + expect(mockedLogger.error).toBeCalledTimes(1); + }); + + it('should throw an error and log and error when the Agentless API returns status 401', async () => { + const soClient = getAgentPolicyCreateMock(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + jest.spyOn(appContextService, 'getConfig').mockReturnValue({ + agentless: { + enabled: true, + api: { + url: 'http://api.agentless.com', + tls: { + certificate: '/path/to/cert', + key: '/path/to/key', + ca: '/path/to/ca', + }, + }, + }, + } as any); + jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isCloudEnabled: true } as any); + mockedListFleetServerHosts.mockResolvedValue({ + items: [ + { + id: 'mocked-fleet-server-id', + host: 'http://fleetserver:8220', + active: true, + is_default: true, + host_urls: ['http://fleetserver:8220'], + }, + ], + } as any); + mockedListEnrollmentApiKeys.mockResolvedValue({ + items: [ + { + id: 'mocked-fleet-enrollment-token-id', + policy_id: 'mocked-policy-id', + api_key: 'mocked-api-key', + }, + ], + } as any); + // Force axios to throw an AxiosError to simulate an error response + (axios as jest.MockedFunction).mockRejectedValueOnce({ + response: { + status: 401, + data: { + message: 'Unauthorized', + }, + }, + } as AxiosError); + + await expect( + agentlessAgentService.createAgentlessAgent(esClient, soClient, { + id: 'mocked-agentless-agent-policy-id', + name: 'agentless agent policy', + namespace: 'default', + supports_agentless: true, + } as AgentPolicy) + ).rejects.toThrowError(); + + // Assert that the error is logged + expect(mockedLogger.error).toBeCalledTimes(1); + }); + + it('should throw an error and log and error when the Agentless API returns status 400', async () => { + const soClient = getAgentPolicyCreateMock(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + jest.spyOn(appContextService, 'getConfig').mockReturnValue({ + agentless: { + enabled: true, + api: { + url: 'http://api.agentless.com', + tls: { + certificate: '/path/to/cert', + key: '/path/to/key', + ca: '/path/to/ca', + }, + }, + }, + } as any); + jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isCloudEnabled: true } as any); + mockedListFleetServerHosts.mockResolvedValue({ + items: [ + { + id: 'mocked-fleet-server-id', + host: 'http://fleetserver:8220', + active: true, + is_default: true, + host_urls: ['http://fleetserver:8220'], + }, + ], + } as any); + mockedListEnrollmentApiKeys.mockResolvedValue({ + items: [ + { + id: 'mocked-fleet-enrollment-token-id', + policy_id: 'mocked-policy-id', + api_key: 'mocked-api-key', + }, + ], + } as any); + // Force axios to throw an AxiosError to simulate an error response + (axios as jest.MockedFunction).mockRejectedValueOnce({ + response: { + status: 400, + data: { + message: 'Bad Request', + }, + }, + } as AxiosError); + + await expect( + agentlessAgentService.createAgentlessAgent(esClient, soClient, { + id: 'mocked-agentless-agent-policy-id', + name: 'agentless agent policy', + namespace: 'default', + supports_agentless: true, + } as AgentPolicy) + ).rejects.toThrowError(); + + // Assert that the error is logged + expect(mockedLogger.error).toBeCalledTimes(1); }); it('should create agentless agent for ESS', async () => { diff --git a/x-pack/plugins/fleet/server/services/agents/agentless_agent.ts b/x-pack/plugins/fleet/server/services/agents/agentless_agent.ts index 617f3db7849f4..9e6d74ddcf827 100644 --- a/x-pack/plugins/fleet/server/services/agents/agentless_agent.ts +++ b/x-pack/plugins/fleet/server/services/agents/agentless_agent.ts @@ -8,9 +8,10 @@ import https from 'https'; import type { ElasticsearchClient, LogMeta, SavedObjectsClientContract } from '@kbn/core/server'; +import type { Logger } from '@kbn/logging'; import { SslConfig, sslSchema } from '@kbn/server-http-tools'; -import type { AxiosError, AxiosRequestConfig } from 'axios'; +import type { AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios'; import axios from 'axios'; import apm from 'elastic-apm-node'; @@ -18,7 +19,11 @@ import apm from 'elastic-apm-node'; import { SO_SEARCH_LIMIT } from '../../constants'; import type { AgentPolicy } from '../../types'; import type { AgentlessApiResponse } from '../../../common/types'; -import { AgentlessAgentCreateError } from '../../errors'; +import { + AgentlessAgentConfigError, + AgentlessAgentCreateError, + AgentlessAgentDeleteError, +} from '../../errors'; import { appContextService } from '../app_context'; @@ -34,8 +39,6 @@ class AgentlessAgentService { agentlessAgentPolicy: AgentPolicy ) { const traceId = apm.currentTransaction?.traceparent; - const withRequestIdMessage = (message: string) => `${message} [Request Id: ${traceId}]`; - const errorMetadata: LogMeta = { trace: { id: traceId, @@ -45,21 +48,25 @@ class AgentlessAgentService { const logger = appContextService.getLogger(); logger.debug(`[Agentless API] Creating agentless agent ${agentlessAgentPolicy.id}`); - if (!isAgentlessApiEnabled) { + const agentlessConfig = appContextService.getConfig()?.agentless; + if (!agentlessConfig) { + logger.error('[Agentless API] Missing agentless configuration', errorMetadata); + throw new AgentlessAgentConfigError('missing Agentless API configuration in Kibana'); + } + + if (!isAgentlessApiEnabled()) { logger.error( - '[Agentless API] Creating agentless agent not supported in non-cloud or non-serverless environments' + '[Agentless API] Agentless agents are only supported in cloud deployment and serverless projects' + ); + throw new AgentlessAgentConfigError( + 'Agentless agents are only supported in cloud deployment and serverless projects' ); - throw new AgentlessAgentCreateError('Agentless agent not supported'); } if (!agentlessAgentPolicy.supports_agentless) { logger.error('[Agentless API] Agentless agent policy does not have agentless enabled'); - throw new AgentlessAgentCreateError('Agentless agent policy does not have agentless enabled'); - } - - const agentlessConfig = appContextService.getConfig()?.agentless; - if (!agentlessConfig) { - logger.error('[Agentless API] Missing agentless configuration', errorMetadata); - throw new AgentlessAgentCreateError('missing agentless configuration'); + throw new AgentlessAgentConfigError( + 'Agentless agent policy does not have supports_agentless enabled' + ); } const policyId = agentlessAgentPolicy.id; @@ -111,67 +118,19 @@ class AgentlessAgentService { logger.debug( `[Agentless API] Creating agentless agent with request config ${requestConfigDebugStatus}` ); - const errorMetadataWithRequestConfig: LogMeta = { - ...errorMetadata, - http: { - request: { - id: traceId, - body: requestConfig.data, - }, - }, - }; const response = await axios(requestConfig).catch( (error: Error | AxiosError) => { - if (!axios.isAxiosError(error)) { - logger.error( - `[Agentless API] Creating agentless failed with an error ${error} ${requestConfigDebugStatus}`, - errorMetadataWithRequestConfig - ); - throw new AgentlessAgentCreateError(withRequestIdMessage(error.message)); - } - - const errorLogCodeCause = `${error.code} ${this.convertCauseErrorsToString(error)}`; - - if (error.response) { - // The request was made and the server responded with a status code and error data - logger.error( - `[Agentless API] Creating agentless failed because the Agentless API responding with a status code that falls out of the range of 2xx: ${JSON.stringify( - error.response.status - )}} ${JSON.stringify(error.response.data)}} ${requestConfigDebugStatus}`, - { - ...errorMetadataWithRequestConfig, - http: { - ...errorMetadataWithRequestConfig.http, - response: { - status_code: error.response.status, - body: error.response.data, - }, - }, - } - ); - throw new AgentlessAgentCreateError( - withRequestIdMessage(`the Agentless API could not create the agentless agent`) - ); - } else if (error.request) { - // The request was made but no response was received - logger.error( - `[Agentless API] Creating agentless agent failed while sending the request to the Agentless API: ${errorLogCodeCause} ${requestConfigDebugStatus}`, - errorMetadataWithRequestConfig - ); - throw new AgentlessAgentCreateError( - withRequestIdMessage(`no response received from the Agentless API`) - ); - } else { - // Something happened in setting up the request that triggered an Error - logger.error( - `[Agentless API] Creating agentless agent failed to be created ${errorLogCodeCause} ${requestConfigDebugStatus}`, - errorMetadataWithRequestConfig - ); - throw new AgentlessAgentCreateError( - withRequestIdMessage('the Agentless API could not create the agentless agent') - ); - } + this.catchAgentlessApiError( + 'create', + error, + logger, + agentlessAgentPolicy.id, + requestConfig, + requestConfigDebugStatus, + errorMetadata, + traceId + ); } ); @@ -199,6 +158,12 @@ class AgentlessAgentService { ca: tlsConfig.certificateAuthorities, }), }; + const traceId = apm.currentTransaction?.traceparent; + const errorMetadata: LogMeta = { + trace: { + id: traceId, + }, + }; const requestConfigDebugStatus = this.createRequestConfigDebug(requestConfig); @@ -223,35 +188,25 @@ class AgentlessAgentService { ); const response = await axios(requestConfig).catch((error: AxiosError) => { - const errorLogCodeCause = `${error.code} ${this.convertCauseErrorsToString(error)}`; - - if (!axios.isAxiosError(error)) { - logger.error( - `[Agentless API] Deleting agentless deployment failed with an error ${JSON.stringify( - error - )} ${requestConfigDebugStatus}` - ); - } - if (error.response) { - logger.error( - `[Agentless API] Deleting Agentless deployment Failed Response Error: ${JSON.stringify( - error.response.status - )}} ${JSON.stringify(error.response.data)}} ${requestConfigDebugStatus} ` - ); - } else if (error.request) { - logger.error( - `[Agentless API] Deleting agentless deployment failed to receive a response from the Agentless API ${errorLogCodeCause} ${requestConfigDebugStatus}` - ); - } else { - logger.error( - `[Agentless API] Deleting agentless deployment failed to delete the request ${errorLogCodeCause} ${requestConfigDebugStatus}` - ); - } + this.catchAgentlessApiError( + 'delete', + error, + logger, + agentlessPolicyId, + requestConfig, + requestConfigDebugStatus, + errorMetadata, + traceId + ); }); return response; } + private withRequestIdMessage(message: string, traceId?: string) { + return `${message} [Request Id: ${traceId}]`; + } + private createTlsConfig(agentlessConfig: AgentlessConfig | undefined) { return new SslConfig( sslSchema.validate({ @@ -263,6 +218,34 @@ class AgentlessAgentService { ); } + private async getFleetUrlAndTokenForAgentlessAgent( + esClient: ElasticsearchClient, + policyId: string, + soClient: SavedObjectsClientContract + ) { + const { items: enrollmentApiKeys } = await listEnrollmentApiKeys(esClient, { + perPage: SO_SEARCH_LIMIT, + showInactive: true, + kuery: `policy_id:"${policyId}"`, + }); + + const { items: fleetHosts } = await listFleetServerHosts(soClient); + // Tech Debt: change this when we add the internal fleet server config to use the internal fleet server host + // https://github.com/elastic/security-team/issues/9695 + const defaultFleetHost = + fleetHosts.length === 1 ? fleetHosts[0] : fleetHosts.find((host) => host.is_default); + + if (!defaultFleetHost) { + throw new AgentlessAgentConfigError('missing default Fleet server host'); + } + if (!enrollmentApiKeys.length) { + throw new AgentlessAgentConfigError('missing Fleet enrollment token'); + } + const fleetToken = enrollmentApiKeys[0].api_key; + const fleetUrl = defaultFleetHost?.host_urls[0]; + return { fleetUrl, fleetToken }; + } + private createRequestConfigDebug(requestConfig: AxiosRequestConfig) { return JSON.stringify({ ...requestConfig, @@ -282,6 +265,129 @@ class AgentlessAgentService { }); } + private catchAgentlessApiError( + action: 'create' | 'delete', + error: Error | AxiosError, + logger: Logger, + agentlessPolicyId: string, + requestConfig: AxiosRequestConfig, + requestConfigDebugStatus: string, + errorMetadata: LogMeta, + traceId?: string + ) { + const errorMetadataWithRequestConfig: LogMeta = { + ...errorMetadata, + http: { + request: { + id: traceId, + body: requestConfig.data, + }, + }, + }; + + const errorLogCodeCause = (axiosError: AxiosError) => + `${axiosError.code} ${this.convertCauseErrorsToString(axiosError)}`; + + if (!axios.isAxiosError(error)) { + logger.error( + `${ + action === 'create' + ? `[Agentless API] Creating agentless failed with an error that is not an AxiosError for agentless policy` + : `[Agentless API] Deleting agentless deployment failed with an error that is not an Axios error for agentless policy` + } ${error} ${requestConfigDebugStatus}`, + errorMetadataWithRequestConfig + ); + + throw this.getAgentlessAgentError(action, error.message, traceId); + } + + const ERROR_HANDLING_MESSAGES = this.getErrorHandlingMessages(agentlessPolicyId); + + if (error.response) { + if (error.response.status in ERROR_HANDLING_MESSAGES) { + const handledResponseErrorMessage = + ERROR_HANDLING_MESSAGES[error.response.status as keyof typeof ERROR_HANDLING_MESSAGES][ + action + ]; + this.handleResponseError( + action, + error.response, + logger, + errorMetadataWithRequestConfig, + requestConfigDebugStatus, + handledResponseErrorMessage.log, + handledResponseErrorMessage.message, + traceId + ); + } else { + const unhandledResponseErrorMessage = ERROR_HANDLING_MESSAGES.unhandled_response[action]; + // The request was made and the server responded with a status code and error data + this.handleResponseError( + action, + error.response, + logger, + errorMetadataWithRequestConfig, + requestConfigDebugStatus, + unhandledResponseErrorMessage.log, + unhandledResponseErrorMessage.message, + traceId + ); + } + } else if (error.request) { + // The request was made but no response was received + const requestErrorMessage = ERROR_HANDLING_MESSAGES.request_error[action]; + logger.error( + `${requestErrorMessage.log} ${errorLogCodeCause(error)} ${requestConfigDebugStatus}`, + errorMetadataWithRequestConfig + ); + + throw this.getAgentlessAgentError(action, requestErrorMessage.message, traceId); + } else { + // Something happened in setting up the request that triggered an Error + logger.error( + `[Agentless API] ${ + action === 'create' ? 'Creating' : 'Deleting' + } the agentless agent failed ${errorLogCodeCause(error)} ${requestConfigDebugStatus}`, + errorMetadataWithRequestConfig + ); + + throw this.getAgentlessAgentError( + action, + `the Agentless API could not ${action} the agentless agent`, + traceId + ); + } + } + + private handleResponseError( + action: 'create' | 'delete', + response: AxiosResponse, + logger: Logger, + errorMetadataWithRequestConfig: LogMeta, + requestConfigDebugStatus: string, + logMessage: string, + userMessage: string, + traceId?: string + ) { + logger.error( + `${logMessage} ${JSON.stringify(response.status)} ${JSON.stringify( + response.data + )}} ${requestConfigDebugStatus}`, + { + ...errorMetadataWithRequestConfig, + http: { + ...errorMetadataWithRequestConfig.http, + response: { + status_code: response?.status, + body: response?.data, + }, + }, + } + ); + + throw this.getAgentlessAgentError(action, userMessage, traceId); + } + private convertCauseErrorsToString = (error: AxiosError) => { if (error.cause instanceof AggregateError) { return error.cause.errors.map((e: Error) => e.message); @@ -289,32 +395,122 @@ class AgentlessAgentService { return error.cause; }; - private async getFleetUrlAndTokenForAgentlessAgent( - esClient: ElasticsearchClient, - policyId: string, - soClient: SavedObjectsClientContract - ) { - const { items: enrollmentApiKeys } = await listEnrollmentApiKeys(esClient, { - perPage: SO_SEARCH_LIMIT, - showInactive: true, - kuery: `policy_id:"${policyId}"`, - }); - - const { items: fleetHosts } = await listFleetServerHosts(soClient); - // Tech Debt: change this when we add the internal fleet server config to use the internal fleet server host - // https://github.com/elastic/security-team/issues/9695 - const defaultFleetHost = - fleetHosts.length === 1 ? fleetHosts[0] : fleetHosts.find((host) => host.is_default); + private getAgentlessAgentError(action: string, userMessage: string, traceId: string | undefined) { + return action === 'create' + ? new AgentlessAgentCreateError(this.withRequestIdMessage(userMessage, traceId)) + : new AgentlessAgentDeleteError(this.withRequestIdMessage(userMessage, traceId)); + } - if (!defaultFleetHost) { - throw new AgentlessAgentCreateError('missing Fleet server host'); - } - if (!enrollmentApiKeys.length) { - throw new AgentlessAgentCreateError('missing Fleet enrollment token'); - } - const fleetToken = enrollmentApiKeys[0].api_key; - const fleetUrl = defaultFleetHost?.host_urls[0]; - return { fleetUrl, fleetToken }; + private getErrorHandlingMessages(agentlessPolicyId: string) { + return { + 400: { + create: { + log: '[Agentless API] Creating the agentless agent failed with a status 400, bad request for agentless policy.', + message: + 'the Agentless API could not create the agentless agent. Please delete the agentless policy and try again or contact your administrator.', + }, + delete: { + log: '[Agentless API] Deleting the agentless deployment failed with a status 400, bad request for agentless policy', + message: + 'the Agentless API could not create the agentless agent. Please delete the agentless policy try again or contact your administrator.', + }, + }, + 401: { + create: { + log: '[Agentless API] Creating the agentless agent failed with a status 401 unauthorized for agentless policy.', + message: + 'the Agentless API could not create the agentless agent because an unauthorized request was sent. Please delete the agentless policy and try again or contact your administrator.', + }, + delete: { + log: '[Agentless API] Deleting the agentless deployment failed with a status 401 unauthorized for agentless policy. Check the Kibana Agentless API tls configuration', + message: + 'the Agentless API could not delete the agentless deployment because an unauthorized request was sent. Please try again or contact your administrator.', + }, + }, + 403: { + create: { + log: '[Agentless API] Creating the agentless agent failed with a status 403 forbidden for agentless policy. Check the Kibana Agentless API configuration and endpoints.', + message: + 'the Agentless API could not create the agentless agent because a forbidden request was sent. Please delete the agentless policy and try again or contact your administrator.', + }, + delete: { + log: '[Agentless API] Deleting the agentless deployment failed with a status 403 forbidden for agentless policy. Check the Kibana Agentless API configuration and endpoints.', + message: + 'the Agentless API could not delete the agentless deployment because a forbidden request was sent. Please try again or contact your administrator.', + }, + }, + 404: { + // this is likely to happen when creating agentless agents, but covering it in case + create: { + log: '[Agentless API] Creating the agentless agent failed with a status 404 not found.', + message: + 'the Agentless API could not create the agentless agent because it returned a 404 error not found.', + }, + delete: { + log: '[Agentless API] Deleting the agentless deployment failed with a status 404 not found', + message: `the Agentless API could not delete the agentless deployment ${agentlessPolicyId} because it could not be found.`, + }, + }, + 408: { + create: { + log: '[Agentless API] Creating the agentless agent failed with a status 408, the request timed out', + message: + 'the Agentless API request timed out waiting for the agentless agent status to respond, please wait a few minutes for the agent to enroll with fleet. If agent fails to enroll with Fleet please delete the agentless policy try again or contact your administrator.', + }, + delete: { + log: '[Agentless API] Deleting the agentless deployment failed with a status 408, the request timed out', + message: `the Agentless API could not delete the agentless deployment ${agentlessPolicyId} because the request timed out, please wait a few minutes for the agentless agent deployment to be removed. If it continues to persist please try again or contact your administrator.`, + }, + }, + 429: { + create: { + log: '[Agentless API] Creating the agentless agent failed with a status 429 for agentless policy, agentless agent limit has been reached for this deployment or project.', + message: + 'the Agentless API could not create the agentless agent, you have reached the limit of agentless agents provisioned for this deployment or project. Consider removing some agentless agents and try again or use agent-based agents for this integration.', + }, + // this is likely to happen when deleting agentless agents, but covering it in case + delete: { + log: '[Agentless API] Deleting the agentless deployment failed with a status 429 for agentless policy, agentless agent limit has been reached for this deployment or project.', + message: + 'the Agentless API could not delete the agentless deployment, you have reached the limit of agentless agents provisioned for this deployment or project. Consider removing some agentless agents and try again or use agent-based agents for this integration.', + }, + }, + 500: { + create: { + log: '[Agentless API] Creating the agentless agent failed with a status 500 internal service error.', + message: + 'the Agentless API could not create the agentless agent because it returned a 500 internal error. Please delete the agentless policy and try again later or contact your administrator.', + }, + delete: { + log: '[Agentless API] Deleting the agentless deployment failed with a status 500 internal service error.', + message: + 'the Agentless API could not delete the agentless deployment because it returned a 500 internal error. Please try again later or contact your administrator.', + }, + }, + unhandled_response: { + create: { + log: '[Agentless API] Creating agentless agent failed because the Agentless API responded with an unhandled status code that falls out of the range of 2xx:', + message: + 'the Agentless API could not create the agentless agent due to an unexpected error. Please delete the agentless policy and try again later or contact your administrator.', + }, + delete: { + log: '[Agentless API] Deleting agentless deployment failed because the Agentless API responded with an unhandled status code that falls out of the range of 2xx:', + message: `the Agentless API could not delete the agentless deployment ${agentlessPolicyId}. Please try again later or contact your administrator.`, + }, + }, + request_error: { + create: { + log: '[Agentless API] Creating agentless agent failed with a request error:', + message: + 'the Agentless API could not create the agentless agent due to a request error. Please delete the agentless policy and try again later or contact your administrator.', + }, + delete: { + log: '[Agentless API] Deleting agentless deployment failed with a request error:', + message: + 'the Agentless API could not delete the agentless deployment due to a request error. Please try again later or contact your administrator.', + }, + }, + }; } } diff --git a/x-pack/plugins/fleet/server/services/utils/agentless.ts b/x-pack/plugins/fleet/server/services/utils/agentless.ts index c43f10db16b46..0f5d4e9d1de85 100644 --- a/x-pack/plugins/fleet/server/services/utils/agentless.ts +++ b/x-pack/plugins/fleet/server/services/utils/agentless.ts @@ -10,7 +10,7 @@ import type { FleetConfigType } from '../../config'; export { isOnlyAgentlessIntegration } from '../../../common/services/agentless_policy_helper'; export const isAgentlessApiEnabled = () => { - const cloudSetup = appContextService.getCloud && appContextService.getCloud(); + const cloudSetup = appContextService.getCloud(); const isHosted = cloudSetup?.isCloudEnabled || cloudSetup?.isServerlessEnabled; return Boolean(isHosted && appContextService.getConfig()?.agentless?.enabled); }; From a6dc47ddeb15d6c7b91e1abaaf83fba3eee2fef2 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Wed, 23 Oct 2024 10:06:47 -0600 Subject: [PATCH 14/14] [Security Assistant] Fix Security Assistant settings link behavior and adjust initial settings tab (#197323) --- .../conversation_settings/translations.ts | 2 +- .../settings/assistant_settings_management.tsx | 10 +++++----- .../settings_context_menu.tsx | 17 +++++++++++++++++ .../stack_management/management_settings.tsx | 4 ++-- 4 files changed, 25 insertions(+), 8 deletions(-) diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings/translations.ts b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings/translations.ts index 55852fc2a1bad..529631574e6f7 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings/translations.ts +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings/translations.ts @@ -59,7 +59,7 @@ export const SETTINGS_PROMPT_HELP_TEXT_TITLE = i18n.translate( export const STREAMING_TITLE = i18n.translate( 'xpack.elasticAssistant.assistant.conversations.settings.streamingTitle', { - defaultMessage: 'STREAMING', + defaultMessage: 'Streaming', } ); 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 12b26da336e72..c0dc904695257 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 @@ -37,7 +37,7 @@ interface Props { dataViews: DataViewsContract; selectedConversation: Conversation; onTabChange?: (tabId: string) => void; - currentTab?: SettingsTabs; + currentTab: SettingsTabs; } /** @@ -65,14 +65,14 @@ export const AssistantSettingsManagement: React.FC = React.memo( const tabsConfig = useMemo( () => [ - { - id: CONNECTORS_TAB, - label: i18n.CONNECTORS_MENU_ITEM, - }, { id: CONVERSATIONS_TAB, label: i18n.CONVERSATIONS_MENU_ITEM, }, + { + id: CONNECTORS_TAB, + label: i18n.CONNECTORS_MENU_ITEM, + }, { id: SYSTEM_PROMPTS_TAB, label: i18n.SYSTEM_PROMPTS_MENU_ITEM, diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/settings_context_menu/settings_context_menu.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/settings_context_menu/settings_context_menu.tsx index 3a19a68643006..eddf785256d82 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/settings_context_menu/settings_context_menu.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/settings_context_menu/settings_context_menu.tsx @@ -12,6 +12,7 @@ import { EuiContextMenuPanel, EuiContextMenuItem, EuiConfirmModal, + EuiIcon, EuiNotificationBadge, EuiPopover, EuiButtonIcon, @@ -68,6 +69,7 @@ export const SettingsContextMenu: React.FC = React.memo( () => navigateToApp('management', { path: 'kibana/securityAiAssistantManagement', + openInNewTab: true, }), [navigateToApp] ); @@ -81,6 +83,7 @@ export const SettingsContextMenu: React.FC = React.memo( () => navigateToApp('management', { path: `kibana/securityAiAssistantManagement?tab=${KNOWLEDGE_BASE_TAB}`, + openInNewTab: true, }), [navigateToApp] ); @@ -101,6 +104,13 @@ export const SettingsContextMenu: React.FC = React.memo( data-test-subj={'ai-assistant-settings'} > {i18n.AI_ASSISTANT_SETTINGS} + , = React.memo( data-test-subj={'knowledge-base'} > {i18n.KNOWLEDGE_BASE} + , { const [searchParams] = useSearchParams(); const currentTab = useMemo( - () => (searchParams.get('tab') as SettingsTabs) ?? CONNECTORS_TAB, + () => (searchParams.get('tab') as SettingsTabs) ?? CONVERSATIONS_TAB, [searchParams] );