From aaefcdf5664ba8d537ee3a82dc78299bbab2bc58 Mon Sep 17 00:00:00 2001
From: Juan Pablo Djeredjian
Date: Mon, 14 Aug 2023 16:17:02 +0200
Subject: [PATCH 01/26] [Security Solution] Flaky Cypress test: Detection
rules, Prebuilt Rules Installation and Update workflow - Installation of
prebuilt rules package via Fleet should install package from Fleet in the
background (#163468)
Fixes: https://github.com/elastic/kibana/issues/163447
https://github.com/elastic/kibana/issues/163586
## Summary
- Fixes flaky test:
`x-pack/plugins/security_solution/cypress/e2e/detection_response/prebuilt_rules/prebuilt_rules_install_update_workflows.cy.ts`
- Test title: `Detection rules, Prebuilt Rules Installation and Update
workflow - Installation of prebuilt rules package via Fleet should
install package from Fleet in the background`
## Details
- Initially ran the flaky test runner with multiple iterations and all
gave succesful results, i.e. no flakiness or failed tests.
- But: after checking the logs for the failed tests in the original
failed build, discovered that the reason the test failed is because:
- when checking Fleet's response for the installation of the
`security_detection_engine`, the API response was not as expected from
the API spec:
```
**Expected:** [{ name: 'security_detection_engine', installSource: 'registry' }]
**Actual:** [{ name: 'security_detection_engine', installSource: undefined }]
```
Since we cannot rely 100% that the Fleet API will return the correct
value for the installSource, this PR deletes this part of the test to
prevent any type of flakiness caused by external factors such as this.
---
.../prebuilt_rules_install_update_workflows.cy.ts | 15 +++++----------
1 file changed, 5 insertions(+), 10 deletions(-)
diff --git a/x-pack/plugins/security_solution/cypress/e2e/detection_response/prebuilt_rules/prebuilt_rules_install_update_workflows.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_response/prebuilt_rules/prebuilt_rules_install_update_workflows.cy.ts
index 4957d6edc3371..f148e973300dd 100644
--- a/x-pack/plugins/security_solution/cypress/e2e/detection_response/prebuilt_rules/prebuilt_rules_install_update_workflows.cy.ts
+++ b/x-pack/plugins/security_solution/cypress/e2e/detection_response/prebuilt_rules/prebuilt_rules_install_update_workflows.cy.ts
@@ -60,8 +60,7 @@ describe('Detection rules, Prebuilt Rules Installation and Update workflow', ()
});
it('should install package from Fleet in the background', () => {
- /* Assert that the package in installed from Fleet by checking that
- /* the installSource is "registry", as opposed to "bundle" */
+ /* Assert that the package in installed from Fleet */
cy.wait('@installPackageBulk', {
timeout: 60000,
}).then(({ response: bulkResponse }) => {
@@ -70,7 +69,6 @@ describe('Detection rules, Prebuilt Rules Installation and Update workflow', ()
const packages = bulkResponse?.body.items.map(
({ name, result }: BulkInstallPackageInfo) => ({
name,
- installSource: result.installSource,
})
);
@@ -86,17 +84,14 @@ describe('Detection rules, Prebuilt Rules Installation and Update workflow', ()
cy.wrap(response?.body)
.should('have.property', 'items')
.should('have.length.greaterThan', 0);
- cy.wrap(response?.body)
- .should('have.property', '_meta')
- .should('have.property', 'install_source')
- .should('eql', 'registry');
});
} else {
// Normal flow, install via the Fleet bulk install API
expect(packages.length).to.have.greaterThan(0);
- expect(packages).to.deep.include.members([
- { name: 'security_detection_engine', installSource: 'registry' },
- ]);
+ // At least one of the packages installed should be the security_detection_engine package
+ expect(packages).to.satisfy((pckgs: BulkInstallPackageInfo[]) =>
+ pckgs.some((pkg) => pkg.name === 'security_detection_engine')
+ );
}
});
});
From bc241affd32b449a2eb1d14737fc53941f126e9a Mon Sep 17 00:00:00 2001
From: "Joey F. Poon"
Date: Mon, 14 Aug 2023 07:34:51 -0700
Subject: [PATCH 02/26] [Security Solution] move tier into source.metadata
(#163670)
---
.../server/endpoint/services/metering_service.ts | 4 +++-
.../plugins/security_solution_serverless/server/types.ts | 7 +++++++
2 files changed, 10 insertions(+), 1 deletion(-)
diff --git a/x-pack/plugins/security_solution_serverless/server/endpoint/services/metering_service.ts b/x-pack/plugins/security_solution_serverless/server/endpoint/services/metering_service.ts
index 2ec94d78722e0..b47450eca235c 100644
--- a/x-pack/plugins/security_solution_serverless/server/endpoint/services/metering_service.ts
+++ b/x-pack/plugins/security_solution_serverless/server/endpoint/services/metering_service.ts
@@ -113,13 +113,15 @@ export class EndpointMeteringService {
creation_timestamp: timestampStr,
usage: {
type: 'security_solution_endpoint',
- sub_type: this.tier,
period_seconds: SAMPLE_PERIOD_SECONDS,
quantity: 1,
},
source: {
id: taskId,
instance_group_id: projectId,
+ metadata: {
+ tier: this.tier,
+ },
},
};
}
diff --git a/x-pack/plugins/security_solution_serverless/server/types.ts b/x-pack/plugins/security_solution_serverless/server/types.ts
index ac45cadd21c6d..91b1750e62c81 100644
--- a/x-pack/plugins/security_solution_serverless/server/types.ts
+++ b/x-pack/plugins/security_solution_serverless/server/types.ts
@@ -19,6 +19,8 @@ import type { CloudSetup } from '@kbn/cloud-plugin/server';
import type { SecuritySolutionEssPluginSetup } from '@kbn/security-solution-ess/server';
import type { MlPluginSetup } from '@kbn/ml-plugin/server';
+import type { ProductTier } from '../common/product';
+
import type { ServerlessSecurityConfig } from './config';
// eslint-disable-next-line @typescript-eslint/no-empty-interface
@@ -63,6 +65,11 @@ export interface UsageMetrics {
export interface UsageSource {
id: string;
instance_group_id: string;
+ metadata?: UsageSourceMetadata;
+}
+
+export interface UsageSourceMetadata {
+ tier?: ProductTier;
}
export interface SecurityUsageReportingTaskSetupContract {
From e12ad9785c961ced93c69bd28836e7732fac6ce4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Cau=C3=AA=20Marcondes?=
<55978943+cauemarcondes@users.noreply.github.com>
Date: Mon, 14 Aug 2023 15:36:09 +0100
Subject: [PATCH 03/26] [Profiling] remove prerelease tag from add data page
(#163809)
---
x-pack/plugins/profiling/public/views/no_data_view/index.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/x-pack/plugins/profiling/public/views/no_data_view/index.tsx b/x-pack/plugins/profiling/public/views/no_data_view/index.tsx
index e982e4d53e308..c50f50d3170ed 100644
--- a/x-pack/plugins/profiling/public/views/no_data_view/index.tsx
+++ b/x-pack/plugins/profiling/public/views/no_data_view/index.tsx
@@ -330,7 +330,7 @@ docker.elastic.co/observability/profiling-agent:${hostAgentVersion} /root/pf-hos
iconType="gear"
fill
href={`${core.http.basePath.prepend(
- `/app/integrations/detail/profiler_agent-${data?.profilerAgent.version}/overview?prerelease=true`
+ `/app/integrations/detail/profiler_agent-${data?.profilerAgent.version}/overview`
)}`}
>
{i18n.translate('xpack.profiling.tabs.elasticAgentIntegrarion.step2.button', {
From 97f44c1e505697f28239b30aa0958336813a553f Mon Sep 17 00:00:00 2001
From: Alison Goryachev
Date: Mon, 14 Aug 2023 10:40:16 -0400
Subject: [PATCH 04/26] [Index Management] Disable legacy index templates
(#163518)
---
config/serverless.yml | 5 +-
.../test_suites/core_plugins/rendering.ts | 1 +
x-pack/plugins/index_management/README.md | 4 +-
.../helpers/setup_environment.tsx | 6 +-
.../home/indices_tab.test.ts | 10 +-
.../__jest__/components/index_table.test.js | 10 +-
.../public/application/app_context.tsx | 5 +-
.../application/mount_management_section.ts | 8 +-
.../index_actions_context_menu.js | 4 +-
.../template_clone/template_clone.tsx | 7 +-
.../template_create/template_create.tsx | 7 +-
.../sections/template_edit/template_edit.tsx | 8 +-
.../plugins/index_management/public/plugin.ts | 4 +-
.../plugins/index_management/public/types.ts | 1 +
.../plugins/index_management/server/config.ts | 9 ++
.../plugins/index_management/server/plugin.ts | 4 +
.../register_privileges_route.test.ts | 2 +
.../api/templates/register_get_routes.ts | 24 +++--
.../server/test/helpers/route_dependencies.ts | 1 +
.../plugins/index_management/server/types.ts | 1 +
.../test_suites/common/index.ts | 1 +
.../common/index_management/index.ts | 14 +++
.../index_management/index_templates.ts | 94 +++++++++++++++++++
.../test_serverless/functional/config.base.ts | 3 +
.../functional/test_suites/common/index.ts | 3 +
.../common/index_management/index.ts | 14 +++
.../index_management/index_templates.ts | 35 +++++++
27 files changed, 259 insertions(+), 26 deletions(-)
create mode 100644 x-pack/test_serverless/api_integration/test_suites/common/index_management/index.ts
create mode 100644 x-pack/test_serverless/api_integration/test_suites/common/index_management/index_templates.ts
create mode 100644 x-pack/test_serverless/functional/test_suites/common/index_management/index.ts
create mode 100644 x-pack/test_serverless/functional/test_suites/common/index_management/index_templates.ts
diff --git a/config/serverless.yml b/config/serverless.yml
index f874fc0971f8b..b0fc28f058b2e 100644
--- a/config/serverless.yml
+++ b/config/serverless.yml
@@ -32,8 +32,11 @@ xpack.remote_clusters.enabled: false
xpack.snapshot_restore.enabled: false
xpack.license_management.enabled: false
-# Disable index management actions from the UI
+# Management team UI configurations
+# Disable index actions from the Index Management UI
xpack.index_management.enableIndexActions: false
+# Disable legacy index templates from Index Management UI
+xpack.index_management.enableLegacyTemplates: false
# Keep deeplinks visible so that they are shown in the sidenav
dev_tools.deeplinks.navLinkStatus: visible
diff --git a/test/plugin_functional/test_suites/core_plugins/rendering.ts b/test/plugin_functional/test_suites/core_plugins/rendering.ts
index 66a2e385d3e6c..c0573c10c10b1 100644
--- a/test/plugin_functional/test_suites/core_plugins/rendering.ts
+++ b/test/plugin_functional/test_suites/core_plugins/rendering.ts
@@ -240,6 +240,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) {
'xpack.ilm.ui.enabled (boolean)',
'xpack.index_management.ui.enabled (boolean)',
'xpack.index_management.enableIndexActions (any)',
+ 'xpack.index_management.enableLegacyTemplates (any)',
'xpack.infra.sources.default.fields.message (array)',
/**
* xpack.infra.logs is conditional and will resolve to an object of properties
diff --git a/x-pack/plugins/index_management/README.md b/x-pack/plugins/index_management/README.md
index fba162259ce91..b50309ac36099 100644
--- a/x-pack/plugins/index_management/README.md
+++ b/x-pack/plugins/index_management/README.md
@@ -53,7 +53,7 @@ POST %25%7B%5B%40metadata%5D%5Bbeat%5D%7D-%25%7B%5B%40metadata%5D%5Bversion%5D%7
### Quick steps for testing
-By default, **legacy index templates** are not shown in the UI. Make them appear by creating one in Console:
+**Legacy index templates** are only shown in the UI on stateful *and* if a user has existing legacy index templates. You can test this functionality by creating one in Console:
```
PUT _template/template_1
@@ -62,6 +62,8 @@ PUT _template/template_1
}
```
+On serverless, Elasticsearch does not support legacy index templates and therefore this functionality is disabled in Kibana via the config `xpack.index_management.enableLegacyTemplates`. For more details, see [#163518](https://github.com/elastic/kibana/pull/163518).
+
To test **Cloud-managed templates**:
1. Add `cluster.metadata.managed_index_templates` setting via Dev Tools:
diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/setup_environment.tsx b/x-pack/plugins/index_management/__jest__/client_integration/helpers/setup_environment.tsx
index 67fa2d99787ba..b1dd1d748f309 100644
--- a/x-pack/plugins/index_management/__jest__/client_integration/helpers/setup_environment.tsx
+++ b/x-pack/plugins/index_management/__jest__/client_integration/helpers/setup_environment.tsx
@@ -56,6 +56,11 @@ const appDependencies = {
executionContext: executionContextServiceMock.createStartContract(),
},
plugins: {},
+ // Default stateful configuration
+ config: {
+ enableLegacyTemplates: true,
+ enableIndexActions: true,
+ },
} as any;
export const kibanaVersion = new SemVer(MAJOR_VERSION);
@@ -82,7 +87,6 @@ export const WithAppDependencies =
(props: any) => {
httpService.setup(httpSetup);
const mergedDependencies = merge({}, appDependencies, overridingDependencies);
-
return (
diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts
index 8b2b1d6568253..16a3e1fd09bbd 100644
--- a/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts
+++ b/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts
@@ -225,11 +225,11 @@ describe('', () => {
]);
httpRequestsMockHelpers.setReloadIndicesResponse({ indexNames: [indexNameA, indexNameB] });
- testBed = await setup(httpSetup, {
- enableIndexActions: true,
+ await act(async () => {
+ testBed = await setup(httpSetup);
});
- const { component, find } = testBed;
+ const { component, find } = testBed;
component.update();
find('indexTableIndexNameLink').at(0).simulate('click');
@@ -270,8 +270,8 @@ describe('', () => {
});
test('should be able to open a closed index', async () => {
- testBed = await setup(httpSetup, {
- enableIndexActions: true,
+ await act(async () => {
+ testBed = await setup(httpSetup);
});
const { component, find, actions } = testBed;
diff --git a/x-pack/plugins/index_management/__jest__/components/index_table.test.js b/x-pack/plugins/index_management/__jest__/components/index_table.test.js
index ad704311dd210..5d8371010b3fe 100644
--- a/x-pack/plugins/index_management/__jest__/components/index_table.test.js
+++ b/x-pack/plugins/index_management/__jest__/components/index_table.test.js
@@ -168,7 +168,11 @@ describe('index table', () => {
},
plugins: {},
url: urlServiceMock,
- enableIndexActions: true,
+ // Default stateful configuration
+ config: {
+ enableLegacyTemplates: true,
+ enableIndexActions: true,
+ },
};
component = (
@@ -515,8 +519,8 @@ describe('index table', () => {
describe('Common index actions', () => {
beforeEach(() => {
- // Mock initialization of services
- setupMockComponent({ enableIndexActions: false });
+ // Mock initialization of services; set enableIndexActions=false to verify config behavior
+ setupMockComponent({ config: { enableIndexActions: false, enableLegacyTemplates: true } });
});
test('Common index actions should be hidden when feature is turned off', async () => {
diff --git a/x-pack/plugins/index_management/public/application/app_context.tsx b/x-pack/plugins/index_management/public/application/app_context.tsx
index 9acbda3f9685f..eb52f50d62ecf 100644
--- a/x-pack/plugins/index_management/public/application/app_context.tsx
+++ b/x-pack/plugins/index_management/public/application/app_context.tsx
@@ -44,6 +44,10 @@ export interface AppDependencies {
httpService: HttpService;
notificationService: NotificationService;
};
+ config: {
+ enableIndexActions: boolean;
+ enableLegacyTemplates: boolean;
+ };
history: ScopedHistory;
setBreadcrumbs: ManagementAppMountParams['setBreadcrumbs'];
uiSettings: IUiSettingsClient;
@@ -52,7 +56,6 @@ export interface AppDependencies {
docLinks: DocLinksStart;
kibanaVersion: SemVer;
theme$: Observable;
- enableIndexActions: boolean;
}
export const AppContextProvider = ({
diff --git a/x-pack/plugins/index_management/public/application/mount_management_section.ts b/x-pack/plugins/index_management/public/application/mount_management_section.ts
index 6bb3b834ce85f..997568a2eb69a 100644
--- a/x-pack/plugins/index_management/public/application/mount_management_section.ts
+++ b/x-pack/plugins/index_management/public/application/mount_management_section.ts
@@ -53,7 +53,8 @@ export async function mountManagementSection(
extensionsService: ExtensionsService,
isFleetEnabled: boolean,
kibanaVersion: SemVer,
- enableIndexActions: boolean = true
+ enableIndexActions: boolean = true,
+ enableLegacyTemplates: boolean = true
) {
const { element, setBreadcrumbs, history, theme$ } = params;
const [core, startDependencies] = await coreSetup.getStartServices();
@@ -95,7 +96,10 @@ export async function mountManagementSection(
uiMetricService,
extensionsService,
},
- enableIndexActions,
+ config: {
+ enableIndexActions,
+ enableLegacyTemplates,
+ },
history,
setBreadcrumbs,
uiSettings,
diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.js b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.js
index 4188797431e5d..4dd22c0a73e13 100644
--- a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.js
+++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.js
@@ -49,7 +49,9 @@ export class IndexActionsContextMenu extends Component {
this.setState({ isActionConfirmed });
};
panels({ services: { extensionsService }, core: { getUrlForApp } }) {
- const { enableIndexActions } = this.context;
+ const {
+ config: { enableIndexActions },
+ } = this.context;
const {
closeIndices,
diff --git a/x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx b/x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx
index 3a49237a517c9..eff5cbb554904 100644
--- a/x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx
+++ b/x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx
@@ -19,6 +19,7 @@ import { getTemplateDetailsLink } from '../../services/routing';
import { saveTemplate, useLoadIndexTemplate } from '../../services/api';
import { getIsLegacyFromQueryParams } from '../../lib/index_templates';
import { attemptToURIDecode } from '../../../shared_imports';
+import { useAppContext } from '../../app_context';
interface MatchParams {
name: string;
@@ -32,7 +33,11 @@ export const TemplateClone: React.FunctionComponent {
const decodedTemplateName = attemptToURIDecode(name)!;
- const isLegacy = getIsLegacyFromQueryParams(location);
+ const {
+ config: { enableLegacyTemplates },
+ } = useAppContext();
+ // We don't expect the `legacy` query to be used when legacy templates are disabled, however, we add the `enableLegacyTemplates` check as a safeguard
+ const isLegacy = enableLegacyTemplates && getIsLegacyFromQueryParams(location);
const [isSaving, setIsSaving] = useState(false);
const [saveError, setSaveError] = useState(null);
diff --git a/x-pack/plugins/index_management/public/application/sections/template_create/template_create.tsx b/x-pack/plugins/index_management/public/application/sections/template_create/template_create.tsx
index cb8f29d222d63..e5422ca93db26 100644
--- a/x-pack/plugins/index_management/public/application/sections/template_create/template_create.tsx
+++ b/x-pack/plugins/index_management/public/application/sections/template_create/template_create.tsx
@@ -18,12 +18,17 @@ import { TemplateForm } from '../../components';
import { breadcrumbService } from '../../services/breadcrumbs';
import { saveTemplate } from '../../services/api';
import { getTemplateDetailsLink } from '../../services/routing';
+import { useAppContext } from '../../app_context';
export const TemplateCreate: React.FunctionComponent = ({ history }) => {
const [isSaving, setIsSaving] = useState(false);
const [saveError, setSaveError] = useState(null);
+ const {
+ config: { enableLegacyTemplates },
+ } = useAppContext();
const search = parse(useLocation().search.substring(1));
- const isLegacy = Boolean(search.legacy);
+ // We don't expect the `legacy` query to be used when legacy templates are disabled, however, we add the `enableLegacyTemplates` check as a safeguard
+ const isLegacy = enableLegacyTemplates && Boolean(search.legacy);
const onSave = async (template: TemplateDeserialized) => {
const { name } = template;
diff --git a/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx b/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx
index c96502fd15066..b0a6b95351386 100644
--- a/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx
+++ b/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx
@@ -23,6 +23,7 @@ import { useLoadIndexTemplate, updateTemplate } from '../../services/api';
import { getTemplateDetailsLink } from '../../services/routing';
import { TemplateForm } from '../../components';
import { getIsLegacyFromQueryParams } from '../../lib/index_templates';
+import { useAppContext } from '../../app_context';
interface MatchParams {
name: string;
@@ -36,7 +37,12 @@ export const TemplateEdit: React.FunctionComponent {
const decodedTemplateName = attemptToURIDecode(name)!;
- const isLegacy = getIsLegacyFromQueryParams(location);
+ const {
+ config: { enableLegacyTemplates },
+ } = useAppContext();
+
+ // We don't expect the `legacy` query to be used when legacy templates are disabled, however, we add the enableLegacyTemplates check as a safeguard
+ const isLegacy = enableLegacyTemplates && getIsLegacyFromQueryParams(location);
const [isSaving, setIsSaving] = useState(false);
const [saveError, setSaveError] = useState(null);
diff --git a/x-pack/plugins/index_management/public/plugin.ts b/x-pack/plugins/index_management/public/plugin.ts
index fc965a061e0bf..6625c9fd3ea4b 100644
--- a/x-pack/plugins/index_management/public/plugin.ts
+++ b/x-pack/plugins/index_management/public/plugin.ts
@@ -39,6 +39,7 @@ export class IndexMgmtUIPlugin {
const {
ui: { enabled: isIndexManagementUiEnabled },
enableIndexActions,
+ enableLegacyTemplates,
} = this.ctx.config.get();
if (isIndexManagementUiEnabled) {
@@ -57,7 +58,8 @@ export class IndexMgmtUIPlugin {
this.extensionsService,
Boolean(fleet),
kibanaVersion,
- enableIndexActions
+ enableIndexActions,
+ enableLegacyTemplates
);
},
});
diff --git a/x-pack/plugins/index_management/public/types.ts b/x-pack/plugins/index_management/public/types.ts
index 20d2405a0fa4b..d9551c03e4352 100644
--- a/x-pack/plugins/index_management/public/types.ts
+++ b/x-pack/plugins/index_management/public/types.ts
@@ -29,4 +29,5 @@ export interface ClientConfigType {
enabled: boolean;
};
enableIndexActions?: boolean;
+ enableLegacyTemplates?: boolean;
}
diff --git a/x-pack/plugins/index_management/server/config.ts b/x-pack/plugins/index_management/server/config.ts
index c5d459486a8ef..f480c7747ca8d 100644
--- a/x-pack/plugins/index_management/server/config.ts
+++ b/x-pack/plugins/index_management/server/config.ts
@@ -30,6 +30,14 @@ const schemaLatest = schema.object(
schema.boolean({ defaultValue: true }),
schema.never()
),
+ enableLegacyTemplates: schema.conditional(
+ schema.contextRef('serverless'),
+ true,
+ // Legacy templates functionality is disabled in serverless; refer to the serverless.yml file as the source of truth
+ // We take this approach in order to have a central place (serverless.yml) for serverless config across Kibana
+ schema.boolean({ defaultValue: true }),
+ schema.never()
+ ),
},
{ defaultValue: undefined }
);
@@ -38,6 +46,7 @@ const configLatest: PluginConfigDescriptor = {
exposeToBrowser: {
ui: true,
enableIndexActions: true,
+ enableLegacyTemplates: true,
},
schema: schemaLatest,
deprecations: () => [],
diff --git a/x-pack/plugins/index_management/server/plugin.ts b/x-pack/plugins/index_management/server/plugin.ts
index a36101ad2911e..a42216d9f1bb7 100644
--- a/x-pack/plugins/index_management/server/plugin.ts
+++ b/x-pack/plugins/index_management/server/plugin.ts
@@ -12,6 +12,7 @@ import { Dependencies } from './types';
import { ApiRoutes } from './routes';
import { IndexDataEnricher } from './services';
import { handleEsError } from './shared_imports';
+import { IndexManagementConfig } from './config';
export interface IndexManagementPluginSetup {
indexDataEnricher: {
@@ -22,10 +23,12 @@ export interface IndexManagementPluginSetup {
export class IndexMgmtServerPlugin implements Plugin {
private readonly apiRoutes: ApiRoutes;
private readonly indexDataEnricher: IndexDataEnricher;
+ private readonly config: IndexManagementConfig;
constructor(initContext: PluginInitializerContext) {
this.apiRoutes = new ApiRoutes();
this.indexDataEnricher = new IndexDataEnricher();
+ this.config = initContext.config.get();
}
setup(
@@ -51,6 +54,7 @@ export class IndexMgmtServerPlugin implements Plugin security !== undefined && security.license.isEnabled(),
+ isLegacyTemplatesEnabled: this.config.enableLegacyTemplates,
},
indexDataEnricher: this.indexDataEnricher,
lib: {
diff --git a/x-pack/plugins/index_management/server/routes/api/component_templates/register_privileges_route.test.ts b/x-pack/plugins/index_management/server/routes/api/component_templates/register_privileges_route.test.ts
index dc4214ae43f73..601695c64e054 100644
--- a/x-pack/plugins/index_management/server/routes/api/component_templates/register_privileges_route.test.ts
+++ b/x-pack/plugins/index_management/server/routes/api/component_templates/register_privileges_route.test.ts
@@ -46,6 +46,7 @@ describe('GET privileges', () => {
router,
config: {
isSecurityEnabled: () => true,
+ isLegacyTemplatesEnabled: true,
},
indexDataEnricher: mockedIndexDataEnricher,
lib: {
@@ -112,6 +113,7 @@ describe('GET privileges', () => {
router,
config: {
isSecurityEnabled: () => false,
+ isLegacyTemplatesEnabled: true,
},
indexDataEnricher: mockedIndexDataEnricher,
lib: {
diff --git a/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts b/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts
index 32661bb308876..ce389af9b13e8 100644
--- a/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts
+++ b/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts
@@ -17,7 +17,7 @@ import { getCloudManagedTemplatePrefix } from '../../../lib/get_managed_template
import { RouteDependencies } from '../../../types';
import { addBasePath } from '..';
-export function registerGetAllRoute({ router, lib: { handleEsError } }: RouteDependencies) {
+export function registerGetAllRoute({ router, config, lib: { handleEsError } }: RouteDependencies) {
router.get(
{ path: addBasePath('/index_templates'), validate: false },
async (context, request, response) => {
@@ -25,17 +25,24 @@ export function registerGetAllRoute({ router, lib: { handleEsError } }: RouteDep
try {
const cloudManagedTemplatePrefix = await getCloudManagedTemplatePrefix(client);
-
- const legacyTemplatesEs = await client.asCurrentUser.indices.getTemplate();
const { index_templates: templatesEs } =
await client.asCurrentUser.indices.getIndexTemplate();
+ // @ts-expect-error TemplateSerialized.index_patterns not compatible with IndicesIndexTemplate.index_patterns
+ const templates = deserializeTemplateList(templatesEs, cloudManagedTemplatePrefix);
+
+ if (config.isLegacyTemplatesEnabled === false) {
+ // If isLegacyTemplatesEnabled=false, we do not want to fetch legacy templates and return an empty array;
+ // we retain the same response format to limit changes required on the client
+ return response.ok({ body: { templates, legacyTemplates: [] } });
+ }
+
+ const legacyTemplatesEs = await client.asCurrentUser.indices.getTemplate();
+
const legacyTemplates = deserializeLegacyTemplateList(
legacyTemplatesEs,
cloudManagedTemplatePrefix
);
- // @ts-expect-error TemplateSerialized.index_patterns not compatible with IndicesIndexTemplate.index_patterns
- const templates = deserializeTemplateList(templatesEs, cloudManagedTemplatePrefix);
const body = {
templates,
@@ -59,7 +66,7 @@ const querySchema = schema.object({
legacy: schema.maybe(schema.oneOf([schema.literal('true'), schema.literal('false')])),
});
-export function registerGetOneRoute({ router, lib: { handleEsError } }: RouteDependencies) {
+export function registerGetOneRoute({ router, config, lib: { handleEsError } }: RouteDependencies) {
router.get(
{
path: addBasePath('/index_templates/{name}'),
@@ -68,7 +75,10 @@ export function registerGetOneRoute({ router, lib: { handleEsError } }: RouteDep
async (context, request, response) => {
const { client } = (await context.core).elasticsearch;
const { name } = request.params as TypeOf;
- const isLegacy = (request.query as TypeOf).legacy === 'true';
+ // We don't expect the `legacy` query to be used when legacy templates are disabled, however, we add the `enableLegacyTemplates` check as a safeguard
+ const isLegacy =
+ config.isLegacyTemplatesEnabled !== false &&
+ (request.query as TypeOf).legacy === 'true';
try {
const cloudManagedTemplatePrefix = await getCloudManagedTemplatePrefix(client);
diff --git a/x-pack/plugins/index_management/server/test/helpers/route_dependencies.ts b/x-pack/plugins/index_management/server/test/helpers/route_dependencies.ts
index 592e7490cdbe2..bfcf2a18a7736 100644
--- a/x-pack/plugins/index_management/server/test/helpers/route_dependencies.ts
+++ b/x-pack/plugins/index_management/server/test/helpers/route_dependencies.ts
@@ -12,6 +12,7 @@ import type { RouteDependencies } from '../../types';
export const routeDependencies: Omit = {
config: {
isSecurityEnabled: jest.fn().mockReturnValue(true),
+ isLegacyTemplatesEnabled: true,
},
indexDataEnricher: new IndexDataEnricher(),
lib: {
diff --git a/x-pack/plugins/index_management/server/types.ts b/x-pack/plugins/index_management/server/types.ts
index fc245fb664f9c..bd3d889f2bce9 100644
--- a/x-pack/plugins/index_management/server/types.ts
+++ b/x-pack/plugins/index_management/server/types.ts
@@ -23,6 +23,7 @@ export interface RouteDependencies {
router: IRouter;
config: {
isSecurityEnabled: () => boolean;
+ isLegacyTemplatesEnabled: boolean;
};
indexDataEnricher: IndexDataEnricher;
lib: {
diff --git a/x-pack/test_serverless/api_integration/test_suites/common/index.ts b/x-pack/test_serverless/api_integration/test_suites/common/index.ts
index de30854beccfc..3ca6b715102d9 100644
--- a/x-pack/test_serverless/api_integration/test_suites/common/index.ts
+++ b/x-pack/test_serverless/api_integration/test_suites/common/index.ts
@@ -13,5 +13,6 @@ export default function ({ loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./spaces'));
loadTestFile(require.resolve('./security_response_headers'));
loadTestFile(require.resolve('./rollups'));
+ loadTestFile(require.resolve('./index_management'));
});
}
diff --git a/x-pack/test_serverless/api_integration/test_suites/common/index_management/index.ts b/x-pack/test_serverless/api_integration/test_suites/common/index_management/index.ts
new file mode 100644
index 0000000000000..dd7d8bc20e624
--- /dev/null
+++ b/x-pack/test_serverless/api_integration/test_suites/common/index_management/index.ts
@@ -0,0 +1,14 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { FtrProviderContext } from '../../../ftr_provider_context';
+
+export default function ({ loadTestFile }: FtrProviderContext) {
+ describe('Index Management APIs', function () {
+ loadTestFile(require.resolve('./index_templates'));
+ });
+}
diff --git a/x-pack/test_serverless/api_integration/test_suites/common/index_management/index_templates.ts b/x-pack/test_serverless/api_integration/test_suites/common/index_management/index_templates.ts
new file mode 100644
index 0000000000000..a4e082387ab4a
--- /dev/null
+++ b/x-pack/test_serverless/api_integration/test_suites/common/index_management/index_templates.ts
@@ -0,0 +1,94 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import expect from 'expect';
+import { FtrProviderContext } from '../../../ftr_provider_context';
+
+const API_BASE_PATH = '/api/index_management';
+
+export default function ({ getService }: FtrProviderContext) {
+ const supertest = getService('supertest');
+ const es = getService('es');
+ const log = getService('log');
+
+ describe('Index templates', function () {
+ const templateName = `template-${Math.random()}`;
+ const indexTemplate = {
+ name: templateName,
+ body: {
+ index_patterns: ['test*'],
+ },
+ };
+
+ before(async () => {
+ // Create a new index template to test against
+ try {
+ await es.indices.putIndexTemplate(indexTemplate);
+ } catch (err) {
+ log.debug('[Setup error] Error creating index template');
+ throw err;
+ }
+ });
+
+ after(async () => {
+ // Cleanup template created for testing purposes
+ try {
+ await es.indices.deleteIndexTemplate({
+ name: templateName,
+ });
+ } catch (err) {
+ log.debug('[Cleanup error] Error deleting index template');
+ throw err;
+ }
+ });
+
+ describe('get all', () => {
+ it('should list all the index templates with the expected parameters', async () => {
+ const { body: allTemplates } = await supertest
+ .get(`${API_BASE_PATH}/index_templates`)
+ .set('kbn-xsrf', 'xxx')
+ .set('x-elastic-internal-origin', 'xxx')
+ .expect(200);
+
+ // Legacy templates are not applicable on serverless
+ expect(allTemplates.legacyTemplates.length).toEqual(0);
+
+ const indexTemplateFound = allTemplates.templates.find(
+ (template: { name: string }) => template.name === indexTemplate.name
+ );
+
+ expect(indexTemplateFound).toBeTruthy();
+
+ const expectedKeys = [
+ 'name',
+ 'indexPatterns',
+ 'hasSettings',
+ 'hasAliases',
+ 'hasMappings',
+ '_kbnMeta',
+ ].sort();
+
+ expect(Object.keys(indexTemplateFound).sort()).toEqual(expectedKeys);
+ });
+ });
+
+ describe('get one', () => {
+ it('should return an index template with the expected parameters', async () => {
+ const { body } = await supertest
+ .get(`${API_BASE_PATH}/index_templates/${templateName}`)
+ .set('kbn-xsrf', 'xxx')
+ .set('x-elastic-internal-origin', 'xxx')
+ .expect(200);
+
+ const expectedKeys = ['name', 'indexPatterns', 'template', '_kbnMeta'].sort();
+
+ expect(body.name).toEqual(templateName);
+ expect(Object.keys(body).sort()).toEqual(expectedKeys);
+ });
+ });
+ });
+}
diff --git a/x-pack/test_serverless/functional/config.base.ts b/x-pack/test_serverless/functional/config.base.ts
index 23739a9615e69..640ae2402b544 100644
--- a/x-pack/test_serverless/functional/config.base.ts
+++ b/x-pack/test_serverless/functional/config.base.ts
@@ -55,6 +55,9 @@ export function createTestConfig(options: CreateTestConfigOptions) {
management: {
pathname: '/app/management',
},
+ indexManagement: {
+ pathname: '/app/management/data/index_management',
+ },
},
// choose where screenshots should be saved
screenshots: {
diff --git a/x-pack/test_serverless/functional/test_suites/common/index.ts b/x-pack/test_serverless/functional/test_suites/common/index.ts
index 31497afb8c7d8..7150589527b04 100644
--- a/x-pack/test_serverless/functional/test_suites/common/index.ts
+++ b/x-pack/test_serverless/functional/test_suites/common/index.ts
@@ -14,5 +14,8 @@ export default function ({ loadTestFile }: FtrProviderContext) {
// platform security
loadTestFile(require.resolve('./security/navigation/avatar_menu'));
+
+ // Management
+ loadTestFile(require.resolve('./index_management'));
});
}
diff --git a/x-pack/test_serverless/functional/test_suites/common/index_management/index.ts b/x-pack/test_serverless/functional/test_suites/common/index_management/index.ts
new file mode 100644
index 0000000000000..52472972a1faa
--- /dev/null
+++ b/x-pack/test_serverless/functional/test_suites/common/index_management/index.ts
@@ -0,0 +1,14 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { FtrProviderContext } from '../../../ftr_provider_context';
+
+export default ({ loadTestFile }: FtrProviderContext) => {
+ describe('Index Management', function () {
+ loadTestFile(require.resolve('./index_templates'));
+ });
+};
diff --git a/x-pack/test_serverless/functional/test_suites/common/index_management/index_templates.ts b/x-pack/test_serverless/functional/test_suites/common/index_management/index_templates.ts
new file mode 100644
index 0000000000000..26feb519a39a8
--- /dev/null
+++ b/x-pack/test_serverless/functional/test_suites/common/index_management/index_templates.ts
@@ -0,0 +1,35 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import expect from '@kbn/expect';
+import { FtrProviderContext } from '../../../ftr_provider_context';
+
+export default ({ getPageObjects, getService }: FtrProviderContext) => {
+ const testSubjects = getService('testSubjects');
+ const pageObjects = getPageObjects(['common', 'indexManagement', 'header']);
+ const browser = getService('browser');
+ const security = getService('security');
+ const retry = getService('retry');
+
+ describe('Index Templates', function () {
+ before(async () => {
+ await security.testUser.setRoles(['index_management_user']);
+ await pageObjects.common.navigateToApp('indexManagement');
+ // Navigate to the index templates tab
+ await pageObjects.indexManagement.changeTabs('templatesTab');
+ });
+
+ it('renders the index templates tab', async () => {
+ await retry.waitFor('index templates list to be visible', async () => {
+ return await testSubjects.exists('templateList');
+ });
+
+ const url = await browser.getCurrentUrl();
+ expect(url).to.contain(`/templates`);
+ });
+ });
+};
From 797104b4d010ed7543b72ca679dcb762d7e0e62a Mon Sep 17 00:00:00 2001
From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com>
Date: Mon, 14 Aug 2023 10:43:11 -0400
Subject: [PATCH 05/26] [Security Solution][Endpoint] Improvements to the
`run_endpoint_agent.js` CLI utility (#163748)
## Summary
- Increases the amount of time it waits for the Fleet Server agent to
show up in Fleet
- Add an `AxiosError` formatter utility and makes use of it in CLI
common services
---
.../common/endpoint_metadata_services.ts | 43 ++++++------
.../scripts/endpoint/common/fleet_services.ts | 17 +++--
.../endpoint/common/format_axios_error.ts | 65 +++++++++++++++++++
.../common/random_policy_id_generator.ts | 19 +++---
.../scripts/endpoint/common/stack_services.ts | 11 ++--
.../endpoint_agent_runner/fleet_server.ts | 35 ++++++++--
6 files changed, 148 insertions(+), 42 deletions(-)
create mode 100644 x-pack/plugins/security_solution/scripts/endpoint/common/format_axios_error.ts
diff --git a/x-pack/plugins/security_solution/scripts/endpoint/common/endpoint_metadata_services.ts b/x-pack/plugins/security_solution/scripts/endpoint/common/endpoint_metadata_services.ts
index b46eac58e24b3..a69f348c366eb 100644
--- a/x-pack/plugins/security_solution/scripts/endpoint/common/endpoint_metadata_services.ts
+++ b/x-pack/plugins/security_solution/scripts/endpoint/common/endpoint_metadata_services.ts
@@ -10,6 +10,7 @@ import type { KbnClient } from '@kbn/test';
import type { WriteResponseBase } from '@elastic/elasticsearch/lib/api/types';
import { clone, merge } from 'lodash';
import type { DeepPartial } from 'utility-types';
+import { catchAxiosErrorFormatAndThrow } from './format_axios_error';
import type { GetMetadataListRequestQuery } from '../../../common/api/endpoint';
import { resolvePathVariables } from '../../../public/common/utils/resolve_path_variables';
import {
@@ -27,13 +28,15 @@ export const fetchEndpointMetadata = async (
agentId: string
): Promise => {
return (
- await kbnClient.request({
- method: 'GET',
- path: resolvePathVariables(HOST_METADATA_GET_ROUTE, { id: agentId }),
- headers: {
- 'Elastic-Api-Version': '2023-10-31',
- },
- })
+ await kbnClient
+ .request({
+ method: 'GET',
+ path: resolvePathVariables(HOST_METADATA_GET_ROUTE, { id: agentId }),
+ headers: {
+ 'Elastic-Api-Version': '2023-10-31',
+ },
+ })
+ .catch(catchAxiosErrorFormatAndThrow)
).data;
};
@@ -42,18 +45,20 @@ export const fetchEndpointMetadataList = async (
{ page = 0, pageSize = 100, ...otherOptions }: Partial = {}
): Promise => {
return (
- await kbnClient.request({
- method: 'GET',
- path: HOST_METADATA_LIST_ROUTE,
- headers: {
- 'Elastic-Api-Version': '2023-10-31',
- },
- query: {
- page,
- pageSize,
- ...otherOptions,
- },
- })
+ await kbnClient
+ .request({
+ method: 'GET',
+ path: HOST_METADATA_LIST_ROUTE,
+ headers: {
+ 'Elastic-Api-Version': '2023-10-31',
+ },
+ query: {
+ page,
+ pageSize,
+ ...otherOptions,
+ },
+ })
+ .catch(catchAxiosErrorFormatAndThrow)
).data;
};
diff --git a/x-pack/plugins/security_solution/scripts/endpoint/common/fleet_services.ts b/x-pack/plugins/security_solution/scripts/endpoint/common/fleet_services.ts
index ea788063b572c..d81fa0c294706 100644
--- a/x-pack/plugins/security_solution/scripts/endpoint/common/fleet_services.ts
+++ b/x-pack/plugins/security_solution/scripts/endpoint/common/fleet_services.ts
@@ -35,6 +35,7 @@ import type {
} from '@kbn/fleet-plugin/common/types';
import nodeFetch from 'node-fetch';
import semver from 'semver';
+import { catchAxiosErrorFormatAndThrow } from './format_axios_error';
import { FleetAgentGenerator } from '../../../common/endpoint/data_generators/fleet_agent_generator';
const fleetGenerator = new FleetAgentGenerator();
@@ -106,6 +107,7 @@ export const fetchFleetAgents = async (
path: AGENT_API_ROUTES.LIST_PATTERN,
query: options,
})
+ .catch(catchAxiosErrorFormatAndThrow)
.then((response) => response.data);
};
@@ -161,6 +163,7 @@ export const fetchFleetServerUrl = async (kbnClient: KbnClient): Promise response.data);
// TODO:PT need to also pull in the Proxies and use that instead if defiend for url
@@ -195,6 +198,7 @@ export const fetchAgentPolicyEnrollmentKey = async (
path: enrollmentAPIKeyRouteService.getListPath(),
query: { kuery: `policy_id: "${agentPolicyId}"` },
})
+ .catch(catchAxiosErrorFormatAndThrow)
.then((response) => response.data.items[0]);
if (!apiKey) {
@@ -219,6 +223,7 @@ export const fetchAgentPolicyList = async (
path: agentPolicyRouteService.getListPath(),
query: options,
})
+ .catch(catchAxiosErrorFormatAndThrow)
.then((response) => response.data);
};
@@ -369,11 +374,13 @@ export const unEnrollFleetAgent = async (
agentId: string,
force = false
): Promise => {
- const { data } = await kbnClient.request({
- method: 'POST',
- path: agentRouteService.getUnenrollPath(agentId),
- body: { revoke: force },
- });
+ const { data } = await kbnClient
+ .request({
+ method: 'POST',
+ path: agentRouteService.getUnenrollPath(agentId),
+ body: { revoke: force },
+ })
+ .catch(catchAxiosErrorFormatAndThrow);
return data;
};
diff --git a/x-pack/plugins/security_solution/scripts/endpoint/common/format_axios_error.ts b/x-pack/plugins/security_solution/scripts/endpoint/common/format_axios_error.ts
new file mode 100644
index 0000000000000..ccb3dc125f561
--- /dev/null
+++ b/x-pack/plugins/security_solution/scripts/endpoint/common/format_axios_error.ts
@@ -0,0 +1,65 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { AxiosError } from 'axios';
+
+/* eslint-disable @typescript-eslint/no-explicit-any */
+
+export class FormattedAxiosError extends Error {
+ public readonly request: {
+ method: string;
+ url: string;
+ data: unknown;
+ };
+ public readonly response: {
+ status: number;
+ statusText: string;
+ data: any;
+ };
+
+ constructor(axiosError: AxiosError) {
+ super(axiosError.message);
+
+ this.request = {
+ method: axiosError.config.method ?? '?',
+ url: axiosError.config.url ?? '?',
+ data: axiosError.config.data ?? '',
+ };
+
+ this.response = {
+ status: axiosError?.response?.status ?? 0,
+ statusText: axiosError?.response?.statusText ?? '',
+ data: axiosError?.response?.data,
+ };
+
+ this.name = this.constructor.name;
+ }
+
+ toJSON() {
+ return {
+ message: this.message,
+ request: this.request,
+ response: this.response,
+ };
+ }
+
+ toString() {
+ return JSON.stringify(this.toJSON(), null, 2);
+ }
+}
+
+/**
+ * Used with `promise.catch()`, it will format the Axios error to a new error and will re-throw
+ * @param error
+ */
+export const catchAxiosErrorFormatAndThrow = (error: Error): never => {
+ if (error instanceof AxiosError) {
+ throw new FormattedAxiosError(error);
+ }
+
+ throw error;
+};
diff --git a/x-pack/plugins/security_solution/scripts/endpoint/common/random_policy_id_generator.ts b/x-pack/plugins/security_solution/scripts/endpoint/common/random_policy_id_generator.ts
index acf6f53bc785e..3b494d3bfe9cb 100644
--- a/x-pack/plugins/security_solution/scripts/endpoint/common/random_policy_id_generator.ts
+++ b/x-pack/plugins/security_solution/scripts/endpoint/common/random_policy_id_generator.ts
@@ -12,6 +12,7 @@ import {
PACKAGE_POLICY_API_ROUTES,
PACKAGE_POLICY_SAVED_OBJECT_TYPE,
} from '@kbn/fleet-plugin/common/constants';
+import { catchAxiosErrorFormatAndThrow } from './format_axios_error';
import { indexFleetEndpointPolicy } from '../../../common/endpoint/data_loaders/index_fleet_endpoint_policy';
import { setupFleetForEndpoint } from '../../../common/endpoint/data_loaders/setup_fleet_for_endpoint';
import type { GetPolicyListResponse } from '../../../public/management/pages/policy/types';
@@ -20,14 +21,16 @@ import { getEndpointPackageInfo } from '../../../common/endpoint/utils/package';
const fetchEndpointPolicies = (
kbnClient: KbnClient
): Promise> => {
- return kbnClient.request({
- method: 'GET',
- path: PACKAGE_POLICY_API_ROUTES.LIST_PATTERN,
- query: {
- perPage: 100,
- kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name: endpoint`,
- },
- });
+ return kbnClient
+ .request({
+ method: 'GET',
+ path: PACKAGE_POLICY_API_ROUTES.LIST_PATTERN,
+ query: {
+ perPage: 100,
+ kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name: endpoint`,
+ },
+ })
+ .catch(catchAxiosErrorFormatAndThrow);
};
// Setup a list of real endpoint policies and return a method to randomly select one
diff --git a/x-pack/plugins/security_solution/scripts/endpoint/common/stack_services.ts b/x-pack/plugins/security_solution/scripts/endpoint/common/stack_services.ts
index a2a24aec142b8..a3ad237fc3bcb 100644
--- a/x-pack/plugins/security_solution/scripts/endpoint/common/stack_services.ts
+++ b/x-pack/plugins/security_solution/scripts/endpoint/common/stack_services.ts
@@ -11,6 +11,7 @@ import { KbnClient } from '@kbn/test';
import type { StatusResponse } from '@kbn/core-status-common-internal';
import pRetry from 'p-retry';
import nodeFetch from 'node-fetch';
+import { catchAxiosErrorFormatAndThrow } from './format_axios_error';
import { isLocalhost } from './is_localhost';
import { getLocalhostRealIp } from './localhost_services';
import { createSecuritySuperuser } from './security_user_services';
@@ -189,10 +190,12 @@ export const createKbnClient = ({
*/
export const fetchStackVersion = async (kbnClient: KbnClient): Promise => {
const status = (
- await kbnClient.request({
- method: 'GET',
- path: '/api/status',
- })
+ await kbnClient
+ .request({
+ method: 'GET',
+ path: '/api/status',
+ })
+ .catch(catchAxiosErrorFormatAndThrow)
).data;
if (!status?.version?.number) {
diff --git a/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/fleet_server.ts b/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/fleet_server.ts
index a7058501f125c..f9d88382d81c7 100644
--- a/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/fleet_server.ts
+++ b/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/fleet_server.ts
@@ -36,6 +36,8 @@ import type {
PostFleetServerHostsResponse,
} from '@kbn/fleet-plugin/common/types/rest_spec/fleet_server_hosts';
import chalk from 'chalk';
+import type { FormattedAxiosError } from '../common/format_axios_error';
+import { catchAxiosErrorFormatAndThrow } from '../common/format_axios_error';
import { isLocalhost } from '../common/is_localhost';
import { dump } from './utils';
import { fetchFleetServerUrl, waitForHostToEnroll } from '../common/fleet_services';
@@ -243,7 +245,7 @@ export const startFleetServerWithDocker = async ({
containerId = (await execa('docker', dockerArgs)).stdout;
- const fleetServerAgent = await waitForHostToEnroll(kbnClient, containerName);
+ const fleetServerAgent = await waitForHostToEnroll(kbnClient, containerName, 120000);
log.verbose(`Fleet server enrolled agent:\n${JSON.stringify(fleetServerAgent, null, 2)}`);
@@ -313,11 +315,13 @@ const configureFleetIfNeeded = async () => {
log.info(`Updating Fleet Settings for Output [${output.name} (${id})]`);
- await kbnClient.request({
- method: 'PUT',
- path: outputRoutesService.getUpdatePath(id),
- body: update,
- });
+ await kbnClient
+ .request({
+ method: 'PUT',
+ path: outputRoutesService.getUpdatePath(id),
+ body: update,
+ })
+ .catch(catchAxiosErrorFormatAndThrow);
}
}
}
@@ -354,6 +358,25 @@ const addFleetServerHostToFleetSettings = async (
path: fleetServerHostsRoutesService.getCreatePath(),
body: newFleetHostEntry,
})
+ .catch(catchAxiosErrorFormatAndThrow)
+ .catch((error: FormattedAxiosError) => {
+ if (
+ error.response.status === 403 &&
+ ((error.response?.data?.message as string) ?? '').includes('disabled')
+ ) {
+ log.error(`Update failed with [403: ${error.response.data.message}].
+
+${chalk.red('Are you running this utility against a Serverless project?')}
+If so, the following entry should be added to your local
+'config/serverless.[project_type].dev.yml' (ex. 'serverless.security.dev.yml'):
+
+${chalk.bold(chalk.cyan('xpack.fleet.internal.fleetServerStandalone: false'))}
+
+`);
+ }
+
+ throw error;
+ })
.then((response) => response.data);
log.verbose(item);
From ec02f088d9ef0c9b49807947ae027f75c8227d38 Mon Sep 17 00:00:00 2001
From: Ignacio Rivas
Date: Mon, 14 Aug 2023 17:55:49 +0300
Subject: [PATCH 06/26] [Index Management] Add enrich policies fetch api and
expose in plugin api (#163556)
---
.../common/constants/api_base_path.ts | 2 +
.../common/constants/index.ts | 2 +-
.../plugins/index_management/common/index.ts | 2 +-
.../common/types/enrich_policies.ts | 16 +++++
.../index_management/common/types/index.ts | 2 +
.../plugins/index_management/public/mocks.ts | 3 +
.../plugins/index_management/public/plugin.ts | 3 +-
.../index_management/public/services/index.ts | 3 +
.../services/public_api_service.mock.ts | 18 ++++++
.../public/services/public_api_service.ts | 40 ++++++++++++
.../plugins/index_management/public/types.ts | 3 +-
.../server/lib/enrich_policies.test.ts | 24 ++++++++
.../server/lib/enrich_policies.ts | 52 ++++++++++++++++
.../enrich_policies/enrich_policies.test.ts | 61 +++++++++++++++++++
.../routes/api/enrich_policies/index.ts | 8 +++
.../register_enrich_policies_routes.ts | 14 +++++
.../enrich_policies/register_list_route.ts | 26 ++++++++
.../server/routes/api/index.ts | 4 +-
.../index_management/server/routes/index.ts | 2 +
.../server/test/helpers/index.ts | 2 +
.../server/test/helpers/policies_fixtures.ts | 19 ++++++
21 files changed, 301 insertions(+), 5 deletions(-)
create mode 100644 x-pack/plugins/index_management/common/types/enrich_policies.ts
create mode 100644 x-pack/plugins/index_management/public/services/public_api_service.mock.ts
create mode 100644 x-pack/plugins/index_management/public/services/public_api_service.ts
create mode 100644 x-pack/plugins/index_management/server/lib/enrich_policies.test.ts
create mode 100644 x-pack/plugins/index_management/server/lib/enrich_policies.ts
create mode 100644 x-pack/plugins/index_management/server/routes/api/enrich_policies/enrich_policies.test.ts
create mode 100644 x-pack/plugins/index_management/server/routes/api/enrich_policies/index.ts
create mode 100644 x-pack/plugins/index_management/server/routes/api/enrich_policies/register_enrich_policies_routes.ts
create mode 100644 x-pack/plugins/index_management/server/routes/api/enrich_policies/register_list_route.ts
create mode 100644 x-pack/plugins/index_management/server/test/helpers/policies_fixtures.ts
diff --git a/x-pack/plugins/index_management/common/constants/api_base_path.ts b/x-pack/plugins/index_management/common/constants/api_base_path.ts
index c923913bb9d83..c111d95dab192 100644
--- a/x-pack/plugins/index_management/common/constants/api_base_path.ts
+++ b/x-pack/plugins/index_management/common/constants/api_base_path.ts
@@ -6,3 +6,5 @@
*/
export const API_BASE_PATH = '/api/index_management';
+
+export const INTERNAL_API_BASE_PATH = '/internal/index_management';
diff --git a/x-pack/plugins/index_management/common/constants/index.ts b/x-pack/plugins/index_management/common/constants/index.ts
index 6641e6ef67c7d..786dad4a5e375 100644
--- a/x-pack/plugins/index_management/common/constants/index.ts
+++ b/x-pack/plugins/index_management/common/constants/index.ts
@@ -6,7 +6,7 @@
*/
export { BASE_PATH } from './base_path';
-export { API_BASE_PATH } from './api_base_path';
+export { API_BASE_PATH, INTERNAL_API_BASE_PATH } from './api_base_path';
export { INVALID_INDEX_PATTERN_CHARS, INVALID_TEMPLATE_NAME_CHARS } from './invalid_characters';
export * from './index_statuses';
diff --git a/x-pack/plugins/index_management/common/index.ts b/x-pack/plugins/index_management/common/index.ts
index 127123609b186..a481d17615d8d 100644
--- a/x-pack/plugins/index_management/common/index.ts
+++ b/x-pack/plugins/index_management/common/index.ts
@@ -8,7 +8,7 @@
// TODO: https://github.com/elastic/kibana/issues/110892
/* eslint-disable @kbn/eslint/no_export_all */
-export { API_BASE_PATH, BASE_PATH, MAJOR_VERSION } from './constants';
+export { API_BASE_PATH, INTERNAL_API_BASE_PATH, BASE_PATH, MAJOR_VERSION } from './constants';
export { getTemplateParameter } from './lib';
diff --git a/x-pack/plugins/index_management/common/types/enrich_policies.ts b/x-pack/plugins/index_management/common/types/enrich_policies.ts
new file mode 100644
index 0000000000000..4688cb41135f1
--- /dev/null
+++ b/x-pack/plugins/index_management/common/types/enrich_policies.ts
@@ -0,0 +1,16 @@
+/*
+ * 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 { EnrichPolicyType } from '@elastic/elasticsearch/lib/api/types';
+
+export interface SerializedEnrichPolicy {
+ type: EnrichPolicyType;
+ name: string;
+ sourceIndices: string[];
+ matchField: string;
+ enrichFields: string[];
+}
diff --git a/x-pack/plugins/index_management/common/types/index.ts b/x-pack/plugins/index_management/common/types/index.ts
index 0cc514b47024f..ce5d96a842366 100644
--- a/x-pack/plugins/index_management/common/types/index.ts
+++ b/x-pack/plugins/index_management/common/types/index.ts
@@ -16,3 +16,5 @@ export * from './templates';
export type { DataStreamFromEs, Health, DataStream, DataStreamIndex } from './data_streams';
export * from './component_templates';
+
+export * from './enrich_policies';
diff --git a/x-pack/plugins/index_management/public/mocks.ts b/x-pack/plugins/index_management/public/mocks.ts
index 30e21c80be5b1..69a43b985787a 100644
--- a/x-pack/plugins/index_management/public/mocks.ts
+++ b/x-pack/plugins/index_management/public/mocks.ts
@@ -6,12 +6,15 @@
*/
import { extensionsServiceMock } from './services/extensions_service.mock';
+import { publicApiServiceMock } from './services/public_api_service.mock';
export { extensionsServiceMock } from './services/extensions_service.mock';
+export { publicApiServiceMock } from './services/public_api_service.mock';
function createIdxManagementSetupMock() {
const mock = {
extensionsService: extensionsServiceMock,
+ publicApiService: publicApiServiceMock,
};
return mock;
diff --git a/x-pack/plugins/index_management/public/plugin.ts b/x-pack/plugins/index_management/public/plugin.ts
index 6625c9fd3ea4b..0771e254fd6aa 100644
--- a/x-pack/plugins/index_management/public/plugin.ts
+++ b/x-pack/plugins/index_management/public/plugin.ts
@@ -11,7 +11,7 @@ import SemVer from 'semver/classes/semver';
import { CoreSetup, PluginInitializerContext } from '@kbn/core/public';
import { setExtensionsService } from './application/store/selectors/extension_service';
-import { ExtensionsService } from './services';
+import { ExtensionsService, PublicApiService } from './services';
import {
IndexManagementPluginSetup,
@@ -66,6 +66,7 @@ export class IndexMgmtUIPlugin {
}
return {
+ apiService: new PublicApiService(coreSetup.http),
extensionsService: this.extensionsService.setup(),
};
}
diff --git a/x-pack/plugins/index_management/public/services/index.ts b/x-pack/plugins/index_management/public/services/index.ts
index f32787a427b89..8f4ddbeffba35 100644
--- a/x-pack/plugins/index_management/public/services/index.ts
+++ b/x-pack/plugins/index_management/public/services/index.ts
@@ -7,3 +7,6 @@
export type { ExtensionsSetup } from './extensions_service';
export { ExtensionsService } from './extensions_service';
+
+export type { PublicApiServiceSetup } from './public_api_service';
+export { PublicApiService } from './public_api_service';
diff --git a/x-pack/plugins/index_management/public/services/public_api_service.mock.ts b/x-pack/plugins/index_management/public/services/public_api_service.mock.ts
new file mode 100644
index 0000000000000..85ce1b232c06a
--- /dev/null
+++ b/x-pack/plugins/index_management/public/services/public_api_service.mock.ts
@@ -0,0 +1,18 @@
+/*
+ * 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 { PublicApiServiceSetup } from './public_api_service';
+
+export type PublicApiServiceSetupMock = jest.Mocked;
+
+const createServiceMock = (): PublicApiServiceSetupMock => ({
+ getAllEnrichPolicies: jest.fn(),
+});
+
+export const publicApiServiceMock = {
+ createSetupContract: createServiceMock,
+};
diff --git a/x-pack/plugins/index_management/public/services/public_api_service.ts b/x-pack/plugins/index_management/public/services/public_api_service.ts
new file mode 100644
index 0000000000000..33d43b9304fdb
--- /dev/null
+++ b/x-pack/plugins/index_management/public/services/public_api_service.ts
@@ -0,0 +1,40 @@
+/*
+ * 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 { HttpSetup } from '@kbn/core/public';
+import { sendRequest, SendRequestResponse } from '../shared_imports';
+import { API_BASE_PATH } from '../../common/constants';
+import { SerializedEnrichPolicy } from '../../common/types';
+
+export interface PublicApiServiceSetup {
+ getAllEnrichPolicies(): Promise>;
+}
+
+/**
+ * Index Management public API service
+ */
+export class PublicApiService {
+ private http: HttpSetup;
+
+ /**
+ * constructor
+ * @param http http dependency
+ */
+ constructor(http: HttpSetup) {
+ this.http = http;
+ }
+
+ /**
+ * Gets a list of all the enrich policies
+ */
+ getAllEnrichPolicies() {
+ return sendRequest(this.http, {
+ path: `${API_BASE_PATH}/enrich_policies`,
+ method: 'get',
+ });
+ }
+}
diff --git a/x-pack/plugins/index_management/public/types.ts b/x-pack/plugins/index_management/public/types.ts
index d9551c03e4352..b3e479b081fb4 100644
--- a/x-pack/plugins/index_management/public/types.ts
+++ b/x-pack/plugins/index_management/public/types.ts
@@ -8,9 +8,10 @@
import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public';
import { ManagementSetup } from '@kbn/management-plugin/public';
import { SharePluginStart } from '@kbn/share-plugin/public';
-import { ExtensionsSetup } from './services';
+import { ExtensionsSetup, PublicApiServiceSetup } from './services';
export interface IndexManagementPluginSetup {
+ apiService: PublicApiServiceSetup;
extensionsService: ExtensionsSetup;
}
diff --git a/x-pack/plugins/index_management/server/lib/enrich_policies.test.ts b/x-pack/plugins/index_management/server/lib/enrich_policies.test.ts
new file mode 100644
index 0000000000000..15517b2bfc20f
--- /dev/null
+++ b/x-pack/plugins/index_management/server/lib/enrich_policies.test.ts
@@ -0,0 +1,24 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { serializeEnrichmentPolicies } from './enrich_policies';
+import { createTestESEnrichPolicy } from '../test/helpers';
+
+describe('serializeEnrichmentPolicies', () => {
+ it('knows how to serialize a list of policies', async () => {
+ const mockedESPolicy = createTestESEnrichPolicy('my-policy', 'match');
+ expect(serializeEnrichmentPolicies([mockedESPolicy])).toEqual([
+ {
+ name: 'my-policy',
+ type: 'match',
+ sourceIndices: ['users'],
+ matchField: 'email',
+ enrichFields: ['first_name', 'last_name', 'city'],
+ },
+ ]);
+ });
+});
diff --git a/x-pack/plugins/index_management/server/lib/enrich_policies.ts b/x-pack/plugins/index_management/server/lib/enrich_policies.ts
new file mode 100644
index 0000000000000..ca1748a380c70
--- /dev/null
+++ b/x-pack/plugins/index_management/server/lib/enrich_policies.ts
@@ -0,0 +1,52 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { IScopedClusterClient } from '@kbn/core/server';
+import type { EnrichSummary, EnrichPolicyType } from '@elastic/elasticsearch/lib/api/types';
+import type { SerializedEnrichPolicy } from '../../common/types';
+
+const getPolicyType = (policy: EnrichSummary): EnrichPolicyType => {
+ if (policy.config.match) {
+ return 'match';
+ }
+
+ if (policy.config.geo_match) {
+ return 'geo_match';
+ }
+
+ if (policy.config.range) {
+ return 'range';
+ }
+
+ throw new Error('Unknown policy type');
+};
+
+export const serializeEnrichmentPolicies = (
+ policies: EnrichSummary[]
+): SerializedEnrichPolicy[] => {
+ return policies.map((policy: any) => {
+ const policyType = getPolicyType(policy);
+
+ return {
+ name: policy.config[policyType].name,
+ type: policyType,
+ sourceIndices: policy.config[policyType].indices,
+ matchField: policy.config[policyType].match_field,
+ enrichFields: policy.config[policyType].enrich_fields,
+ };
+ });
+};
+
+const fetchAll = async (client: IScopedClusterClient) => {
+ const res = await client.asCurrentUser.enrich.getPolicy();
+
+ return serializeEnrichmentPolicies(res.policies);
+};
+
+export const enrichPoliciesActions = {
+ fetchAll,
+};
diff --git a/x-pack/plugins/index_management/server/routes/api/enrich_policies/enrich_policies.test.ts b/x-pack/plugins/index_management/server/routes/api/enrich_policies/enrich_policies.test.ts
new file mode 100644
index 0000000000000..57d8f3f05a3d6
--- /dev/null
+++ b/x-pack/plugins/index_management/server/routes/api/enrich_policies/enrich_policies.test.ts
@@ -0,0 +1,61 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { addInternalBasePath } from '..';
+import { RouterMock, routeDependencies, RequestMock } from '../../../test/helpers';
+import { serializeEnrichmentPolicies } from '../../../lib/enrich_policies';
+import { createTestESEnrichPolicy } from '../../../test/helpers';
+
+import { registerEnrichPoliciesRoute } from './register_enrich_policies_routes';
+
+const mockedPolicy = createTestESEnrichPolicy('my-policy', 'match');
+
+describe('Enrich policies API', () => {
+ const router = new RouterMock();
+
+ beforeEach(() => {
+ registerEnrichPoliciesRoute({
+ ...routeDependencies,
+ router,
+ });
+ });
+
+ afterEach(() => {
+ jest.resetAllMocks();
+ });
+
+ describe('Get all policies - GET /internal/index_management/enrich_policies', () => {
+ const getEnrichPolicies = router.getMockESApiFn('enrich.getPolicy');
+
+ it('returns all available policies', async () => {
+ const mockRequest: RequestMock = {
+ method: 'get',
+ path: addInternalBasePath('/enrich_policies'),
+ };
+
+ getEnrichPolicies.mockResolvedValue({ policies: [mockedPolicy] });
+
+ const res = await router.runRequest(mockRequest);
+
+ expect(res).toEqual({
+ body: serializeEnrichmentPolicies([mockedPolicy]),
+ });
+ });
+
+ it('should return an error if it fails', async () => {
+ const mockRequest: RequestMock = {
+ method: 'get',
+ path: addInternalBasePath('/enrich_policies'),
+ };
+
+ const error = new Error('Oh no!');
+ getEnrichPolicies.mockRejectedValue(error);
+
+ await expect(router.runRequest(mockRequest)).rejects.toThrowError(error);
+ });
+ });
+});
diff --git a/x-pack/plugins/index_management/server/routes/api/enrich_policies/index.ts b/x-pack/plugins/index_management/server/routes/api/enrich_policies/index.ts
new file mode 100644
index 0000000000000..945728dfff9d8
--- /dev/null
+++ b/x-pack/plugins/index_management/server/routes/api/enrich_policies/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 { registerEnrichPoliciesRoute } from './register_enrich_policies_routes';
diff --git a/x-pack/plugins/index_management/server/routes/api/enrich_policies/register_enrich_policies_routes.ts b/x-pack/plugins/index_management/server/routes/api/enrich_policies/register_enrich_policies_routes.ts
new file mode 100644
index 0000000000000..ccafe26a2e68f
--- /dev/null
+++ b/x-pack/plugins/index_management/server/routes/api/enrich_policies/register_enrich_policies_routes.ts
@@ -0,0 +1,14 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { RouteDependencies } from '../../../types';
+
+import { registerListRoute } from './register_list_route';
+
+export function registerEnrichPoliciesRoute(dependencies: RouteDependencies) {
+ registerListRoute(dependencies);
+}
diff --git a/x-pack/plugins/index_management/server/routes/api/enrich_policies/register_list_route.ts b/x-pack/plugins/index_management/server/routes/api/enrich_policies/register_list_route.ts
new file mode 100644
index 0000000000000..1df52d8f2ba17
--- /dev/null
+++ b/x-pack/plugins/index_management/server/routes/api/enrich_policies/register_list_route.ts
@@ -0,0 +1,26 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { IScopedClusterClient } from '@kbn/core/server';
+import { RouteDependencies } from '../../../types';
+import { addInternalBasePath } from '..';
+import { enrichPoliciesActions } from '../../../lib/enrich_policies';
+
+export function registerListRoute({ router, lib: { handleEsError } }: RouteDependencies) {
+ router.get(
+ { path: addInternalBasePath('/enrich_policies'), validate: false },
+ async (context, request, response) => {
+ const client = (await context.core).elasticsearch.client as IScopedClusterClient;
+ try {
+ const policies = await enrichPoliciesActions.fetchAll(client);
+ return response.ok({ body: policies });
+ } catch (error) {
+ return handleEsError({ error, response });
+ }
+ }
+ );
+}
diff --git a/x-pack/plugins/index_management/server/routes/api/index.ts b/x-pack/plugins/index_management/server/routes/api/index.ts
index 98b16e21913a7..85d937717f41f 100644
--- a/x-pack/plugins/index_management/server/routes/api/index.ts
+++ b/x-pack/plugins/index_management/server/routes/api/index.ts
@@ -5,6 +5,8 @@
* 2.0.
*/
-import { API_BASE_PATH } from '../../../common';
+import { API_BASE_PATH, INTERNAL_API_BASE_PATH } from '../../../common';
export const addBasePath = (uri: string): string => API_BASE_PATH + uri;
+
+export const addInternalBasePath = (uri: string): string => INTERNAL_API_BASE_PATH + uri;
diff --git a/x-pack/plugins/index_management/server/routes/index.ts b/x-pack/plugins/index_management/server/routes/index.ts
index e2a2eaf1184f6..79d90762920bf 100644
--- a/x-pack/plugins/index_management/server/routes/index.ts
+++ b/x-pack/plugins/index_management/server/routes/index.ts
@@ -15,6 +15,7 @@ import { registerSettingsRoutes } from './api/settings';
import { registerStatsRoute } from './api/stats';
import { registerComponentTemplateRoutes } from './api/component_templates';
import { registerNodesRoute } from './api/nodes';
+import { registerEnrichPoliciesRoute } from './api/enrich_policies';
export class ApiRoutes {
setup(dependencies: RouteDependencies) {
@@ -26,6 +27,7 @@ export class ApiRoutes {
registerMappingRoute(dependencies);
registerComponentTemplateRoutes(dependencies);
registerNodesRoute(dependencies);
+ registerEnrichPoliciesRoute(dependencies);
}
start() {}
diff --git a/x-pack/plugins/index_management/server/test/helpers/index.ts b/x-pack/plugins/index_management/server/test/helpers/index.ts
index 682b520c12b00..cfce28a430198 100644
--- a/x-pack/plugins/index_management/server/test/helpers/index.ts
+++ b/x-pack/plugins/index_management/server/test/helpers/index.ts
@@ -9,3 +9,5 @@ export type { RequestMock } from './router_mock';
export { RouterMock } from './router_mock';
export { routeDependencies } from './route_dependencies';
+
+export { createTestESEnrichPolicy } from './policies_fixtures';
diff --git a/x-pack/plugins/index_management/server/test/helpers/policies_fixtures.ts b/x-pack/plugins/index_management/server/test/helpers/policies_fixtures.ts
new file mode 100644
index 0000000000000..235c4ad80a141
--- /dev/null
+++ b/x-pack/plugins/index_management/server/test/helpers/policies_fixtures.ts
@@ -0,0 +1,19 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import type { EnrichPolicyType } from '@elastic/elasticsearch/lib/api/types';
+
+export const createTestESEnrichPolicy = (name: string, type: EnrichPolicyType) => ({
+ config: {
+ [type]: {
+ name,
+ indices: ['users'],
+ match_field: 'email',
+ enrich_fields: ['first_name', 'last_name', 'city'],
+ },
+ },
+});
From fb1cf79a90919cf1e53961f8a5f02b295d20602a Mon Sep 17 00:00:00 2001
From: Tiago Costa
Date: Mon, 14 Aug 2023 15:57:57 +0100
Subject: [PATCH 07/26] fix(NA): ci checktypes job
---
.../routes/assistant_functions/get_apm_service_summary/index.ts | 1 +
1 file changed, 1 insertion(+)
diff --git a/x-pack/plugins/apm/server/routes/assistant_functions/get_apm_service_summary/index.ts b/x-pack/plugins/apm/server/routes/assistant_functions/get_apm_service_summary/index.ts
index 596ad49bf9fd4..23969c44f2ee1 100644
--- a/x-pack/plugins/apm/server/routes/assistant_functions/get_apm_service_summary/index.ts
+++ b/x-pack/plugins/apm/server/routes/assistant_functions/get_apm_service_summary/index.ts
@@ -285,6 +285,7 @@ export async function getApmServiceSummary({
}),
apmAlertsClient.search({
size: 100,
+ // @ts-expect-error types for apm alerts client needs to be reviewed
query: {
bool: {
filter: [
From 10102cce60e5480ea763721b8462599240ad2f94 Mon Sep 17 00:00:00 2001
From: Yngrid Coello
Date: Mon, 14 Aug 2023 17:08:22 +0200
Subject: [PATCH 08/26] [Logs onboarding] Install system integration when
onboarding system logs (#163794)
Closes https://github.com/elastic/kibana/issues/163244.
### Changes
- Use `@kbn/use-tracked-promise` to fetch data from fleet apis, couldn't
use the `useFetcher` hook because it's tailoring internal routes in the
plugin;
- `useInstallSystemIntegration ` is a new hook responsible for
installing the system integration.
- `system-integration-banner` component have been created to hold all
the logic related to system integration.
#### After this changes
##### When integration is not installed
https://github.com/elastic/kibana/assets/1313018/4162319f-35d3-42d3-bd4d-821d1da26a8b
##### When integration is already installed
https://github.com/elastic/kibana/assets/1313018/5f1bf76e-7ed4-4f2c-ba4c-a8b2f3ff80a2
If a user doesn't have the required privileges to install the
integrations they can still continue with the onboarding process but
will see the following message in the UI. After the onboarding is
finished they will be redirected to discover using dataset names
`system.auth` and `system.syslog`.
### How to test?
- Enter the [custom
deployment](https://yngrdyn-deploy-kiban-pr163794.kb.us-west2.gcp.elastic-cloud.com/)
- Check [installed
integrations](https://yngrdyn-deploy-kiban-pr163794.kb.us-west2.gcp.elastic-cloud.com/app/integrations/installed)
`/app/integrations/installed`
- Go to observability onboarding [landing
page](https://yngrdyn-deploy-kiban-pr163794.kb.us-west2.gcp.elastic-cloud.com/app/observabilityOnboarding):
`app/observabilityOnboarding`
- Select `Stream host system logs` or `Quickstart`
- System integration should install (if it's not installed in the
deployment) or just notify that has been installed (if it's already
installed in the deployment)
- After entering `Stream host system logs` page System integration
should be installed
---
.../app/system_logs/install_elastic_agent.tsx | 3 +
.../system_logs/system_integration_banner.tsx | 169 ++++++++++++++++++
.../components/shared/popover_tooltip.tsx | 50 ++++++
.../hooks/use_install_system_integration.ts | 91 ++++++++++
.../public/hooks/use_kibana.ts | 19 ++
5 files changed, 332 insertions(+)
create mode 100644 x-pack/plugins/observability_onboarding/public/components/app/system_logs/system_integration_banner.tsx
create mode 100644 x-pack/plugins/observability_onboarding/public/components/shared/popover_tooltip.tsx
create mode 100644 x-pack/plugins/observability_onboarding/public/hooks/use_install_system_integration.ts
create mode 100644 x-pack/plugins/observability_onboarding/public/hooks/use_kibana.ts
diff --git a/x-pack/plugins/observability_onboarding/public/components/app/system_logs/install_elastic_agent.tsx b/x-pack/plugins/observability_onboarding/public/components/app/system_logs/install_elastic_agent.tsx
index 5f12dead46f23..470ce4517bed4 100644
--- a/x-pack/plugins/observability_onboarding/public/components/app/system_logs/install_elastic_agent.tsx
+++ b/x-pack/plugins/observability_onboarding/public/components/app/system_logs/install_elastic_agent.tsx
@@ -37,6 +37,7 @@ import {
} from '../../shared/step_panel';
import { ApiKeyBanner } from '../custom_logs/wizard/api_key_banner';
import { getDiscoverNavigationParams } from '../utils';
+import { SystemIntegrationBanner } from './system_integration_banner';
import { TroubleshootingLink } from '../../shared/troubleshooting_link';
export function InstallElasticAgent() {
@@ -226,6 +227,8 @@ export function InstallElasticAgent() {
+
+
{apiKeyEncoded && onboardingId ? (
();
+ const [error, setError] = useState();
+
+ const onIntegrationCreationSuccess = useCallback(
+ ({ version }: { version?: string }) => {
+ setIntegrationVersion(version);
+ },
+ []
+ );
+
+ const onIntegrationCreationFailure = useCallback(
+ (e: SystemIntegrationError) => {
+ setError(e);
+ },
+ []
+ );
+
+ const { performRequest, requestState } = useInstallSystemIntegration({
+ onIntegrationCreationSuccess,
+ onIntegrationCreationFailure,
+ });
+
+ useEffect(() => {
+ performRequest();
+ }, [performRequest]);
+
+ const isInstallingIntegration = requestState.state === 'pending';
+ const hasFailedInstallingIntegration = requestState.state === 'rejected';
+ const hasInstalledIntegration = requestState.state === 'resolved';
+
+ if (isInstallingIntegration) {
+ return (
+
+
+
+
+
+ {i18n.translate(
+ 'xpack.observability_onboarding.systemIntegration.installing',
+ {
+ defaultMessage: 'Installing system integration',
+ }
+ )}
+
+
+ }
+ color="primary"
+ />
+ );
+ }
+ if (hasFailedInstallingIntegration) {
+ return (
+
+
+ {error?.message}
+
+
+ );
+ }
+ if (hasInstalledIntegration) {
+ return (
+
+
+
+
+ {i18n.translate(
+ 'xpack.observability_onboarding.systemIntegration.installed.tooltip.description',
+ {
+ defaultMessage:
+ 'Integrations streamline connecting your data to the Elastic Stack.',
+ }
+ )}
+
+
+ {
+ event.preventDefault();
+ navigateToAppUrl(
+ `/integrations/detail/system-${integrationVersion}`
+ );
+ }}
+ >
+ {i18n.translate(
+ 'xpack.observability_onboarding.systemIntegration.installed.tooltip.link.label',
+ {
+ defaultMessage: 'Learn more',
+ }
+ )}
+
+ ),
+ }}
+ />
+
+
+
+ ),
+ }}
+ />
+ }
+ color="success"
+ iconType="check"
+ />
+
+ );
+ }
+ return null;
+}
diff --git a/x-pack/plugins/observability_onboarding/public/components/shared/popover_tooltip.tsx b/x-pack/plugins/observability_onboarding/public/components/shared/popover_tooltip.tsx
new file mode 100644
index 0000000000000..66165edc8e133
--- /dev/null
+++ b/x-pack/plugins/observability_onboarding/public/components/shared/popover_tooltip.tsx
@@ -0,0 +1,50 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { EuiButtonIcon, EuiPopover, EuiPopoverTitle } from '@elastic/eui';
+import React, { useState } from 'react';
+
+interface PopoverTooltipProps {
+ ariaLabel?: string;
+ iconType?: string;
+ title?: string;
+ children: React.ReactNode;
+}
+
+export function PopoverTooltip({
+ ariaLabel,
+ iconType = 'iInCircle',
+ title,
+ children,
+}: PopoverTooltipProps) {
+ const [isPopoverOpen, setIsPopoverOpen] = useState(false);
+
+ return (
+ setIsPopoverOpen(false)}
+ style={{ margin: '-5px 0 0 -5px' }}
+ button={
+ ) => {
+ setIsPopoverOpen(!isPopoverOpen);
+ event.stopPropagation();
+ }}
+ size="xs"
+ color="primary"
+ iconType={iconType}
+ />
+ }
+ >
+ {title && {title}}
+ {children}
+
+ );
+}
diff --git a/x-pack/plugins/observability_onboarding/public/hooks/use_install_system_integration.ts b/x-pack/plugins/observability_onboarding/public/hooks/use_install_system_integration.ts
new file mode 100644
index 0000000000000..5473ebd09c240
--- /dev/null
+++ b/x-pack/plugins/observability_onboarding/public/hooks/use_install_system_integration.ts
@@ -0,0 +1,91 @@
+/*
+ * 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 { useCallback } from 'react';
+import { useTrackedPromise } from '@kbn/use-tracked-promise';
+import { i18n } from '@kbn/i18n';
+import { useKibana } from './use_kibana';
+
+// Errors
+const UNAUTHORIZED_ERROR = i18n.translate(
+ 'xpack.observability_onboarding.installSystemIntegration.error.unauthorized',
+ {
+ defaultMessage:
+ 'Required kibana privilege {requiredKibanaPrivileges} is missing, please add the required privilege to the role of the authenticated user.',
+ values: {
+ requiredKibanaPrivileges: "['Fleet', 'Integrations']",
+ },
+ }
+);
+
+type ErrorType = 'AuthorizationError' | 'UnknownError';
+export interface SystemIntegrationError {
+ type: ErrorType;
+ message: string;
+}
+
+type IntegrationInstallStatus =
+ | 'installed'
+ | 'installing'
+ | 'install_failed'
+ | 'not_installed';
+
+export const useInstallSystemIntegration = ({
+ onIntegrationCreationSuccess,
+ onIntegrationCreationFailure,
+}: {
+ onIntegrationCreationSuccess: ({ version }: { version?: string }) => void;
+ onIntegrationCreationFailure: (error: SystemIntegrationError) => void;
+}) => {
+ const {
+ services: { http },
+ } = useKibana();
+ const [requestState, callPerformRequest] = useTrackedPromise(
+ {
+ cancelPreviousOn: 'creation',
+ createPromise: async () => {
+ const { item: systemIntegration } = await http.get<{
+ item: { version: string; status: IntegrationInstallStatus };
+ }>('/api/fleet/epm/packages/system');
+
+ if (systemIntegration.status !== 'installed') {
+ await http.post('/api/fleet/epm/packages/system');
+ }
+
+ return {
+ version: systemIntegration.version,
+ };
+ },
+ onResolve: ({ version }: { version?: string }) => {
+ onIntegrationCreationSuccess({ version });
+ },
+ onReject: (requestError: any) => {
+ if (requestError?.body?.statusCode === 403) {
+ onIntegrationCreationFailure({
+ type: 'AuthorizationError' as const,
+ message: UNAUTHORIZED_ERROR,
+ });
+ } else {
+ onIntegrationCreationFailure({
+ type: 'UnknownError' as const,
+ message: requestError?.body?.message,
+ });
+ }
+ },
+ },
+ [onIntegrationCreationSuccess, onIntegrationCreationFailure]
+ );
+
+ const performRequest = useCallback(() => {
+ callPerformRequest();
+ }, [callPerformRequest]);
+
+ return {
+ performRequest,
+ requestState,
+ };
+};
diff --git a/x-pack/plugins/observability_onboarding/public/hooks/use_kibana.ts b/x-pack/plugins/observability_onboarding/public/hooks/use_kibana.ts
new file mode 100644
index 0000000000000..3102d3903b85f
--- /dev/null
+++ b/x-pack/plugins/observability_onboarding/public/hooks/use_kibana.ts
@@ -0,0 +1,19 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { CoreStart } from '@kbn/core/public';
+import {
+ context as KibanaContext,
+ KibanaContextProvider,
+ useKibana,
+} from '@kbn/kibana-react-plugin/public';
+
+export type Services = CoreStart;
+
+const useTypedKibana = () => useKibana();
+
+export { KibanaContextProvider, useTypedKibana as useKibana, KibanaContext };
From 78021ba4658403862d25b4725a0c4f8ae0ad2c44 Mon Sep 17 00:00:00 2001
From: Maryam Saeidi
Date: Mon, 14 Aug 2023 17:14:36 +0200
Subject: [PATCH 09/26] Replace locahost with 127.0.0.1 in synthrace default
config (#163813)
## Summary
To fix the following error when running `node scripts/synthtrace
simple_trace.ts --local --live`:
```
Error: Could not connect to Kibana: request to http://elastic:changeme@localhost:5601/ failed, reason: connect ECONNREFUSED ::1:5601
at getKibanaUrl (/kibana/packages/kbn-apm-synthtrace/src/cli/utils/get_service_urls.ts:76:11)
```
---
packages/kbn-apm-synthtrace/src/cli/utils/get_service_urls.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/packages/kbn-apm-synthtrace/src/cli/utils/get_service_urls.ts b/packages/kbn-apm-synthtrace/src/cli/utils/get_service_urls.ts
index 67a319b8dd498..64abc36c05602 100644
--- a/packages/kbn-apm-synthtrace/src/cli/utils/get_service_urls.ts
+++ b/packages/kbn-apm-synthtrace/src/cli/utils/get_service_urls.ts
@@ -80,8 +80,8 @@ async function getKibanaUrl({ target, logger }: { target: string; logger: Logger
export async function getServiceUrls({ logger, target, kibana }: RunOptions & { logger: Logger }) {
if (!target) {
// assume things are running locally
- kibana = kibana || 'http://localhost:5601';
- target = 'http://localhost:9200';
+ kibana = kibana || 'http://127.0.0.1:5601';
+ target = 'http://127.0.0.1:9200';
}
if (!target) {
From 650760784da2a0135ee25540a3f4e5148d794713 Mon Sep 17 00:00:00 2001
From: Dario Gieselaar
Date: Mon, 14 Aug 2023 17:31:07 +0200
Subject: [PATCH 10/26] [Connectors] await response when streaming sub action
fails (#162908)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../sub_action_connector.ts | 21 ++++++++++++
.../tests/chat/chat.spec.ts | 34 +++++++++++++++++++
2 files changed, 55 insertions(+)
diff --git a/x-pack/plugins/actions/server/sub_action_framework/sub_action_connector.ts b/x-pack/plugins/actions/server/sub_action_framework/sub_action_connector.ts
index a795b0eedc2ac..66f29afcd5ef1 100644
--- a/x-pack/plugins/actions/server/sub_action_framework/sub_action_connector.ts
+++ b/x-pack/plugins/actions/server/sub_action_framework/sub_action_connector.ts
@@ -11,6 +11,8 @@ import { Logger } from '@kbn/logging';
import axios, { AxiosInstance, AxiosResponse, AxiosError, AxiosRequestHeaders } from 'axios';
import { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server';
import { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
+import { finished } from 'stream/promises';
+import { IncomingMessage } from 'http';
import { assertURL } from './helpers/validators';
import { ActionsConfigurationUtilities } from '../actions_config';
import { SubAction, SubActionRequestParams } from './types';
@@ -140,6 +142,25 @@ export abstract class SubActionConnector {
`Request to external service failed. Connector Id: ${this.connector.id}. Connector type: ${this.connector.type}. Method: ${error.config.method}. URL: ${error.config.url}`
);
+ let responseBody = '';
+
+ // The error response body may also be a stream, e.g. for the GenAI connector
+ if (error.response?.config?.responseType === 'stream' && error.response?.data) {
+ try {
+ const incomingMessage = error.response.data as IncomingMessage;
+
+ incomingMessage.on('data', (chunk) => {
+ responseBody += chunk.toString();
+ });
+
+ await finished(incomingMessage);
+
+ error.response.data = JSON.parse(responseBody);
+ } catch {
+ // the response body is a nice to have, no worries if it fails
+ }
+ }
+
const errorMessage = `Status code: ${
error.status ?? error.response?.status
}. Message: ${this.getResponseErrorMessage(error)}`;
diff --git a/x-pack/test/observability_ai_assistant_api_integration/tests/chat/chat.spec.ts b/x-pack/test/observability_ai_assistant_api_integration/tests/chat/chat.spec.ts
index afca8f55f0e17..aabe8f898cc92 100644
--- a/x-pack/test/observability_ai_assistant_api_integration/tests/chat/chat.spec.ts
+++ b/x-pack/test/observability_ai_assistant_api_integration/tests/chat/chat.spec.ts
@@ -151,6 +151,40 @@ export default function ApiTest({ getService }: FtrProviderContext) {
]);
});
+ it('returns a useful error if the request fails', async () => {
+ requestHandler = (request, response) => {
+ response.writeHead(400, {
+ 'Content-Type': 'text/event-stream',
+ 'Cache-Control': 'no-cache',
+ Connection: 'keep-alive',
+ });
+
+ response.write(
+ JSON.stringify({
+ error: {
+ code: 'context_length_exceeded',
+ message:
+ "This model's maximum context length is 8192 tokens. However, your messages resulted in 11036 tokens. Please reduce the length of the messages.",
+ param: 'messages',
+ type: 'invalid_request_error',
+ },
+ })
+ );
+
+ response.end();
+ };
+
+ const response = await supertest.post(CHAT_API_URL).set('kbn-xsrf', 'foo').send({
+ messages,
+ connectorId,
+ functions: [],
+ });
+
+ expect(response.body.message).to.contain(
+ `400 - Bad Request - This model's maximum context length is 8192 tokens. However, your messages resulted in 11036 tokens. Please reduce the length of the messages.`
+ );
+ });
+
after(async () => {
requestHandler = () => {};
await supertest
From 3d8e425f4ac3dfdc25ab395d6253d98e933f8910 Mon Sep 17 00:00:00 2001
From: Ievgen Sorokopud
Date: Mon, 14 Aug 2023 18:07:16 +0200
Subject: [PATCH 11/26] [Security Solution][Skipped Test] Saved query
validation error test (#163050)
## Summary
Original ticket: https://github.com/elastic/kibana/issues/159060
This PR un-skips test which was disabled after the Rule Editing page
[refactoring](https://github.com/elastic/kibana/pull/157749). There we
stopped fields validation on page loading. To be able to show the
"failed to load saved query" error on page loading we force the field
validation when we failed to load a saved query.
---
.../custom_saved_query_rule.cy.ts | 28 +++++++++++++++----
.../pages/rule_editing/index.tsx | 2 --
.../rules/step_define_rule/schema.tsx | 6 ++--
.../rules/step_define_rule/translations.tsx | 7 -----
.../translations/translations/fr-FR.json | 1 -
.../translations/translations/ja-JP.json | 1 -
.../translations/translations/zh-CN.json | 1 -
7 files changed, 26 insertions(+), 20 deletions(-)
diff --git a/x-pack/plugins/security_solution/cypress/e2e/detection_response/rule_creation/custom_saved_query_rule.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_response/rule_creation/custom_saved_query_rule.cy.ts
index 3c43bca292602..8cb8fc2ba7576 100644
--- a/x-pack/plugins/security_solution/cypress/e2e/detection_response/rule_creation/custom_saved_query_rule.cy.ts
+++ b/x-pack/plugins/security_solution/cypress/e2e/detection_response/rule_creation/custom_saved_query_rule.cy.ts
@@ -9,7 +9,6 @@ import { getNewRule, getSavedQueryRule } from '../../../objects/rule';
import {
DEFINE_CONTINUE_BUTTON,
- CUSTOM_QUERY_BAR,
LOAD_QUERY_DYNAMICALLY_CHECKBOX,
QUERY_BAR,
} from '../../../screens/create_new_rule';
@@ -37,7 +36,7 @@ import {
} from '../../../tasks/create_new_rule';
import { saveEditedRule } from '../../../tasks/edit_rule';
import { login, visit } from '../../../tasks/login';
-import { getDetails } from '../../../tasks/rule_details';
+import { assertDetailsNotExist, getDetails } from '../../../tasks/rule_details';
import { createRule } from '../../../tasks/api_calls/rules';
import { RULE_CREATION, SECURITY_DETECTIONS_RULES_URL } from '../../../urls/navigation';
@@ -110,12 +109,29 @@ describe('Custom saved_query rules', () => {
cy.get(TOASTER).should('contain', FAILED_TO_LOAD_ERROR);
});
- // TODO: this error depended on the schema validation running. Can we show the error
- // based on the saved query failing to load instead of relying on the schema validation?
- it.skip('Shows validation error on rule edit when saved query can not be loaded', function () {
+ it('Shows validation error on rule edit when saved query can not be loaded', function () {
editFirstRule();
- cy.get(CUSTOM_QUERY_BAR).should('contain', FAILED_TO_LOAD_ERROR);
+ cy.get(TOASTER).should('contain', FAILED_TO_LOAD_ERROR);
+ });
+
+ it('Allows to update saved_query rule with non-existent query', () => {
+ editFirstRule();
+
+ cy.get(LOAD_QUERY_DYNAMICALLY_CHECKBOX).should('exist');
+
+ cy.intercept('PUT', '/api/detection_engine/rules').as('editedRule');
+ saveEditedRule();
+
+ cy.wait('@editedRule').then(({ response }) => {
+ // updated rule type shouldn't change
+ cy.wrap(response?.body.type).should('equal', 'saved_query');
+ });
+
+ cy.get(DEFINE_RULE_PANEL_PROGRESS).should('not.exist');
+
+ assertDetailsNotExist(SAVED_QUERY_NAME_DETAILS);
+ assertDetailsNotExist(SAVED_QUERY_DETAILS);
});
});
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_editing/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_editing/index.tsx
index 822a90cb0af6e..f749f63b3c5e4 100644
--- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_editing/index.tsx
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_editing/index.tsx
@@ -20,7 +20,6 @@ import { FormattedMessage } from '@kbn/i18n-react';
import type { FC } from 'react';
import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useParams } from 'react-router-dom';
-import { noop } from 'lodash';
import type { DataViewListItem } from '@kbn/data-views-plugin/common';
import { RulePreview } from '../../../../detections/components/rules/rule_preview';
@@ -163,7 +162,6 @@ const EditRulePageComponent: FC<{ rule: Rule }> = ({ rule }) => {
const { isSavedQueryLoading, savedQuery } = useGetSavedQuery({
savedQueryId: rule?.saved_id,
ruleType: rule?.type,
- onError: noop,
});
// Since in the edit step we start with an existing rule, we assume that
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/schema.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/schema.tsx
index 968f63f58c9ff..d996ee4e49592 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/schema.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/schema.tsx
@@ -39,7 +39,6 @@ import {
THREAT_MATCH_INDEX_HELPER_TEXT,
THREAT_MATCH_REQUIRED,
THREAT_MATCH_EMPTIES,
- SAVED_QUERY_REQUIRED,
} from './translations';
export const schema: FormSchema = {
@@ -147,7 +146,10 @@ export const schema: FormSchema = {
return undefined;
}
if (savedId) {
- return { code: 'ERR_FIELD_MISSING', path, message: SAVED_QUERY_REQUIRED };
+ // Ignore field validation error in this case.
+ // Instead, we show the error toast when saved query object does not exist.
+ // https://github.com/elastic/kibana/issues/159060
+ return undefined;
}
const message = isEqlRule(formData.ruleType) ? EQL_QUERY_REQUIRED : CUSTOM_QUERY_REQUIRED;
return { code: 'ERR_FIELD_MISSING', path, message };
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/translations.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/translations.tsx
index 28253e34550e2..12b82d427edb1 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/translations.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/translations.tsx
@@ -14,13 +14,6 @@ export const CUSTOM_QUERY_REQUIRED = i18n.translate(
}
);
-export const SAVED_QUERY_REQUIRED = i18n.translate(
- 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.savedQueryFieldRequiredError',
- {
- defaultMessage: 'Failed to load the saved query. Select a new one or add a custom query.',
- }
-);
-
export const EQL_QUERY_REQUIRED = i18n.translate(
'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.eqlQueryFieldRequiredError',
{
diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json
index 720b0e8d549a2..0a41979cac1c8 100644
--- a/x-pack/plugins/translations/translations/fr-FR.json
+++ b/x-pack/plugins/translations/translations/fr-FR.json
@@ -31155,7 +31155,6 @@
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.ruleTypeField.threatMatchTitle": "Correspondance d'indicateur",
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.ruleTypeField.thresholdTypeDescription": "Agrégez les résultats de recherche pour détecter à quel moment le nombre de correspondances dépasse le seuil.",
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.ruleTypeField.thresholdTypeTitle": "Seuil",
- "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.savedQueryFieldRequiredError": "Impossible de charger la requête enregistrée. Sélectionnez-en une nouvelle ou ajoutez une requête personnalisée.",
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.SavedQueryFormRowLabel": "Requête enregistrée",
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.source": "Source",
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.suppressionMissingFieldsLabel": "Si un champ de suppression est manquant",
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index a239b3d1e0477..806ad69f7faa3 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -31154,7 +31154,6 @@
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.ruleTypeField.threatMatchTitle": "インジケーター一致",
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.ruleTypeField.thresholdTypeDescription": "クエリ結果を集約し、いつ一致数がしきい値を超えるのかを検出します。",
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.ruleTypeField.thresholdTypeTitle": "しきい値",
- "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.savedQueryFieldRequiredError": "保存されたクエリを読み込めませんでした。新しいクエリを選択するか、カスタムクエリを追加してください。",
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.SavedQueryFormRowLabel": "保存されたクエリ",
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.source": "送信元",
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.suppressionMissingFieldsLabel": "抑制フィールドが欠落している場合",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index fa22bef215154..7cd9c279a468f 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -31150,7 +31150,6 @@
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.ruleTypeField.threatMatchTitle": "指标匹配",
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.ruleTypeField.thresholdTypeDescription": "聚合查询结果以检测匹配数目何时超过阈值。",
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.ruleTypeField.thresholdTypeTitle": "阈值",
- "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.savedQueryFieldRequiredError": "无法加载已保存查询。选择新查询或添加定制查询。",
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.SavedQueryFormRowLabel": "已保存查询",
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.source": "源",
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.suppressionMissingFieldsLabel": "如果阻止字段缺失",
From bc37dc2c5a7682941237a913a10b001e3d89cdc0 Mon Sep 17 00:00:00 2001
From: Dmitrii Shevchenko
Date: Mon, 14 Aug 2023 18:35:57 +0200
Subject: [PATCH 12/26] [Security Solution] Initial OpenAPI codegen
implementation (#163186)
**Resolves: https://github.com/elastic/security-team/issues/7134**
## Summary
Implemented request and response schema generation from OpenAPI
specifications.
The code generator script scans the
`x-pack/plugins/security_solution/common/api` directory, locates all
`*.schema.yaml` files, and generates a corresponding `*.gen.ts` artifact
for each, containing `zod` schema definitions.
Right now, all generation sources are set to `x-codegen-enabled: false`
to prevent the creation of duplicate schemas. Maintaining the old
`io-ts` schemas alongside the new `zod` ones could potentially lead to
confusion among developers. Thus, the recommended migration strategy is
to incrementally replace old schema usages with new ones, subsequently
removing outdated ones. I'll be implementing this approach in the
upcoming PRs.
### How to use the generator
If you need to test the generator locally, enable several sources and
run the generator script to see the results.
Navigate to `x-pack/plugins/security_solution` and run `yarn
openapi:generate`
Important note: if you want to enable route schemas, ensure you also
enable all their dependencies, such as common schemas. Failing to do so
will result in the generated code importing non-existent files.
### Example
Input file
(`x-pack/plugins/security_solution/common/api/detection_engine/model/error_schema.schema.yaml`):
```yaml
openapi: 3.0.0
info:
title: Error Schema
version: 'not applicable'
paths: {}
components:
schemas:
ErrorSchema:
type: object
required:
- error
properties:
id:
type: string
rule_id:
$ref: './rule_schema/common_attributes.schema.yaml#/components/schemas/RuleSignatureId'
list_id:
type: string
minLength: 1
item_id:
type: string
minLength: 1
error:
type: object
required:
- status_code
- message
properties:
status_code:
type: integer
minimum: 400
message:
type: string
```
Generated output file
(`x-pack/plugins/security_solution/common/api/detection_engine/model/error_schema.gen.ts`):
```ts
/*
* 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 { z } from 'zod';
/*
* NOTICE: Do not edit this file manually.
* This file is automatically generated by the OpenAPI Generator `yarn openapi:generate`.
*/
import { RuleSignatureId } from './rule_schema/common_attributes.gen';
export type ErrorSchema = z.infer;
export const ErrorSchema = z.object({
id: z.string().optional(),
rule_id: RuleSignatureId.optional(),
list_id: z.string().min(1).optional(),
item_id: z.string().min(1).optional(),
error: z.object({
status_code: z.number().min(400),
message: z.string(),
}),
});
```
---------
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
---
.github/CODEOWNERS | 2 +
.../model/error_schema.schema.yaml | 1 +
.../rule_schema/common_attributes.schema.yaml | 1 +
.../rule_schema/rule_schemas.schema.yaml | 2 +-
.../model/warning_schema.gen.ts | 21 ++++
...les_and_timelines_status_route.schema.yaml | 81 ++++++------
...uilt_rules_and_timelines_route.schema.yaml | 51 ++++----
.../bulk_actions_route.schema.yaml | 1 +
...ml => bulk_update_rules_route.schema.yaml} | 0
.../bulk_crud/response_schema.schema.yaml | 1 +
x-pack/plugins/security_solution/package.json | 4 +-
.../scripts/openapi/generate.js | 11 ++
.../scripts/openapi/lib/fix_eslint.ts | 19 +++
.../scripts/openapi/lib/format_output.ts | 12 ++
.../openapi/lib/remove_gen_artifacts.ts | 21 ++++
.../scripts/openapi/openapi_generator.ts | 77 ++++++++++++
.../parsers/get_api_operations_list.ts | 117 ++++++++++++++++++
.../scripts/openapi/parsers/get_components.ts | 15 +++
.../openapi/parsers/get_imports_map.ts | 77 ++++++++++++
.../scripts/openapi/parsers/openapi_types.ts | 57 +++++++++
.../template_service/register_helpers.ts | 55 ++++++++
.../template_service/register_templates.ts | 31 +++++
.../template_service/template_service.ts | 43 +++++++
.../templates/disclaimer.handlebars | 5 +
.../templates/schema_item.handlebars | 98 +++++++++++++++
.../templates/schemas.handlebars | 72 +++++++++++
.../plugins/security_solution/tsconfig.json | 1 +
27 files changed, 803 insertions(+), 73 deletions(-)
create mode 100644 x-pack/plugins/security_solution/common/api/detection_engine/model/warning_schema.gen.ts
rename x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_crud/bulk_update_rules/{bulk_patch_rules_route.schema.yaml => bulk_update_rules_route.schema.yaml} (100%)
create mode 100644 x-pack/plugins/security_solution/scripts/openapi/generate.js
create mode 100644 x-pack/plugins/security_solution/scripts/openapi/lib/fix_eslint.ts
create mode 100644 x-pack/plugins/security_solution/scripts/openapi/lib/format_output.ts
create mode 100644 x-pack/plugins/security_solution/scripts/openapi/lib/remove_gen_artifacts.ts
create mode 100644 x-pack/plugins/security_solution/scripts/openapi/openapi_generator.ts
create mode 100644 x-pack/plugins/security_solution/scripts/openapi/parsers/get_api_operations_list.ts
create mode 100644 x-pack/plugins/security_solution/scripts/openapi/parsers/get_components.ts
create mode 100644 x-pack/plugins/security_solution/scripts/openapi/parsers/get_imports_map.ts
create mode 100644 x-pack/plugins/security_solution/scripts/openapi/parsers/openapi_types.ts
create mode 100644 x-pack/plugins/security_solution/scripts/openapi/template_service/register_helpers.ts
create mode 100644 x-pack/plugins/security_solution/scripts/openapi/template_service/register_templates.ts
create mode 100644 x-pack/plugins/security_solution/scripts/openapi/template_service/template_service.ts
create mode 100644 x-pack/plugins/security_solution/scripts/openapi/template_service/templates/disclaimer.handlebars
create mode 100644 x-pack/plugins/security_solution/scripts/openapi/template_service/templates/schema_item.handlebars
create mode 100644 x-pack/plugins/security_solution/scripts/openapi/template_service/templates/schemas.handlebars
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index aa0442041059b..c60926929dc16 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -1197,6 +1197,8 @@ x-pack/plugins/cloud_integrations/cloud_full_story/server/config.ts @elastic/kib
/x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema @elastic/security-detection-rule-management @elastic/security-detection-engine
/x-pack/plugins/security_solution/server/utils @elastic/security-detection-rule-management
+/x-pack/plugins/security_solution/scripts/openapi @elastic/security-detection-rule-management
+
## Security Solution sub teams - Detection Engine
/x-pack/plugins/security_solution/common/api/detection_engine @elastic/security-detection-engine
diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/model/error_schema.schema.yaml b/x-pack/plugins/security_solution/common/api/detection_engine/model/error_schema.schema.yaml
index 6953912ad1a18..7e9a11ccf56dc 100644
--- a/x-pack/plugins/security_solution/common/api/detection_engine/model/error_schema.schema.yaml
+++ b/x-pack/plugins/security_solution/common/api/detection_engine/model/error_schema.schema.yaml
@@ -4,6 +4,7 @@ info:
version: 'not applicable'
paths: {}
components:
+ x-codegen-enabled: false
schemas:
ErrorSchema:
type: object
diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/common_attributes.schema.yaml b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/common_attributes.schema.yaml
index 6d9c48581578b..0e5a602e71018 100644
--- a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/common_attributes.schema.yaml
+++ b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/common_attributes.schema.yaml
@@ -4,6 +4,7 @@ info:
version: 'not applicable'
paths: {}
components:
+ x-codegen-enabled: false
schemas:
UUID:
type: string
diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_schemas.schema.yaml b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_schemas.schema.yaml
index acffa91ca5b74..a4cdcae498e7a 100644
--- a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_schemas.schema.yaml
+++ b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_schemas.schema.yaml
@@ -4,6 +4,7 @@ info:
version: 'not applicable'
paths: {}
components:
+ x-codegen-enabled: false
schemas:
SortOrder:
type: string
@@ -31,7 +32,6 @@ components:
type: object
description: |-
Rule execution result is an aggregate that groups plain rule execution events by execution UUID.
- It contains such information as execution UUID, date, status and metrics.
properties:
execution_uuid:
type: string
diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/model/warning_schema.gen.ts b/x-pack/plugins/security_solution/common/api/detection_engine/model/warning_schema.gen.ts
new file mode 100644
index 0000000000000..9bb7d32c19645
--- /dev/null
+++ b/x-pack/plugins/security_solution/common/api/detection_engine/model/warning_schema.gen.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 { z } from 'zod';
+
+/*
+ * NOTICE: Do not edit this file manually.
+ * This file is automatically generated by the OpenAPI Generator `yarn openapi:generate`.
+ */
+
+export type WarningSchema = z.infer;
+export const WarningSchema = z.object({
+ type: z.string(),
+ message: z.string(),
+ actionPath: z.string(),
+ buttonLabel: z.string().optional(),
+});
diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/get_prebuilt_rules_and_timelines_status/get_prebuilt_rules_and_timelines_status_route.schema.yaml b/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/get_prebuilt_rules_and_timelines_status/get_prebuilt_rules_and_timelines_status_route.schema.yaml
index 91e5c6b7150f4..deea1b32aa3aa 100644
--- a/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/get_prebuilt_rules_and_timelines_status/get_prebuilt_rules_and_timelines_status_route.schema.yaml
+++ b/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/get_prebuilt_rules_and_timelines_status/get_prebuilt_rules_and_timelines_status_route.schema.yaml
@@ -16,46 +16,41 @@ paths:
content:
application/json:
schema:
- $ref: '#/components/schemas/GetPrebuiltRulesStatusResponse'
-
-components:
- schemas:
- GetPrebuiltRulesStatusResponse:
- type: object
- properties:
- rules_custom_installed:
- type: integer
- description: The total number of custom rules
- minimum: 0
- rules_installed:
- type: integer
- description: The total number of installed prebuilt rules
- minimum: 0
- rules_not_installed:
- type: integer
- description: The total number of available prebuilt rules that are not installed
- minimum: 0
- rules_not_updated:
- type: integer
- description: The total number of outdated prebuilt rules
- minimum: 0
- timelines_installed:
- type: integer
- description: The total number of installed prebuilt timelines
- minimum: 0
- timelines_not_installed:
- type: integer
- description: The total number of available prebuilt timelines that are not installed
- minimum: 0
- timelines_not_updated:
- type: integer
- description: The total number of outdated prebuilt timelines
- minimum: 0
- required:
- - rules_custom_installed
- - rules_installed
- - rules_not_installed
- - rules_not_updated
- - timelines_installed
- - timelines_not_installed
- - timelines_not_updated
+ type: object
+ properties:
+ rules_custom_installed:
+ type: integer
+ description: The total number of custom rules
+ minimum: 0
+ rules_installed:
+ type: integer
+ description: The total number of installed prebuilt rules
+ minimum: 0
+ rules_not_installed:
+ type: integer
+ description: The total number of available prebuilt rules that are not installed
+ minimum: 0
+ rules_not_updated:
+ type: integer
+ description: The total number of outdated prebuilt rules
+ minimum: 0
+ timelines_installed:
+ type: integer
+ description: The total number of installed prebuilt timelines
+ minimum: 0
+ timelines_not_installed:
+ type: integer
+ description: The total number of available prebuilt timelines that are not installed
+ minimum: 0
+ timelines_not_updated:
+ type: integer
+ description: The total number of outdated prebuilt timelines
+ minimum: 0
+ required:
+ - rules_custom_installed
+ - rules_installed
+ - rules_not_installed
+ - rules_not_updated
+ - timelines_installed
+ - timelines_not_installed
+ - timelines_not_updated
diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/install_prebuilt_rules_and_timelines/install_prebuilt_rules_and_timelines_route.schema.yaml b/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/install_prebuilt_rules_and_timelines/install_prebuilt_rules_and_timelines_route.schema.yaml
index ec3ce832e3ef4..a7c2309d4a542 100644
--- a/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/install_prebuilt_rules_and_timelines/install_prebuilt_rules_and_timelines_route.schema.yaml
+++ b/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/install_prebuilt_rules_and_timelines/install_prebuilt_rules_and_timelines_route.schema.yaml
@@ -16,31 +16,26 @@ paths:
content:
application/json:
schema:
- $ref: '#/components/schemas/InstallPrebuiltRulesResponse'
-
-components:
- schemas:
- InstallPrebuiltRulesResponse:
- type: object
- properties:
- rules_installed:
- type: integer
- description: The number of rules installed
- minimum: 0
- rules_updated:
- type: integer
- description: The number of rules updated
- minimum: 0
- timelines_installed:
- type: integer
- description: The number of timelines installed
- minimum: 0
- timelines_updated:
- type: integer
- description: The number of timelines updated
- minimum: 0
- required:
- - rules_installed
- - rules_updated
- - timelines_installed
- - timelines_updated
+ type: object
+ properties:
+ rules_installed:
+ type: integer
+ description: The number of rules installed
+ minimum: 0
+ rules_updated:
+ type: integer
+ description: The number of rules updated
+ minimum: 0
+ timelines_installed:
+ type: integer
+ description: The number of timelines installed
+ minimum: 0
+ timelines_updated:
+ type: integer
+ description: The number of timelines updated
+ minimum: 0
+ required:
+ - rules_installed
+ - rules_updated
+ - timelines_installed
+ - timelines_updated
diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route.schema.yaml b/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route.schema.yaml
index f30f009e4b6a0..8eba09881bbd9 100644
--- a/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route.schema.yaml
+++ b/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route.schema.yaml
@@ -34,6 +34,7 @@ paths:
$ref: '#/components/schemas/BulkEditActionResponse'
components:
+ x-codegen-enabled: false
schemas:
BulkEditSkipReason:
type: string
diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_crud/bulk_update_rules/bulk_patch_rules_route.schema.yaml b/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_crud/bulk_update_rules/bulk_update_rules_route.schema.yaml
similarity index 100%
rename from x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_crud/bulk_update_rules/bulk_patch_rules_route.schema.yaml
rename to x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_crud/bulk_update_rules/bulk_update_rules_route.schema.yaml
diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_crud/response_schema.schema.yaml b/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_crud/response_schema.schema.yaml
index 99781d15f8eaa..b30ac7135c64d 100644
--- a/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_crud/response_schema.schema.yaml
+++ b/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_crud/response_schema.schema.yaml
@@ -4,6 +4,7 @@ info:
version: 8.9.0
paths: {}
components:
+ x-codegen-enabled: false
schemas:
BulkCrudRulesResponse:
type: array
diff --git a/x-pack/plugins/security_solution/package.json b/x-pack/plugins/security_solution/package.json
index 19c02e030d391..7f7b3e6f746e2 100644
--- a/x-pack/plugins/security_solution/package.json
+++ b/x-pack/plugins/security_solution/package.json
@@ -25,6 +25,8 @@
"test:generate": "node scripts/endpoint/resolver_generator",
"mappings:generate": "node scripts/mappings/mappings_generator",
"mappings:load": "node scripts/mappings/mappings_loader",
- "junit:transform": "node scripts/junit_transformer --pathPattern '../../../target/kibana-security-solution/cypress/results/*.xml' --rootDirectory ../../../ --reportName 'Security Solution Cypress' --writeInPlace"
+ "junit:transform": "node scripts/junit_transformer --pathPattern '../../../target/kibana-security-solution/cypress/results/*.xml' --rootDirectory ../../../ --reportName 'Security Solution Cypress' --writeInPlace",
+ "openapi:generate": "node scripts/openapi/generate",
+ "openapi:generate:debug": "node --inspect-brk scripts/openapi/generate"
}
}
diff --git a/x-pack/plugins/security_solution/scripts/openapi/generate.js b/x-pack/plugins/security_solution/scripts/openapi/generate.js
new file mode 100644
index 0000000000000..bd88357a3754d
--- /dev/null
+++ b/x-pack/plugins/security_solution/scripts/openapi/generate.js
@@ -0,0 +1,11 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+require('../../../../../src/setup_node_env');
+const { generate } = require('./openapi_generator');
+
+generate();
diff --git a/x-pack/plugins/security_solution/scripts/openapi/lib/fix_eslint.ts b/x-pack/plugins/security_solution/scripts/openapi/lib/fix_eslint.ts
new file mode 100644
index 0000000000000..23d8bf540f731
--- /dev/null
+++ b/x-pack/plugins/security_solution/scripts/openapi/lib/fix_eslint.ts
@@ -0,0 +1,19 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import execa from 'execa';
+import { resolve } from 'path';
+
+const KIBANA_ROOT = resolve(__dirname, '../../../../../../');
+
+export async function fixEslint(path: string) {
+ await execa('npx', ['eslint', '--fix', path], {
+ // Need to run eslint from the Kibana root directory, otherwise it will not
+ // be able to pick up the right config
+ cwd: KIBANA_ROOT,
+ });
+}
diff --git a/x-pack/plugins/security_solution/scripts/openapi/lib/format_output.ts b/x-pack/plugins/security_solution/scripts/openapi/lib/format_output.ts
new file mode 100644
index 0000000000000..6c374aa1f06d2
--- /dev/null
+++ b/x-pack/plugins/security_solution/scripts/openapi/lib/format_output.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 execa from 'execa';
+
+export async function formatOutput(path: string) {
+ await execa('npx', ['prettier', '--write', path]);
+}
diff --git a/x-pack/plugins/security_solution/scripts/openapi/lib/remove_gen_artifacts.ts b/x-pack/plugins/security_solution/scripts/openapi/lib/remove_gen_artifacts.ts
new file mode 100644
index 0000000000000..3cbf421b8c94b
--- /dev/null
+++ b/x-pack/plugins/security_solution/scripts/openapi/lib/remove_gen_artifacts.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 fs from 'fs/promises';
+import globby from 'globby';
+import { resolve } from 'path';
+
+/**
+ * Removes any *.gen.ts files from the target directory
+ *
+ * @param folderPath target directory
+ */
+export async function removeGenArtifacts(folderPath: string) {
+ const artifactsPath = await globby([resolve(folderPath, './**/*.gen.ts')]);
+
+ await Promise.all(artifactsPath.map((artifactPath) => fs.unlink(artifactPath)));
+}
diff --git a/x-pack/plugins/security_solution/scripts/openapi/openapi_generator.ts b/x-pack/plugins/security_solution/scripts/openapi/openapi_generator.ts
new file mode 100644
index 0000000000000..272e62061c6a4
--- /dev/null
+++ b/x-pack/plugins/security_solution/scripts/openapi/openapi_generator.ts
@@ -0,0 +1,77 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+/* eslint-disable no-console */
+
+import SwaggerParser from '@apidevtools/swagger-parser';
+import chalk from 'chalk';
+import fs from 'fs/promises';
+import globby from 'globby';
+import { resolve } from 'path';
+import { fixEslint } from './lib/fix_eslint';
+import { formatOutput } from './lib/format_output';
+import { removeGenArtifacts } from './lib/remove_gen_artifacts';
+import { getApiOperationsList } from './parsers/get_api_operations_list';
+import { getComponents } from './parsers/get_components';
+import { getImportsMap } from './parsers/get_imports_map';
+import type { OpenApiDocument } from './parsers/openapi_types';
+import { initTemplateService } from './template_service/template_service';
+
+const ROOT_SECURITY_SOLUTION_FOLDER = resolve(__dirname, '../..');
+const COMMON_API_FOLDER = resolve(ROOT_SECURITY_SOLUTION_FOLDER, './common/api');
+const SCHEMA_FILES_GLOB = resolve(ROOT_SECURITY_SOLUTION_FOLDER, './**/*.schema.yaml');
+const GENERATED_ARTIFACTS_GLOB = resolve(COMMON_API_FOLDER, './**/*.gen.ts');
+
+export const generate = async () => {
+ console.log(chalk.bold(`Generating API route schemas`));
+ console.log(chalk.bold(`Working directory: ${chalk.underline(COMMON_API_FOLDER)}`));
+
+ console.log(`👀 Searching for schemas`);
+ const schemaPaths = await globby([SCHEMA_FILES_GLOB]);
+
+ console.log(`🕵️♀️ Found ${schemaPaths.length} schemas, parsing`);
+ const parsedSchemas = await Promise.all(
+ schemaPaths.map(async (schemaPath) => {
+ const parsedSchema = (await SwaggerParser.parse(schemaPath)) as OpenApiDocument;
+ return { schemaPath, parsedSchema };
+ })
+ );
+
+ console.log(`🧹 Cleaning up any previously generated artifacts`);
+ await removeGenArtifacts(COMMON_API_FOLDER);
+
+ console.log(`🪄 Generating new artifacts`);
+ const TemplateService = await initTemplateService();
+ await Promise.all(
+ parsedSchemas.map(async ({ schemaPath, parsedSchema }) => {
+ const components = getComponents(parsedSchema);
+ const apiOperations = getApiOperationsList(parsedSchema);
+ const importsMap = getImportsMap(parsedSchema);
+
+ // If there are no operations or components to generate, skip this file
+ const shouldGenerate = apiOperations.length > 0 || components !== undefined;
+ if (!shouldGenerate) {
+ return;
+ }
+
+ const result = TemplateService.compileTemplate('schemas', {
+ components,
+ apiOperations,
+ importsMap,
+ });
+
+ // Write the generation result to disk
+ await fs.writeFile(schemaPath.replace('.schema.yaml', '.gen.ts'), result);
+ })
+ );
+
+ // Format the output folder using prettier as the generator produces
+ // unformatted code and fix any eslint errors
+ console.log(`💅 Formatting output`);
+ await formatOutput(GENERATED_ARTIFACTS_GLOB);
+ await fixEslint(GENERATED_ARTIFACTS_GLOB);
+};
diff --git a/x-pack/plugins/security_solution/scripts/openapi/parsers/get_api_operations_list.ts b/x-pack/plugins/security_solution/scripts/openapi/parsers/get_api_operations_list.ts
new file mode 100644
index 0000000000000..c9d9a75c07854
--- /dev/null
+++ b/x-pack/plugins/security_solution/scripts/openapi/parsers/get_api_operations_list.ts
@@ -0,0 +1,117 @@
+/*
+ * 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 { OpenAPIV3 } from 'openapi-types';
+import type { NormalizedOperation, ObjectSchema, OpenApiDocument } from './openapi_types';
+
+const HTTP_METHODS = Object.values(OpenAPIV3.HttpMethods);
+
+export function getApiOperationsList(parsedSchema: OpenApiDocument): NormalizedOperation[] {
+ const operations: NormalizedOperation[] = Object.entries(parsedSchema.paths).flatMap(
+ ([path, pathDefinition]) => {
+ return HTTP_METHODS.flatMap((method) => {
+ const operation = pathDefinition?.[method];
+ if (operation?.['x-codegen-enabled'] !== true) {
+ // Skip the operation if it's not enabled for codegen
+ return [];
+ }
+
+ // Convert the query parameters to a schema object. In OpenAPI spec the
+ // query and path params are different from the request body, we want to
+ // convert them to a single schema format to simplify their usage in the
+ // templates
+ const params: Record<'query' | 'path', ObjectSchema> = {
+ query: {
+ type: 'object',
+ properties: {},
+ required: [],
+ },
+ path: {
+ type: 'object',
+ properties: {},
+ required: [],
+ },
+ };
+
+ operation.parameters?.forEach((parameter) => {
+ if ('name' in parameter && (parameter.in === 'query' || parameter.in === 'path')) {
+ params[parameter.in].properties[parameter.name] = {
+ ...parameter.schema,
+ description: parameter.description,
+ };
+
+ if (parameter.required) {
+ params[parameter.in].required.push(parameter.name);
+ }
+ }
+ });
+
+ const requestParams = Object.keys(params.path.properties).length ? params.path : undefined;
+ const requestQuery = Object.keys(params.query.properties).length ? params.query : undefined;
+
+ // We don't use $ref in responses or request bodies currently, so we
+ // throw an error if we encounter one to narrow down the types. The
+ // support might be added in the future if needed.
+ if ('$ref' in operation.responses?.['200']) {
+ throw new Error(
+ `Cannot generate response for ${method} ${path}: $ref in response is not supported`
+ );
+ }
+ const response = operation.responses?.['200']?.content?.['application/json']?.schema;
+
+ if (operation.requestBody && '$ref' in operation.requestBody) {
+ throw new Error(
+ `Cannot generate request for ${method} ${path}: $ref in request body is not supported`
+ );
+ }
+ const requestBody = operation.requestBody?.content?.['application/json']?.schema;
+
+ const { operationId, description, tags, deprecated } = operation;
+
+ // Operation ID is used as a prefix for the generated function names,
+ // runtime schemas, etc. So it must be unique and not empty
+ if (!operationId) {
+ throw new Error(`Missing operationId for ${method} ${path}`);
+ }
+
+ return {
+ path,
+ method,
+ operationId,
+ description,
+ tags,
+ deprecated,
+ requestParams,
+ requestQuery,
+ requestBody,
+ response,
+ };
+ });
+ }
+ );
+
+ // Check that all operation IDs are unique
+ const operationIdOccurrences = operations.reduce((acc, operation) => {
+ acc[operation.operationId] = (acc[operation.operationId] ?? 0) + 1;
+ return acc;
+ }, {} as Record);
+ const duplicateOperationIds = Object.entries(operationIdOccurrences).filter(
+ ([, count]) => count > 1
+ );
+ if (duplicateOperationIds.length) {
+ throw new Error(
+ `Operation IDs must be unique, found duplicates: ${duplicateOperationIds
+ .map(([operationId, count]) => `${operationId} (${count})`)
+ .join(', ')}`
+ );
+ }
+
+ // Sort the operations by operationId to make the generated code more stable
+ operations.sort((a, b) => a.operationId.localeCompare(b.operationId));
+
+ return operations;
+}
diff --git a/x-pack/plugins/security_solution/scripts/openapi/parsers/get_components.ts b/x-pack/plugins/security_solution/scripts/openapi/parsers/get_components.ts
new file mode 100644
index 0000000000000..5b3fef72905c0
--- /dev/null
+++ b/x-pack/plugins/security_solution/scripts/openapi/parsers/get_components.ts
@@ -0,0 +1,15 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import type { OpenApiDocument } from './openapi_types';
+
+export function getComponents(parsedSchema: OpenApiDocument) {
+ if (parsedSchema.components?.['x-codegen-enabled'] === false) {
+ return undefined;
+ }
+ return parsedSchema.components;
+}
diff --git a/x-pack/plugins/security_solution/scripts/openapi/parsers/get_imports_map.ts b/x-pack/plugins/security_solution/scripts/openapi/parsers/get_imports_map.ts
new file mode 100644
index 0000000000000..8e068b61ba034
--- /dev/null
+++ b/x-pack/plugins/security_solution/scripts/openapi/parsers/get_imports_map.ts
@@ -0,0 +1,77 @@
+/*
+ * 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 { uniq } from 'lodash';
+import type { OpenApiDocument } from './openapi_types';
+
+export interface ImportsMap {
+ [importPath: string]: string[];
+}
+
+/**
+ * Traverse the OpenAPI document, find all external references, and return a map
+ * of import paths and imported symbols
+ *
+ * @param parsedSchema Parsed OpenAPI document
+ * @returns A map of import paths to symbols to import
+ */
+export const getImportsMap = (parsedSchema: OpenApiDocument): ImportsMap => {
+ const importMap: Record = {}; // key: import path, value: list of symbols to import
+ const refs = findRefs(parsedSchema);
+ refs.forEach((ref) => {
+ const refParts = ref.split('#/components/schemas/');
+ const importedSymbol = refParts[1];
+ let importPath = refParts[0];
+ if (importPath) {
+ importPath = importPath.replace('.schema.yaml', '.gen');
+ const currentSymbols = importMap[importPath] ?? [];
+ importMap[importPath] = uniq([...currentSymbols, importedSymbol]);
+ }
+ });
+
+ return importMap;
+};
+
+/**
+ * Check if an object has a $ref property
+ *
+ * @param obj Any object
+ * @returns True if the object has a $ref property
+ */
+const hasRef = (obj: unknown): obj is { $ref: string } => {
+ return typeof obj === 'object' && obj !== null && '$ref' in obj;
+};
+
+/**
+ * Traverse the OpenAPI document recursively and find all references
+ *
+ * @param obj Any object
+ * @returns A list of external references
+ */
+function findRefs(obj: unknown): string[] {
+ const refs: string[] = [];
+
+ function search(element: unknown) {
+ if (typeof element === 'object' && element !== null) {
+ if (hasRef(element)) {
+ refs.push(element.$ref);
+ }
+
+ Object.values(element).forEach((value) => {
+ if (Array.isArray(value)) {
+ value.forEach(search);
+ } else {
+ search(value);
+ }
+ });
+ }
+ }
+
+ search(obj);
+
+ return refs;
+}
diff --git a/x-pack/plugins/security_solution/scripts/openapi/parsers/openapi_types.ts b/x-pack/plugins/security_solution/scripts/openapi/parsers/openapi_types.ts
new file mode 100644
index 0000000000000..2449f34fa4b76
--- /dev/null
+++ b/x-pack/plugins/security_solution/scripts/openapi/parsers/openapi_types.ts
@@ -0,0 +1,57 @@
+/*
+ * 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 { OpenAPIV3 } from 'openapi-types';
+
+interface AdditionalProperties {
+ /**
+ * Whether or not the route and its schemas should be generated
+ */
+ 'x-codegen-enabled'?: boolean;
+}
+
+export type OpenApiDocument = OpenAPIV3.Document;
+
+// Override the OpenAPI types to add the x-codegen-enabled property to the
+// components object.
+declare module 'openapi-types' {
+ // eslint-disable-next-line @typescript-eslint/no-namespace
+ namespace OpenAPIV3 {
+ interface ComponentsObject {
+ 'x-codegen-enabled'?: boolean;
+ }
+ }
+}
+
+/**
+ * OpenAPI types do not have a dedicated type for objects, so we need to create
+ * to use for path and query parameters
+ */
+export interface ObjectSchema {
+ type: 'object';
+ required: string[];
+ description?: string;
+ properties: {
+ [name: string]: OpenAPIV3.ReferenceObject | OpenAPIV3.SchemaObject;
+ };
+}
+
+/**
+ * The normalized operation object that is used in the templates
+ */
+export interface NormalizedOperation {
+ path: string;
+ method: OpenAPIV3.HttpMethods;
+ operationId: string;
+ description?: string;
+ tags?: string[];
+ deprecated?: boolean;
+ requestParams?: ObjectSchema;
+ requestQuery?: ObjectSchema;
+ requestBody?: OpenAPIV3.ReferenceObject | OpenAPIV3.SchemaObject;
+ response?: OpenAPIV3.ReferenceObject | OpenAPIV3.SchemaObject;
+}
diff --git a/x-pack/plugins/security_solution/scripts/openapi/template_service/register_helpers.ts b/x-pack/plugins/security_solution/scripts/openapi/template_service/register_helpers.ts
new file mode 100644
index 0000000000000..b3bb02f7743c8
--- /dev/null
+++ b/x-pack/plugins/security_solution/scripts/openapi/template_service/register_helpers.ts
@@ -0,0 +1,55 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import type Handlebars from '@kbn/handlebars';
+import { snakeCase, camelCase } from 'lodash';
+
+export function registerHelpers(handlebarsInstance: typeof Handlebars) {
+ handlebarsInstance.registerHelper('concat', (...args) => {
+ const values = args.slice(0, -1) as unknown[];
+ return values.join('');
+ });
+ handlebarsInstance.registerHelper('parseRef', (refName: string) => {
+ return refName.split('/').pop();
+ });
+ handlebarsInstance.registerHelper('snakeCase', snakeCase);
+ handlebarsInstance.registerHelper('camelCase', camelCase);
+ handlebarsInstance.registerHelper('toJSON', (value: unknown) => {
+ return JSON.stringify(value);
+ });
+ handlebarsInstance.registerHelper('includes', (array: unknown, value: unknown) => {
+ if (!Array.isArray(array)) {
+ return false;
+ }
+ return array.includes(value);
+ });
+ handlebarsInstance.registerHelper('or', (...args) => {
+ // Last arguments is the handlebars context, so we ignore it
+ return args.slice(0, -1).some((arg) => arg);
+ });
+ handlebarsInstance.registerHelper('eq', (a, b) => {
+ return a === b;
+ });
+ handlebarsInstance.registerHelper('defined', (val) => {
+ return val !== undefined;
+ });
+ /**
+ * Check if the OpenAPI schema is unknown
+ */
+ handlebarsInstance.registerHelper('isUnknown', (val: object) => {
+ return !('type' in val || '$ref' in val || 'anyOf' in val || 'oneOf' in val || 'allOf' in val);
+ });
+ handlebarsInstance.registerHelper('isEmpty', (val) => {
+ if (Array.isArray(val)) {
+ return val.length === 0;
+ }
+ if (typeof val === 'object') {
+ return Object.keys(val).length === 0;
+ }
+ return val === undefined || val === null || val === '';
+ });
+}
diff --git a/x-pack/plugins/security_solution/scripts/openapi/template_service/register_templates.ts b/x-pack/plugins/security_solution/scripts/openapi/template_service/register_templates.ts
new file mode 100644
index 0000000000000..fa39b52d99471
--- /dev/null
+++ b/x-pack/plugins/security_solution/scripts/openapi/template_service/register_templates.ts
@@ -0,0 +1,31 @@
+/*
+ * 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 Handlebars from '@kbn/handlebars';
+import fs from 'fs/promises';
+import path from 'path';
+
+export async function registerTemplates(
+ templatesPath: string,
+ handlebarsInstance: typeof Handlebars
+) {
+ const files = await fs.readdir(templatesPath);
+
+ const fileContentsPromises = files.map(async (file) => {
+ const filePath = path.join(templatesPath, file);
+ const content = await fs.readFile(filePath, 'utf-8');
+ return { fileName: path.parse(file).name, content };
+ });
+
+ const fileContents = await Promise.all(fileContentsPromises);
+
+ fileContents.forEach(({ fileName, content }) => {
+ handlebarsInstance.registerPartial(fileName, content);
+ });
+
+ return Object.fromEntries(fileContents.map(({ fileName, content }) => [fileName, content]));
+}
diff --git a/x-pack/plugins/security_solution/scripts/openapi/template_service/template_service.ts b/x-pack/plugins/security_solution/scripts/openapi/template_service/template_service.ts
new file mode 100644
index 0000000000000..becb02bb54ebe
--- /dev/null
+++ b/x-pack/plugins/security_solution/scripts/openapi/template_service/template_service.ts
@@ -0,0 +1,43 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import Handlebars from 'handlebars';
+import type { OpenAPIV3 } from 'openapi-types';
+import { resolve } from 'path';
+import type { ImportsMap } from '../parsers/get_imports_map';
+import type { NormalizedOperation } from '../parsers/openapi_types';
+import { registerHelpers } from './register_helpers';
+import { registerTemplates } from './register_templates';
+
+export interface TemplateContext {
+ importsMap: ImportsMap;
+ apiOperations: NormalizedOperation[];
+ components: OpenAPIV3.ComponentsObject | undefined;
+}
+
+export type TemplateName = 'schemas';
+
+export interface ITemplateService {
+ compileTemplate: (templateName: TemplateName, context: TemplateContext) => string;
+}
+
+/**
+ * Initialize the template service. This service encapsulates the handlebars
+ * initialization logic and provides helper methods for compiling templates.
+ */
+export const initTemplateService = async (): Promise => {
+ // Create a handlebars instance and register helpers and partials
+ const handlebars = Handlebars.create();
+ registerHelpers(handlebars);
+ const templates = await registerTemplates(resolve(__dirname, './templates'), handlebars);
+
+ return {
+ compileTemplate: (templateName: TemplateName, context: TemplateContext) => {
+ return handlebars.compile(templates[templateName])(context);
+ },
+ };
+};
diff --git a/x-pack/plugins/security_solution/scripts/openapi/template_service/templates/disclaimer.handlebars b/x-pack/plugins/security_solution/scripts/openapi/template_service/templates/disclaimer.handlebars
new file mode 100644
index 0000000000000..4be0a93d1b79e
--- /dev/null
+++ b/x-pack/plugins/security_solution/scripts/openapi/template_service/templates/disclaimer.handlebars
@@ -0,0 +1,5 @@
+/*
+ * NOTICE: Do not edit this file manually.
+ * This file is automatically generated by the OpenAPI Generator `yarn openapi:generate`.
+ */
+
\ No newline at end of file
diff --git a/x-pack/plugins/security_solution/scripts/openapi/template_service/templates/schema_item.handlebars b/x-pack/plugins/security_solution/scripts/openapi/template_service/templates/schema_item.handlebars
new file mode 100644
index 0000000000000..87ce8e58105c7
--- /dev/null
+++ b/x-pack/plugins/security_solution/scripts/openapi/template_service/templates/schema_item.handlebars
@@ -0,0 +1,98 @@
+{{~#if type~}}
+ {{~> (concat "type_" type)~}}
+ {{~#if nullable}}.nullable(){{/if~}}
+ {{~#if (eq requiredBool false)}}.optional(){{/if~}}
+ {{~#if (defined default)}}.default({{{toJSON default}}}){{/if~}}
+{{~/if~}}
+
+{{~#if $ref~}}
+ {{parseRef $ref}}
+ {{~#if nullable}}.nullable(){{/if~}}
+ {{~#if (eq requiredBool false)}}.optional(){{/if~}}
+ {{~#if (defined default)}}.default({{{toJSON default}}}){{/if~}}
+{{~/if~}}
+
+{{~#if allOf~}}
+ {{~#each allOf~}}
+ {{~#if @first~}}
+ {{> schema_item }}
+ {{~else~}}
+ .and({{> schema_item }})
+ {{~/if~}}
+ {{~/each~}}
+{{~/if~}}
+
+{{~#if anyOf~}}
+ z.union([
+ {{~#each anyOf~}}
+ {{~> schema_item ~}},
+ {{~/each~}}
+ ])
+{{~/if~}}
+
+{{~#if oneOf~}}
+ z.union([
+ {{~#each oneOf~}}
+ {{~> schema_item ~}},
+ {{~/each~}}
+ ])
+{{~/if~}}
+
+{{#if (isUnknown .)}}
+z.unknown()
+{{/if}}
+
+{{~#*inline "type_array"~}}
+ {{~#if x-preprocess}}
+ z.preprocess({{x-preprocess}}, z.array({{~> schema_item items ~}}))
+ {{else}}
+ z.array({{~> schema_item items ~}})
+ {{~/if~}}
+ {{~#if minItems}}.min({{minItems}}){{/if~}}
+ {{~#if maxItems}}.max({{maxItems}}){{/if~}}
+{{~/inline~}}
+
+{{~#*inline "type_boolean"~}}
+ z.boolean()
+ {{~#if nullable}}.nullable(){{/if~}}
+{{~/inline~}}
+
+{{~#*inline "type_integer"~}}
+ {{~#if x-coerce}}
+ z.coerce.number()
+ {{~else~}}
+ z.number()
+ {{~/if~}}
+ {{~#if minimum includeZero=true}}.min({{minimum}}){{/if~}}
+ {{~#if maximum includeZero=true}}.max({{maximum}}){{/if~}}
+{{~/inline~}}
+
+{{~#*inline "type_object"~}}
+ z.object({
+ {{#each properties}}
+ {{#if description}}
+ /**
+ * {{{description}}}
+ */
+ {{/if}}
+ {{@key}}: {{> schema_item requiredBool=(includes ../required @key)}},
+ {{/each}}
+ })
+{{~/inline~}}
+
+{{~#*inline "type_string"~}}
+ {{~#if enum~}}
+ z.enum([
+ {{~#each enum~}}
+ "{{.}}",
+ {{~/each~}}
+ ])
+ {{~else~}}
+ z.string()
+ {{~#if minLength}}.min({{minLength}}){{/if~}}
+ {{~#if maxLength}}.max({{maxLength}}){{/if~}}
+ {{~#if (eq format 'date-time')}}.datetime(){{/if~}}
+ {{~/if~}}
+ {{#if transform}}.transform({{{transform}}}){{/if~}}
+{{~/inline~}}
+
diff --git a/x-pack/plugins/security_solution/scripts/openapi/template_service/templates/schemas.handlebars b/x-pack/plugins/security_solution/scripts/openapi/template_service/templates/schemas.handlebars
new file mode 100644
index 0000000000000..a6df5d96b124f
--- /dev/null
+++ b/x-pack/plugins/security_solution/scripts/openapi/template_service/templates/schemas.handlebars
@@ -0,0 +1,72 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { z } from "zod";
+
+{{> disclaimer}}
+
+{{#each importsMap}}
+import {
+ {{#each this}}{{.}},{{/each}}
+} from "{{@key}}"
+{{/each}}
+
+{{#each components.schemas}}
+{{#description}}
+/**
+ * {{{.}}}
+ */
+{{/description}}
+export type {{@key}} = z.infer;
+export const {{@key}} = {{> schema_item}};
+
+{{/each}}
+
+{{#each apiOperations}}
+{{#if requestQuery}}
+{{#if requestQuery.description}}
+/**
+* {{{requestQuery.description}}}
+*/
+{{/if}}
+export type {{operationId}}RequestQuery = z.infer;
+export const {{operationId}}RequestQuery = {{> schema_item requestQuery }};
+export type {{operationId}}RequestQueryInput = z.input;
+{{/if}}
+
+{{#if requestParams}}
+{{#if requestParams.description}}
+/**
+* {{{requestParams.description}}}
+*/
+{{/if}}
+export type {{operationId}}RequestParams = z.infer;
+export const {{operationId}}RequestParams = {{> schema_item requestParams }};
+export type {{operationId}}RequestParamsInput = z.input;
+{{/if}}
+
+{{#if requestBody}}
+{{#if requestBody.description}}
+/**
+* {{{requestBody.description}}}
+*/
+{{/if}}
+export type {{operationId}}RequestBody = z.infer;
+export const {{operationId}}RequestBody = {{> schema_item requestBody }};
+export type {{operationId}}RequestBodyInput = z.input;
+{{/if}}
+
+{{#if response}}
+{{#if response.description}}
+/**
+* {{{response.description}}}
+*/
+{{/if}}
+export type {{operationId}}Response = z.infer;
+export const {{operationId}}Response = {{> schema_item response }};
+{{/if}}
+{{/each}}
diff --git a/x-pack/plugins/security_solution/tsconfig.json b/x-pack/plugins/security_solution/tsconfig.json
index 6c543fd142522..fbcfe9df91c5a 100644
--- a/x-pack/plugins/security_solution/tsconfig.json
+++ b/x-pack/plugins/security_solution/tsconfig.json
@@ -169,5 +169,6 @@
"@kbn/alerts-ui-shared",
"@kbn/core-logging-server-mocks",
"@kbn/core-lifecycle-browser",
+ "@kbn/handlebars",
]
}
From 78250515ffc1d7172a91eef7f4639c7b98df3b92 Mon Sep 17 00:00:00 2001
From: Nathan Reese
Date: Mon, 14 Aug 2023 10:43:15 -0600
Subject: [PATCH 13/26] fix Canvas available in search in serverless (#163740)
Closes https://github.com/elastic/kibana/issues/163442
---------
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
config/serverless.yml | 2 +-
x-pack/plugins/canvas/server/config.test.ts | 53 ---------------------
x-pack/plugins/canvas/server/config.ts | 14 +-----
3 files changed, 2 insertions(+), 67 deletions(-)
delete mode 100644 x-pack/plugins/canvas/server/config.test.ts
diff --git a/config/serverless.yml b/config/serverless.yml
index b0fc28f058b2e..96cdcd7f52d15 100644
--- a/config/serverless.yml
+++ b/config/serverless.yml
@@ -43,7 +43,7 @@ dev_tools.deeplinks.navLinkStatus: visible
management.deeplinks.navLinkStatus: visible
# Other disabled plugins
-#xpack.canvas.enabled: false #only disabable in dev-mode
+xpack.canvas.enabled: false
xpack.cloud_integrations.data_migration.enabled: false
data.search.sessions.enabled: false
advanced_settings.enabled: false
diff --git a/x-pack/plugins/canvas/server/config.test.ts b/x-pack/plugins/canvas/server/config.test.ts
deleted file mode 100644
index 4c66c718d1b2d..0000000000000
--- a/x-pack/plugins/canvas/server/config.test.ts
+++ /dev/null
@@ -1,53 +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.
- */
-
-jest.mock('crypto', () => ({
- randomBytes: jest.fn(),
- constants: jest.requireActual('crypto').constants,
-}));
-
-jest.mock('@kbn/utils', () => ({
- getLogsPath: () => '/mock/kibana/logs/path',
-}));
-
-import { ConfigSchema } from './config';
-
-describe('config schema', () => {
- it('generates proper defaults', () => {
- expect(ConfigSchema.validate({})).toMatchInlineSnapshot(`
- Object {
- "enabled": true,
- }
- `);
-
- expect(ConfigSchema.validate({}, { dev: false })).toMatchInlineSnapshot(`
- Object {
- "enabled": true,
- }
- `);
-
- expect(ConfigSchema.validate({}, { dev: true })).toMatchInlineSnapshot(`
- Object {
- "enabled": true,
- }
- `);
- });
-
- it('should throw error if spaces is disabled', () => {
- expect(() => ConfigSchema.validate({ enabled: false })).toThrow(
- '[enabled]: Canvas can only be disabled in development mode'
- );
-
- expect(() => ConfigSchema.validate({ enabled: false }, { dev: false })).toThrow(
- '[enabled]: Canvas can only be disabled in development mode'
- );
- });
-
- it('should not throw error if spaces is disabled in development mode', () => {
- expect(() => ConfigSchema.validate({ enabled: false }, { dev: true })).not.toThrow();
- });
-});
diff --git a/x-pack/plugins/canvas/server/config.ts b/x-pack/plugins/canvas/server/config.ts
index 6cbcff6930d88..89ed9b3f74314 100644
--- a/x-pack/plugins/canvas/server/config.ts
+++ b/x-pack/plugins/canvas/server/config.ts
@@ -8,17 +8,5 @@
import { schema } from '@kbn/config-schema';
export const ConfigSchema = schema.object({
- enabled: schema.conditional(
- schema.contextRef('dev'),
- true,
- schema.boolean({ defaultValue: true }),
- schema.boolean({
- validate: (rawValue) => {
- if (rawValue === false) {
- return 'Canvas can only be disabled in development mode';
- }
- },
- defaultValue: true,
- })
- ),
+ enabled: schema.boolean({ defaultValue: true }),
});
From 0ce9d335bbc4db95215e2ecb0ff03cc397b07499 Mon Sep 17 00:00:00 2001
From: Dario Gieselaar
Date: Mon, 14 Aug 2023 18:43:59 +0200
Subject: [PATCH 14/26] [APM] Update usage of apmAlertsClient (#163827)
Co-authored-by: Achyut Jhunjhunwala
---
.../get_apm_service_summary/index.ts | 22 ++++++++++---------
1 file changed, 12 insertions(+), 10 deletions(-)
diff --git a/x-pack/plugins/apm/server/routes/assistant_functions/get_apm_service_summary/index.ts b/x-pack/plugins/apm/server/routes/assistant_functions/get_apm_service_summary/index.ts
index 23969c44f2ee1..927e715fb6598 100644
--- a/x-pack/plugins/apm/server/routes/assistant_functions/get_apm_service_summary/index.ts
+++ b/x-pack/plugins/apm/server/routes/assistant_functions/get_apm_service_summary/index.ts
@@ -285,16 +285,18 @@ export async function getApmServiceSummary({
}),
apmAlertsClient.search({
size: 100,
- // @ts-expect-error types for apm alerts client needs to be reviewed
- query: {
- bool: {
- filter: [
- ...termQuery(ALERT_RULE_PRODUCER, 'apm'),
- ...termQuery(ALERT_STATUS, ALERT_STATUS_ACTIVE),
- ...rangeQuery(start, end),
- ...termQuery(SERVICE_NAME, serviceName),
- ...environmentQuery(environment),
- ],
+ track_total_hits: false,
+ body: {
+ query: {
+ bool: {
+ filter: [
+ ...termQuery(ALERT_RULE_PRODUCER, 'apm'),
+ ...termQuery(ALERT_STATUS, ALERT_STATUS_ACTIVE),
+ ...rangeQuery(start, end),
+ ...termQuery(SERVICE_NAME, serviceName),
+ ...environmentQuery(environment),
+ ],
+ },
},
},
}),
From 261186313e93661c93e214b7dc9a26ba0aa5b749 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?=
Date: Mon, 14 Aug 2023 18:48:45 +0200
Subject: [PATCH 15/26] [Flaky test #131192] HTTP SSL Redirects (#163726)
---
.buildkite/ftr_configs.yml | 2 +-
.eslintrc.js | 1 +
test/server_integration/http/platform/cache.ts | 2 +-
.../http/platform/config.status.ts | 1 -
test/server_integration/http/platform/config.ts | 1 -
test/server_integration/http/platform/headers.ts | 1 -
test/server_integration/http/platform/status.ts | 1 -
.../http/ssl_redirect/{config.js => config.ts} | 3 ++-
.../http/ssl_redirect/{index.js => index.ts} | 12 ++++++++----
9 files changed, 13 insertions(+), 11 deletions(-)
rename test/server_integration/http/ssl_redirect/{config.js => config.ts} (93%)
rename test/server_integration/http/ssl_redirect/{index.js => index.ts} (65%)
diff --git a/.buildkite/ftr_configs.yml b/.buildkite/ftr_configs.yml
index e6eb1cec755c8..2e19b903cc8d3 100644
--- a/.buildkite/ftr_configs.yml
+++ b/.buildkite/ftr_configs.yml
@@ -149,7 +149,7 @@ enabled:
- test/plugin_functional/config.ts
- test/server_integration/http/platform/config.status.ts
- test/server_integration/http/platform/config.ts
- - test/server_integration/http/ssl_redirect/config.js
+ - test/server_integration/http/ssl_redirect/config.ts
- test/server_integration/http/ssl_with_p12_intermediate/config.js
- test/server_integration/http/ssl_with_p12/config.js
- test/server_integration/http/ssl/config.js
diff --git a/.eslintrc.js b/.eslintrc.js
index 515fbef1f2e4e..ddd39ed00747a 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -596,6 +596,7 @@ module.exports = {
'test/*/config_open.ts',
'test/*/*.config.ts',
'test/*/{tests,test_suites,apis,apps}/**/*',
+ 'test/server_integration/**/*.ts',
'x-pack/test/*/{tests,test_suites,apis,apps}/**/*',
'x-pack/test/*/*config.*ts',
'x-pack/test/saved_object_api_integration/*/apis/**/*',
diff --git a/test/server_integration/http/platform/cache.ts b/test/server_integration/http/platform/cache.ts
index 2c1aa90e963e2..6e1cd8ab39db0 100644
--- a/test/server_integration/http/platform/cache.ts
+++ b/test/server_integration/http/platform/cache.ts
@@ -7,7 +7,7 @@
*/
import { FtrProviderContext } from '../../services/types';
-// eslint-disable-next-line import/no-default-export
+
export default function ({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
diff --git a/test/server_integration/http/platform/config.status.ts b/test/server_integration/http/platform/config.status.ts
index 456756aa79262..638d850a8f75c 100644
--- a/test/server_integration/http/platform/config.status.ts
+++ b/test/server_integration/http/platform/config.status.ts
@@ -17,7 +17,6 @@ import { FtrConfigProviderContext, findTestPluginPaths } from '@kbn/test';
* and installing plugins against built Kibana. This test must be run against source only in order to build the
* fixture plugins
*/
-// eslint-disable-next-line import/no-default-export
export default async function ({ readConfigFile }: FtrConfigProviderContext) {
const httpConfig = await readConfigFile(require.resolve('../../config.base.js'));
diff --git a/test/server_integration/http/platform/config.ts b/test/server_integration/http/platform/config.ts
index 028ff67b43022..e78525cb8da60 100644
--- a/test/server_integration/http/platform/config.ts
+++ b/test/server_integration/http/platform/config.ts
@@ -8,7 +8,6 @@
import { FtrConfigProviderContext } from '@kbn/test';
-// eslint-disable-next-line import/no-default-export
export default async function ({ readConfigFile }: FtrConfigProviderContext) {
const httpConfig = await readConfigFile(require.resolve('../../config.base.js'));
diff --git a/test/server_integration/http/platform/headers.ts b/test/server_integration/http/platform/headers.ts
index 309dfbc71b5ff..1a8e9fd610679 100644
--- a/test/server_integration/http/platform/headers.ts
+++ b/test/server_integration/http/platform/headers.ts
@@ -14,7 +14,6 @@ import { FtrProviderContext } from '../../services/types';
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
const oneSec = 1_000;
-// eslint-disable-next-line import/no-default-export
export default function ({ getService }: FtrProviderContext) {
const config = getService('config');
diff --git a/test/server_integration/http/platform/status.ts b/test/server_integration/http/platform/status.ts
index 48006576128ce..50c136bfa1027 100644
--- a/test/server_integration/http/platform/status.ts
+++ b/test/server_integration/http/platform/status.ts
@@ -12,7 +12,6 @@ import { FtrProviderContext } from '../../services/types';
type ServiceStatusSerialized = Omit & { level: string };
-// eslint-disable-next-line import/no-default-export
export default function ({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const retry = getService('retry');
diff --git a/test/server_integration/http/ssl_redirect/config.js b/test/server_integration/http/ssl_redirect/config.ts
similarity index 93%
rename from test/server_integration/http/ssl_redirect/config.js
rename to test/server_integration/http/ssl_redirect/config.ts
index 47568b16bf6ba..8f8db0e9aae0e 100644
--- a/test/server_integration/http/ssl_redirect/config.js
+++ b/test/server_integration/http/ssl_redirect/config.ts
@@ -9,10 +9,11 @@
import Url from 'url';
import { readFileSync } from 'fs';
import { CA_CERT_PATH, KBN_CERT_PATH, KBN_KEY_PATH } from '@kbn/dev-utils';
+import { FtrConfigProviderContext } from '@kbn/test';
import { createKibanaSupertestProvider } from '../../services';
-export default async function ({ readConfigFile }) {
+export default async function ({ readConfigFile }: FtrConfigProviderContext) {
const httpConfig = await readConfigFile(require.resolve('../../config.base.js'));
const certificateAuthorities = [readFileSync(CA_CERT_PATH)];
diff --git a/test/server_integration/http/ssl_redirect/index.js b/test/server_integration/http/ssl_redirect/index.ts
similarity index 65%
rename from test/server_integration/http/ssl_redirect/index.js
rename to test/server_integration/http/ssl_redirect/index.ts
index 07ae0eb4bb565..6e4e7cfb7decf 100644
--- a/test/server_integration/http/ssl_redirect/index.js
+++ b/test/server_integration/http/ssl_redirect/index.ts
@@ -6,19 +6,23 @@
* Side Public License, v 1.
*/
-export default function ({ getService }) {
+import { FtrProviderContext } from '../../services/types';
+
+export default function ({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
- // Failing: See https://github.com/elastic/kibana/issues/131192
- // Failing: See https://github.com/elastic/kibana/issues/131192
- describe.skip('kibana server with ssl', () => {
+ describe('kibana server with ssl', () => {
it('redirects http requests at redirect port to https', async () => {
const host = process.env.TEST_KIBANA_HOST || 'localhost';
const port = process.env.TEST_KIBANA_PORT || '5620';
const url = `https://${host}:${port}/`;
await supertest.get('/').expect('location', url).expect(302);
+ });
+ // Skips because the current version of supertest cannot follow redirects
+ // Can be unskipped once https://github.com/elastic/kibana/pull/163716 is merged
+ it.skip('does not boot-loop (2nd redirect points to the landing page)', async () => {
await supertest.get('/').redirects(1).expect('location', '/spaces/enter').expect(302);
});
});
From 52b500b9e320d276a4be3f52141dcd65def2ab41 Mon Sep 17 00:00:00 2001
From: "Joey F. Poon"
Date: Mon, 14 Aug 2023 09:50:50 -0700
Subject: [PATCH 16/26] [Security Solution] add serverless flag to endpoint
policies (#163370)
---
.../common/endpoint/models/policy_config.ts | 4 +-
.../models/policy_config_helpers.test.ts | 9 +-
.../common/endpoint/types/index.ts | 2 +
.../policy/store/policy_details/index.test.ts | 1 +
.../fleet_integration.test.ts | 17 ++-
.../fleet_integration/fleet_integration.ts | 10 +-
.../handlers/create_default_policy.ts | 1 +
.../security_solution_serverless/kibana.jsonc | 3 +-
.../server/endpoint/services/index.ts | 1 +
.../services/set_package_policy_flag.test.ts | 118 ++++++++++++++++++
.../services/set_package_policy_flag.ts | 89 +++++++++++++
.../server/plugin.ts | 17 ++-
.../server/types.ts | 2 +
.../tsconfig.json | 3 +-
14 files changed, 266 insertions(+), 11 deletions(-)
create mode 100644 x-pack/plugins/security_solution_serverless/server/endpoint/services/set_package_policy_flag.test.ts
create mode 100644 x-pack/plugins/security_solution_serverless/server/endpoint/services/set_package_policy_flag.ts
diff --git a/x-pack/plugins/security_solution/common/endpoint/models/policy_config.ts b/x-pack/plugins/security_solution/common/endpoint/models/policy_config.ts
index f85587dc40d7d..986895e12b41b 100644
--- a/x-pack/plugins/security_solution/common/endpoint/models/policy_config.ts
+++ b/x-pack/plugins/security_solution/common/endpoint/models/policy_config.ts
@@ -16,7 +16,8 @@ export const policyFactory = (
cloud = false,
licenseUid = '',
clusterUuid = '',
- clusterName = ''
+ clusterName = '',
+ serverless = false
): PolicyConfig => {
return {
meta: {
@@ -25,6 +26,7 @@ export const policyFactory = (
cluster_uuid: clusterUuid,
cluster_name: clusterName,
cloud,
+ serverless,
},
windows: {
events: {
diff --git a/x-pack/plugins/security_solution/common/endpoint/models/policy_config_helpers.test.ts b/x-pack/plugins/security_solution/common/endpoint/models/policy_config_helpers.test.ts
index fe3fd8c2ebd6a..8be5c054fcfb0 100644
--- a/x-pack/plugins/security_solution/common/endpoint/models/policy_config_helpers.test.ts
+++ b/x-pack/plugins/security_solution/common/endpoint/models/policy_config_helpers.test.ts
@@ -192,7 +192,14 @@ describe('Policy Config helpers', () => {
// This constant makes sure that if the type `PolicyConfig` is ever modified,
// the logic for disabling protections is also modified due to type check.
export const eventsOnlyPolicy = (): PolicyConfig => ({
- meta: { license: '', cloud: false, license_uid: '', cluster_name: '', cluster_uuid: '' },
+ meta: {
+ license: '',
+ cloud: false,
+ license_uid: '',
+ cluster_name: '',
+ cluster_uuid: '',
+ serverless: false,
+ },
windows: {
events: {
credential_access: true,
diff --git a/x-pack/plugins/security_solution/common/endpoint/types/index.ts b/x-pack/plugins/security_solution/common/endpoint/types/index.ts
index 5702f14f2a37a..b71495d6288f2 100644
--- a/x-pack/plugins/security_solution/common/endpoint/types/index.ts
+++ b/x-pack/plugins/security_solution/common/endpoint/types/index.ts
@@ -945,6 +945,8 @@ export interface PolicyConfig {
license_uid: string;
cluster_uuid: string;
cluster_name: string;
+ serverless: boolean;
+ heartbeatinterval?: number;
};
windows: {
advanced?: {
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts
index f3fa972785673..43b35ec683963 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts
@@ -275,6 +275,7 @@ describe('policy details: ', () => {
license_uid: '',
cluster_name: '',
cluster_uuid: '',
+ serverless: false,
},
windows: {
events: {
diff --git a/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts b/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts
index f26531296b6a2..0ff3692971ad4 100644
--- a/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts
+++ b/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts
@@ -109,7 +109,8 @@ describe('ingest_integration tests ', () => {
cloud = cloudService.isCloudEnabled,
licenseUuid = 'updated-uid',
clusterUuid = '',
- clusterName = ''
+ clusterName = '',
+ isServerlessEnabled = cloudService.isServerlessEnabled
) => ({
type: 'endpoint',
enabled: true,
@@ -118,7 +119,14 @@ describe('ingest_integration tests ', () => {
integration_config: {},
policy: {
value: disableProtections(
- policyFactory(license, cloud, licenseUuid, clusterUuid, clusterName)
+ policyFactory(
+ license,
+ cloud,
+ licenseUuid,
+ clusterUuid,
+ clusterName,
+ isServerlessEnabled
+ )
),
},
artifact_manifest: { value: manifest },
@@ -527,6 +535,7 @@ describe('ingest_integration tests ', () => {
beforeEach(() => {
licenseEmitter.next(Platinum); // set license level to platinum
});
+
it('updates successfully when meta fields differ from services', async () => {
const mockPolicy = policyFactory();
mockPolicy.meta.cloud = true; // cloud mock will return true
@@ -534,6 +543,7 @@ describe('ingest_integration tests ', () => {
mockPolicy.meta.cluster_name = 'updated-name';
mockPolicy.meta.cluster_uuid = 'updated-uuid';
mockPolicy.meta.license_uid = 'updated-uid';
+ mockPolicy.meta.serverless = false;
const logger = loggingSystemMock.create().get('ingest_integration.test');
const callback = getPackagePolicyUpdateCallback(
logger,
@@ -552,6 +562,7 @@ describe('ingest_integration tests ', () => {
policyConfig.inputs[0]!.config!.policy.value.meta.cluster_name = 'original-name';
policyConfig.inputs[0]!.config!.policy.value.meta.cluster_uuid = 'original-uuid';
policyConfig.inputs[0]!.config!.policy.value.meta.license_uid = 'original-uid';
+ policyConfig.inputs[0]!.config!.policy.value.meta.serverless = true;
const updatedPolicyConfig = await callback(
policyConfig,
soClient,
@@ -569,6 +580,7 @@ describe('ingest_integration tests ', () => {
mockPolicy.meta.cluster_name = 'updated-name';
mockPolicy.meta.cluster_uuid = 'updated-uuid';
mockPolicy.meta.license_uid = 'updated-uid';
+ mockPolicy.meta.serverless = false;
const logger = loggingSystemMock.create().get('ingest_integration.test');
const callback = getPackagePolicyUpdateCallback(
logger,
@@ -586,6 +598,7 @@ describe('ingest_integration tests ', () => {
policyConfig.inputs[0]!.config!.policy.value.meta.cluster_name = 'updated-name';
policyConfig.inputs[0]!.config!.policy.value.meta.cluster_uuid = 'updated-uuid';
policyConfig.inputs[0]!.config!.policy.value.meta.license_uid = 'updated-uid';
+ policyConfig.inputs[0]!.config!.policy.value.meta.serverless = false;
const updatedPolicyConfig = await callback(
policyConfig,
soClient,
diff --git a/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.ts b/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.ts
index 04bc9afa6d3a1..a9da860a5008e 100644
--- a/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.ts
+++ b/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.ts
@@ -57,14 +57,16 @@ const shouldUpdateMetaValues = (
currentCloudInfo: boolean,
currentClusterName: string,
currentClusterUUID: string,
- currentLicenseUID: string
+ currentLicenseUID: string,
+ currentIsServerlessEnabled: boolean
) => {
return (
endpointPackagePolicy.meta.license !== currentLicenseType ||
endpointPackagePolicy.meta.cloud !== currentCloudInfo ||
endpointPackagePolicy.meta.cluster_name !== currentClusterName ||
endpointPackagePolicy.meta.cluster_uuid !== currentClusterUUID ||
- endpointPackagePolicy.meta.license_uid !== currentLicenseUID
+ endpointPackagePolicy.meta.license_uid !== currentLicenseUID ||
+ endpointPackagePolicy.meta.serverless !== currentIsServerlessEnabled
);
};
@@ -221,7 +223,8 @@ export const getPackagePolicyUpdateCallback = (
cloud?.isCloudEnabled,
esClientInfo.cluster_name,
esClientInfo.cluster_uuid,
- licenseService.getLicenseUID()
+ licenseService.getLicenseUID(),
+ cloud?.isServerlessEnabled
)
) {
newEndpointPackagePolicy.meta.license = licenseService.getLicenseType();
@@ -229,6 +232,7 @@ export const getPackagePolicyUpdateCallback = (
newEndpointPackagePolicy.meta.cluster_name = esClientInfo.cluster_name;
newEndpointPackagePolicy.meta.cluster_uuid = esClientInfo.cluster_uuid;
newEndpointPackagePolicy.meta.license_uid = licenseService.getLicenseUID();
+ newEndpointPackagePolicy.meta.serverless = cloud?.isServerlessEnabled;
endpointIntegrationData.inputs[0].config.policy.value = newEndpointPackagePolicy;
}
diff --git a/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.ts b/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.ts
index db053fd5c3b0e..75addef37ee6e 100644
--- a/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.ts
+++ b/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.ts
@@ -49,6 +49,7 @@ export const createDefaultPolicy = (
? esClientInfo.cluster_uuid
: factoryPolicy.meta.cluster_uuid;
factoryPolicy.meta.license_uid = licenseService.getLicenseUID();
+ factoryPolicy.meta.serverless = cloud.isServerlessEnabled || false;
let defaultPolicyPerType: PolicyConfig =
config?.type === 'cloud'
diff --git a/x-pack/plugins/security_solution_serverless/kibana.jsonc b/x-pack/plugins/security_solution_serverless/kibana.jsonc
index cd2cb7c705634..68b6eb71af8d5 100644
--- a/x-pack/plugins/security_solution_serverless/kibana.jsonc
+++ b/x-pack/plugins/security_solution_serverless/kibana.jsonc
@@ -19,7 +19,8 @@
"securitySolution",
"serverless",
"taskManager",
- "cloud"
+ "cloud",
+ "fleet"
],
"optionalPlugins": [
"securitySolutionEss"
diff --git a/x-pack/plugins/security_solution_serverless/server/endpoint/services/index.ts b/x-pack/plugins/security_solution_serverless/server/endpoint/services/index.ts
index 990731eb640a8..d39a8ed11d24b 100644
--- a/x-pack/plugins/security_solution_serverless/server/endpoint/services/index.ts
+++ b/x-pack/plugins/security_solution_serverless/server/endpoint/services/index.ts
@@ -6,3 +6,4 @@
*/
export { endpointMeteringService } from './metering_service';
+export { setEndpointPackagePolicyServerlessFlag } from './set_package_policy_flag';
diff --git a/x-pack/plugins/security_solution_serverless/server/endpoint/services/set_package_policy_flag.test.ts b/x-pack/plugins/security_solution_serverless/server/endpoint/services/set_package_policy_flag.test.ts
new file mode 100644
index 0000000000000..54a95ae68a1b7
--- /dev/null
+++ b/x-pack/plugins/security_solution_serverless/server/endpoint/services/set_package_policy_flag.test.ts
@@ -0,0 +1,118 @@
+/*
+ * 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 { cloneDeep } from 'lodash';
+
+import type { SavedObjectsClientContract } from '@kbn/core/server';
+import { elasticsearchServiceMock, savedObjectsClientMock } from '@kbn/core/server/mocks';
+import type { ElasticsearchClientMock } from '@kbn/core/server/mocks';
+import {
+ FLEET_ENDPOINT_PACKAGE,
+ PACKAGE_POLICY_SAVED_OBJECT_TYPE,
+ SO_SEARCH_LIMIT,
+} from '@kbn/fleet-plugin/common';
+import type { PackagePolicy } from '@kbn/fleet-plugin/common';
+import type { PackagePolicyClient } from '@kbn/fleet-plugin/server';
+import { createPackagePolicyServiceMock } from '@kbn/fleet-plugin/server/mocks';
+import { policyFactory } from '@kbn/security-solution-plugin/common/endpoint/models/policy_config';
+
+import { setEndpointPackagePolicyServerlessFlag } from './set_package_policy_flag';
+
+describe('setEndpointPackagePolicyServerlessFlag', () => {
+ let esClientMock: ElasticsearchClientMock;
+ let soClientMock: jest.Mocked;
+ let packagePolicyServiceMock: jest.Mocked;
+
+ function generatePackagePolicy(policy = policyFactory()): PackagePolicy {
+ return {
+ inputs: [
+ {
+ config: {
+ policy: {
+ value: policy,
+ },
+ },
+ },
+ ],
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ } as any as PackagePolicy;
+ }
+
+ beforeEach(() => {
+ esClientMock = elasticsearchServiceMock.createClusterClient().asInternalUser;
+ soClientMock = savedObjectsClientMock.create();
+ packagePolicyServiceMock = createPackagePolicyServiceMock();
+ });
+
+ it('updates serverless flag for endpoint policies', async () => {
+ const packagePolicy1 = generatePackagePolicy();
+ const packagePolicy2 = generatePackagePolicy();
+ packagePolicyServiceMock.list.mockResolvedValue({
+ items: [packagePolicy1, packagePolicy2],
+ page: 1,
+ perPage: SO_SEARCH_LIMIT,
+ total: 2,
+ });
+ packagePolicyServiceMock.bulkCreate.mockImplementation();
+
+ await setEndpointPackagePolicyServerlessFlag(
+ soClientMock,
+ esClientMock,
+ packagePolicyServiceMock
+ );
+
+ const expectedPolicy1 = cloneDeep(packagePolicy1);
+ expectedPolicy1!.inputs[0]!.config!.policy.value.meta.serverless = true;
+ const expectedPolicy2 = cloneDeep(packagePolicy2);
+ expectedPolicy2!.inputs[0]!.config!.policy.value.meta.serverless = true;
+ const expectedPolicies = [expectedPolicy1, expectedPolicy2];
+ expect(packagePolicyServiceMock.list).toBeCalledWith(soClientMock, {
+ page: 1,
+ perPage: SO_SEARCH_LIMIT,
+ kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:${FLEET_ENDPOINT_PACKAGE}`,
+ });
+ expect(packagePolicyServiceMock.bulkUpdate).toBeCalledWith(
+ soClientMock,
+ esClientMock,
+ expectedPolicies
+ );
+ });
+
+ it('batches properly when over perPage', async () => {
+ packagePolicyServiceMock.list
+ .mockResolvedValueOnce({
+ items: [],
+ page: 1,
+ perPage: SO_SEARCH_LIMIT,
+ total: SO_SEARCH_LIMIT,
+ })
+ .mockResolvedValueOnce({
+ items: [],
+ page: 2,
+ perPage: SO_SEARCH_LIMIT,
+ total: 1,
+ });
+ packagePolicyServiceMock.bulkCreate.mockImplementation();
+
+ await setEndpointPackagePolicyServerlessFlag(
+ soClientMock,
+ esClientMock,
+ packagePolicyServiceMock
+ );
+
+ expect(packagePolicyServiceMock.list).toHaveBeenNthCalledWith(1, soClientMock, {
+ page: 1,
+ perPage: SO_SEARCH_LIMIT,
+ kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:${FLEET_ENDPOINT_PACKAGE}`,
+ });
+ expect(packagePolicyServiceMock.list).toHaveBeenNthCalledWith(2, soClientMock, {
+ page: 2,
+ perPage: SO_SEARCH_LIMIT,
+ kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:${FLEET_ENDPOINT_PACKAGE}`,
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution_serverless/server/endpoint/services/set_package_policy_flag.ts b/x-pack/plugins/security_solution_serverless/server/endpoint/services/set_package_policy_flag.ts
new file mode 100644
index 0000000000000..0c6191e8df706
--- /dev/null
+++ b/x-pack/plugins/security_solution_serverless/server/endpoint/services/set_package_policy_flag.ts
@@ -0,0 +1,89 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import type { SavedObjectsClientContract, ElasticsearchClient } from '@kbn/core/server';
+import type { PackagePolicyClient } from '@kbn/fleet-plugin/server';
+import type { ListResult, PackagePolicy } from '@kbn/fleet-plugin/common';
+import {
+ FLEET_ENDPOINT_PACKAGE,
+ PACKAGE_POLICY_SAVED_OBJECT_TYPE,
+ SO_SEARCH_LIMIT,
+} from '@kbn/fleet-plugin/common';
+
+// set all endpoint policies serverless flag to true
+// required so that endpoint will write heartbeats
+export async function setEndpointPackagePolicyServerlessFlag(
+ soClient: SavedObjectsClientContract,
+ esClient: ElasticsearchClient,
+ packagePolicyService: PackagePolicyClient
+): Promise {
+ const perPage: number = SO_SEARCH_LIMIT;
+ let page: number = 1;
+ let endpointPackagesResult: ListResult | undefined;
+
+ while (page === 1 || endpointPackagesResult?.total === perPage) {
+ endpointPackagesResult = await getEndpointPackagePolicyBatch(
+ soClient,
+ packagePolicyService,
+ page,
+ perPage
+ );
+ await processBatch(endpointPackagesResult, soClient, esClient, packagePolicyService);
+ page++;
+ }
+}
+
+function getEndpointPackagePolicyBatch(
+ soClient: SavedObjectsClientContract,
+ packagePolicyService: PackagePolicyClient,
+ page: number,
+ perPage: number
+): Promise> {
+ return packagePolicyService.list(soClient, {
+ page,
+ perPage,
+ kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:${FLEET_ENDPOINT_PACKAGE}`,
+ });
+}
+
+async function processBatch(
+ endpointPackagesResult: ListResult,
+ soClient: SavedObjectsClientContract,
+ esClient: ElasticsearchClient,
+ packagePolicyService: PackagePolicyClient
+): Promise {
+ if (!endpointPackagesResult.total) {
+ return;
+ }
+
+ const updatedEndpointPackages = endpointPackagesResult.items.map((endpointPackage) => ({
+ ...endpointPackage,
+ inputs: endpointPackage.inputs.map((input) => {
+ const config = input?.config || {};
+ const policy = config.policy || {};
+ const policyValue = policy?.value || {};
+ const meta = policyValue?.meta || {};
+ return {
+ ...input,
+ config: {
+ ...config,
+ policy: {
+ ...policy,
+ value: {
+ ...policyValue,
+ meta: {
+ ...meta,
+ serverless: true,
+ },
+ },
+ },
+ },
+ };
+ }),
+ }));
+ await packagePolicyService.bulkUpdate(soClient, esClient, updatedEndpointPackages);
+}
diff --git a/x-pack/plugins/security_solution_serverless/server/plugin.ts b/x-pack/plugins/security_solution_serverless/server/plugin.ts
index 4ecf5196cbd61..cef36b0dab9db 100644
--- a/x-pack/plugins/security_solution_serverless/server/plugin.ts
+++ b/x-pack/plugins/security_solution_serverless/server/plugin.ts
@@ -12,9 +12,8 @@ import type {
CoreStart,
Logger,
} from '@kbn/core/server';
+
import { getProductAppFeatures } from '../common/pli/pli_features';
-import { METERING_TASK as ENDPOINT_METERING_TASK } from './endpoint/constants/metering';
-import { endpointMeteringService } from './endpoint/services';
import type { ServerlessSecurityConfig } from './config';
import type {
@@ -25,6 +24,11 @@ import type {
} from './types';
import { SecurityUsageReportingTask } from './task_manager/usage_reporting_task';
import { cloudSecurityMetringTaskProperties } from './cloud_security/cloud_security_metering_task_config';
+import { METERING_TASK as ENDPOINT_METERING_TASK } from './endpoint/constants/metering';
+import {
+ endpointMeteringService,
+ setEndpointPackagePolicyServerlessFlag,
+} from './endpoint/services';
export class SecuritySolutionServerlessPlugin
implements
@@ -92,6 +96,9 @@ export class SecuritySolutionServerlessPlugin
}
public start(_coreStart: CoreStart, pluginsSetup: SecuritySolutionServerlessPluginStartDeps) {
+ const internalESClient = _coreStart.elasticsearch.client.asInternalUser;
+ const internalSOClient = _coreStart.savedObjects.createInternalRepository();
+
this.cspmUsageReportingTask?.start({
taskManager: pluginsSetup.taskManager,
interval: cloudSecurityMetringTaskProperties.interval,
@@ -101,6 +108,12 @@ export class SecuritySolutionServerlessPlugin
taskManager: pluginsSetup.taskManager,
interval: ENDPOINT_METERING_TASK.INTERVAL,
});
+
+ setEndpointPackagePolicyServerlessFlag(
+ internalSOClient,
+ internalESClient,
+ pluginsSetup.fleet.packagePolicyService
+ );
return {};
}
diff --git a/x-pack/plugins/security_solution_serverless/server/types.ts b/x-pack/plugins/security_solution_serverless/server/types.ts
index 91b1750e62c81..5e6ab2fbb5a1b 100644
--- a/x-pack/plugins/security_solution_serverless/server/types.ts
+++ b/x-pack/plugins/security_solution_serverless/server/types.ts
@@ -18,6 +18,7 @@ import type {
import type { CloudSetup } from '@kbn/cloud-plugin/server';
import type { SecuritySolutionEssPluginSetup } from '@kbn/security-solution-ess/server';
import type { MlPluginSetup } from '@kbn/ml-plugin/server';
+import type { FleetStartContract } from '@kbn/fleet-plugin/server';
import type { ProductTier } from '../common/product';
@@ -43,6 +44,7 @@ export interface SecuritySolutionServerlessPluginStartDeps {
securitySolution: SecuritySolutionPluginStart;
features: PluginStartContract;
taskManager: TaskManagerStartContract;
+ fleet: FleetStartContract;
}
export interface UsageRecord {
diff --git a/x-pack/plugins/security_solution_serverless/tsconfig.json b/x-pack/plugins/security_solution_serverless/tsconfig.json
index b69dbcb5b189a..f8e322f580837 100644
--- a/x-pack/plugins/security_solution_serverless/tsconfig.json
+++ b/x-pack/plugins/security_solution_serverless/tsconfig.json
@@ -35,6 +35,7 @@
"@kbn/kibana-utils-plugin",
"@kbn/task-manager-plugin",
"@kbn/cloud-plugin",
- "@kbn/cloud-security-posture-plugin"
+ "@kbn/cloud-security-posture-plugin",
+ "@kbn/fleet-plugin"
]
}
From 3a3af2dd69ae4b09307932f9a8d073f8d7137965 Mon Sep 17 00:00:00 2001
From: Stratoula Kalafateli
Date: Mon, 14 Aug 2023 19:53:01 +0300
Subject: [PATCH 17/26] [Serverless] Not allow link to editor for readonly
visualizations (#163812)
## Summary
Removes the link from the listing page for readonly visualizations
I added this case to our serverless tests
https://github.com/elastic/kibana/issues/162346 as it is a regression
(it used to work ok).
---
.../public/visualize_app/components/visualize_listing.tsx | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/src/plugins/visualizations/public/visualize_app/components/visualize_listing.tsx b/src/plugins/visualizations/public/visualize_app/components/visualize_listing.tsx
index 5c31c08f46853..30bb05c7b43d6 100644
--- a/src/plugins/visualizations/public/visualize_app/components/visualize_listing.tsx
+++ b/src/plugins/visualizations/public/visualize_app/components/visualize_listing.tsx
@@ -378,8 +378,10 @@ export const VisualizeListing = () => {
entityNamePlural={i18n.translate('visualizations.listing.table.entityNamePlural', {
defaultMessage: 'visualizations',
})}
- getDetailViewLink={({ attributes: { editApp, editUrl, error } }) =>
- getVisualizeListItemLink(application, kbnUrlStateStorage, editApp, editUrl, error)
+ getDetailViewLink={({ attributes: { editApp, editUrl, error, readOnly } }) =>
+ readOnly
+ ? undefined
+ : getVisualizeListItemLink(application, kbnUrlStateStorage, editApp, editUrl, error)
}
tableCaption={visualizeLibraryTitle}
{...tableViewProps}
From 41d8296db7a1d6548f11403b73797d34472c4b39 Mon Sep 17 00:00:00 2001
From: Jon
Date: Mon, 14 Aug 2023 12:08:27 -0500
Subject: [PATCH 18/26] Upgrade Node.js to 18.17.1 (#163710)
https://github.com/nodejs/node/blob/main/doc/changelogs/CHANGELOG_V18.md#18.17.1
---
.ci/Dockerfile | 2 +-
.node-version | 2 +-
.nvmrc | 2 +-
WORKSPACE.bazel | 12 ++++++------
docs/developer/advanced/upgrading-nodejs.asciidoc | 2 +-
package.json | 2 +-
6 files changed, 11 insertions(+), 11 deletions(-)
diff --git a/.ci/Dockerfile b/.ci/Dockerfile
index 3165805fe68c1..bf84d0a78d581 100644
--- a/.ci/Dockerfile
+++ b/.ci/Dockerfile
@@ -1,7 +1,7 @@
# NOTE: This Dockerfile is ONLY used to run certain tasks in CI. It is not used to run Kibana or as a distributable.
# If you're looking for the Kibana Docker image distributable, please see: src/dev/build/tasks/os_packages/docker_generator/templates/dockerfile.template.ts
-ARG NODE_VERSION=18.17.0
+ARG NODE_VERSION=18.17.1
FROM node:${NODE_VERSION} AS base
diff --git a/.node-version b/.node-version
index 603606bc91118..4a1f488b6c3b6 100644
--- a/.node-version
+++ b/.node-version
@@ -1 +1 @@
-18.17.0
+18.17.1
diff --git a/.nvmrc b/.nvmrc
index 603606bc91118..4a1f488b6c3b6 100644
--- a/.nvmrc
+++ b/.nvmrc
@@ -1 +1 @@
-18.17.0
+18.17.1
diff --git a/WORKSPACE.bazel b/WORKSPACE.bazel
index baec139453143..dd4c41818949c 100644
--- a/WORKSPACE.bazel
+++ b/WORKSPACE.bazel
@@ -22,13 +22,13 @@ load("@build_bazel_rules_nodejs//:index.bzl", "node_repositories", "yarn_install
# Setup the Node.js toolchain for the architectures we want to support
node_repositories(
node_repositories = {
- "18.17.0-darwin_amd64": ("node-v18.17.0-darwin-x64.tar.gz", "node-v18.17.0-darwin-x64", "2f381442381f7fbde2ca644c3275bec9c9c2a8d361f467b40e39428acdd6ccff"),
- "18.17.0-darwin_arm64": ("node-v18.17.0-darwin-arm64.tar.gz", "node-v18.17.0-darwin-arm64", "19731ef427e77ad9c5f476eb62bfb02a7f179d3012feed0bbded62e45f23e679"),
- "18.17.0-linux_arm64": ("node-v18.17.0-linux-arm64.tar.xz", "node-v18.17.0-linux-arm64", "fbd2904178ee47da6e0386bc9704a12b1f613da6ad194878a517d4a69ba56544"),
- "18.17.0-linux_amd64": ("node-v18.17.0-linux-x64.tar.xz", "node-v18.17.0-linux-x64", "f36facda28c4d5ce76b3a1b4344e688d29d9254943a47f2f1909b1a10acb1959"),
- "18.17.0-windows_amd64": ("node-v18.17.0-win-x64.zip", "node-v18.17.0-win-x64", "06e30b4e70b18d794651ef132c39080e5eaaa1187f938721d57edae2824f4e96"),
+ "18.17.1-darwin_amd64": ("node-v18.17.1-darwin-x64.tar.gz", "node-v18.17.1-darwin-x64", "b3e083d2715f07ec3f00438401fb58faa1e0bdf3c7bde9f38b75ed17809d92fa"),
+ "18.17.1-darwin_arm64": ("node-v18.17.1-darwin-arm64.tar.gz", "node-v18.17.1-darwin-arm64", "18ca716ea57522b90473777cb9f878467f77fdf826d37beb15a0889fdd74533e"),
+ "18.17.1-linux_arm64": ("node-v18.17.1-linux-arm64.tar.xz", "node-v18.17.1-linux-arm64", "3f933716a468524acb68c2514d819b532131eb50399ee946954d4a511303e1bb"),
+ "18.17.1-linux_amd64": ("node-v18.17.1-linux-x64.tar.xz", "node-v18.17.1-linux-x64", "07e76408ddb0300a6f46fcc9abc61f841acde49b45020ec4e86bb9b25df4dced"),
+ "18.17.1-windows_amd64": ("node-v18.17.1-win-x64.zip", "node-v18.17.1-win-x64", "afc83f5cf6e8b45a4d3fb842904f604dcd271fefada31ad6654f8302f8da28c9"),
},
- node_version = "18.17.0",
+ node_version = "18.17.1",
node_urls = [
"https://nodejs.org/dist/v{version}/{filename}",
],
diff --git a/docs/developer/advanced/upgrading-nodejs.asciidoc b/docs/developer/advanced/upgrading-nodejs.asciidoc
index 3f27d5a62147d..9587dfbfd14a0 100644
--- a/docs/developer/advanced/upgrading-nodejs.asciidoc
+++ b/docs/developer/advanced/upgrading-nodejs.asciidoc
@@ -17,7 +17,7 @@ These files must be updated when upgrading Node.js:
- {kib-repo}blob/{branch}/WORKSPACE.bazel[`WORKSPACE.bazel`] - The version is specified in the `node_version` property.
Besides this property, the list of files under `node_repositories` must be updated along with their respective SHA256 hashes.
These can be found on the https://nodejs.org[nodejs.org] website.
- Example for Node.js v18.17.0: https://nodejs.org/dist/v18.17.0/SHASUMS256.txt.asc
+ Example for Node.js v18.17.1: https://nodejs.org/dist/v18.17.1/SHASUMS256.txt.asc
See PR {kib-repo}pull/128123[#128123] for an example of how the Node.js version has been upgraded previously.
diff --git a/package.json b/package.json
index 16f0ec705f349..4cc7f23974933 100644
--- a/package.json
+++ b/package.json
@@ -73,7 +73,7 @@
"url": "https://github.com/elastic/kibana.git"
},
"engines": {
- "node": "18.17.0",
+ "node": "18.17.1",
"yarn": "^1.22.19"
},
"resolutions": {
From 0aaf842353ec8607abe8401128235dd62bb3e1eb Mon Sep 17 00:00:00 2001
From: Marius Dragomir
Date: Mon, 14 Aug 2023 19:19:45 +0200
Subject: [PATCH 19/26] [QA] Change "contains" text for Dev Console CCS test
(#163814)
## Summary
This is a fix for the flaky test
https://github.com/elastic/kibana/issues/163365 that checks Dev Console
with CCS. Before we were looking for the response to the query to
contain `extension: jpg` which might or might not show up in the
viewport based on the logstash data ingested (some of the documents have
a multi-line `message` field that will scroll everything else out of the
viewport).
With the change we're looking for the `_index` metafield which can also
confirm that the search worked and it returned results from a remote
cluster. There shouldn't be any instance of it not showing up in the
viewport since it's one of the first fields in a search hit.
---
test/functional/apps/console/_console_ccs.ts | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/test/functional/apps/console/_console_ccs.ts b/test/functional/apps/console/_console_ccs.ts
index 486223f02d320..8778c2e6e70bb 100644
--- a/test/functional/apps/console/_console_ccs.ts
+++ b/test/functional/apps/console/_console_ccs.ts
@@ -32,8 +32,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await remoteEsArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional');
});
- // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/163365
- describe.skip('Perform CCS Search in Console', () => {
+ describe('Perform CCS Search in Console', () => {
before(async () => {
await PageObjects.console.clearTextArea();
});
@@ -44,7 +43,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.console.clickPlay();
await retry.try(async () => {
const actualResponse = await PageObjects.console.getResponse();
- expect(actualResponse).to.contain('"extension": "jpg",');
+ expect(actualResponse).to.contain('"_index": "ftr-remote:logstash-2015.09.20"');
});
});
});
From 6c291d4290671f64cf1e1b0ce21ae1da508e1237 Mon Sep 17 00:00:00 2001
From: Tim Sullivan
Date: Mon, 14 Aug 2023 10:21:50 -0700
Subject: [PATCH 20/26] [Reporting] API test maintenance (#163665)
## Summary
Addresses https://github.com/elastic/kibana/issues/160573 by assuming
the sizes of the test archives were overly large and possibly created
instability between tests, which led to `Error: socket hang up`.
The change in this PR is to shrink the contents ES archives used for
testing, by removing the report content and encrypted headers, which
aren't important for the test. The tests that need these archives simply
use the metadata to ensure telemetry usage collection is working.
||Before|After|
|---|---|---|
| x-pack/test/functional/es_archives/reporting/bwc/6_2/data.json.gz |
3.2M | 4.0K |
| x-pack/test/functional/es_archives/reporting/bwc/6_3/data.json.gz |
6.7M | 6.0K |
Since this addresses a failed test rather than a flaky test, it's going
off the assumption that this change is sufficient to close the issue.
Closes https://github.com/elastic/kibana/issues/160573
---
.../reporting/bwc/6_2/data.json.gz | Bin 3327908 -> 4095 bytes
.../reporting/bwc/6_3/data.json.gz | Bin 6984158 -> 6147 bytes
2 files changed, 0 insertions(+), 0 deletions(-)
diff --git a/x-pack/test/functional/es_archives/reporting/bwc/6_2/data.json.gz b/x-pack/test/functional/es_archives/reporting/bwc/6_2/data.json.gz
index 85949bd32006e07dd49954ee22e492efaa384e27..18a22390afd9b1ddd682eb53578ec5be53455b46 100644
GIT binary patch
literal 4095
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
z