= ({ enabled, phase }) => {
+ const { euiTheme } = useEuiTheme();
+
+ const isBorealis = euiTheme.themeName === 'EUI_THEME_BOREALIS';
+
+ const phaseIconColors = {
+ hot: isBorealis ? euiTheme.colors.vis.euiColorVis6 : euiThemeVars.euiColorVis9_behindText,
+ warm: isBorealis ? euiTheme.colors.vis.euiColorVis9 : euiThemeVars.euiColorVis5_behindText,
+ cold: isBorealis ? euiTheme.colors.vis.euiColorVis2 : euiThemeVars.euiColorVis1_behindText,
+ frozen: euiTheme.colors.vis.euiColorVis4,
+ delete: euiTheme.colors.darkShade,
+ };
+
return (
{enabled ? (
diff --git a/x-pack/platform/plugins/private/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.scss b/x-pack/platform/plugins/private/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.scss
index a69f244a25aea..7fd59bebea9e6 100644
--- a/x-pack/platform/plugins/private/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.scss
+++ b/x-pack/platform/plugins/private/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.scss
@@ -60,33 +60,17 @@ $ilmTimelineBarHeight: $euiSizeS;
&__hotPhase {
width: var(--ilm-timeline-hot-phase-width);
-
- &__colorBar {
- background-color: $euiColorVis9;
- }
}
&__warmPhase {
width: var(--ilm-timeline-warm-phase-width);
-
- &__colorBar {
- background-color: $euiColorVis5;
- }
}
&__coldPhase {
width: var(--ilm-timeline-cold-phase-width);
-
- &__colorBar {
- background-color: $euiColorVis1;
- }
}
&__frozenPhase {
width: var(--ilm-timeline-frozen-phase-width);
-
- &__colorBar {
- background-color: $euiColorVis4;
- }
}
}
diff --git a/x-pack/platform/plugins/private/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.tsx b/x-pack/platform/plugins/private/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.tsx
index b2ee9c2520831..8cdfdf5bc9bf3 100644
--- a/x-pack/platform/plugins/private/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.tsx
+++ b/x-pack/platform/plugins/private/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.tsx
@@ -5,11 +5,21 @@
* 2.0.
*/
+/** @jsx jsx */
+// Needed for for testing out the css prop feature. See: https://emotion.sh/docs/css-prop#jsx-pragma
+import { css, jsx } from '@emotion/react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import React, { FunctionComponent, memo } from 'react';
-import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiText, EuiIconTip } from '@elastic/eui';
+import {
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiTitle,
+ EuiText,
+ EuiIconTip,
+ useEuiTheme,
+} from '@elastic/eui';
import { useKibana } from '../../../../../shared_imports';
@@ -135,6 +145,17 @@ export const Timeline: FunctionComponent
= memo(
: undefined,
};
+ const { euiTheme } = useEuiTheme();
+
+ const isBorealis = euiTheme.themeName === 'EUI_THEME_BOREALIS';
+
+ const timelineIconColors = {
+ hot: isBorealis ? euiTheme.colors.vis.euiColorVis6 : euiTheme.colors.vis.euiColorVis9,
+ warm: isBorealis ? euiTheme.colors.vis.euiColorVis9 : euiTheme.colors.vis.euiColorVis5,
+ cold: isBorealis ? euiTheme.colors.vis.euiColorVis2 : euiTheme.colors.vis.euiColorVis1,
+ frozen: euiTheme.colors.vis.euiColorVis4,
+ };
+
const phaseAgeInMilliseconds = calculateRelativeFromAbsoluteMilliseconds(absoluteTimings);
const widths = calculateWidths(phaseAgeInMilliseconds);
@@ -188,7 +209,12 @@ export const Timeline: FunctionComponent = memo(
data-test-subj="ilmTimelinePhase-hot"
className="ilmTimeline__phasesContainer__phase ilmTimeline__hotPhase"
>
-
+
= memo(
data-test-subj="ilmTimelinePhase-warm"
className="ilmTimeline__phasesContainer__phase ilmTimeline__warmPhase"
>
-
+
= memo(
data-test-subj="ilmTimelinePhase-cold"
className="ilmTimeline__phasesContainer__phase ilmTimeline__coldPhase"
>
-
+
= memo(
data-test-subj="ilmTimelinePhase-frozen"
className="ilmTimeline__phasesContainer__phase ilmTimeline__frozenPhase"
>
-
+
{
+ const { euiTheme } = useEuiTheme();
+
+ const isBorealis = euiTheme.themeName === 'EUI_THEME_BOREALIS';
+
+ // Changing the mappings for the phases in Borealis as a mid-term solution. See https://github.com/elastic/kibana/issues/203664#issuecomment-2536593361.
+ const phaseToIndicatorColors = {
+ hot: isBorealis ? euiTheme.colors.vis.euiColorVis6 : euiTheme.colors.vis.euiColorVis9,
+ warm: isBorealis ? euiTheme.colors.vis.euiColorVis9 : euiTheme.colors.vis.euiColorVis5,
+ cold: isBorealis ? euiTheme.colors.vis.euiColorVis2 : euiTheme.colors.vis.euiColorVis1,
+ frozen: euiTheme.colors.vis.euiColorVis4,
+ delete: euiTheme.colors.lightShade,
+ };
+
return (
= {
- hot: {
- color: euiThemeVars.euiColorVis9,
- label: 'Hot',
- },
- warm: {
- color: euiThemeVars.euiColorVis5,
- label: 'Warm',
- },
- cold: {
- color: euiThemeVars.euiColorVis1,
- label: 'Cold',
- },
- frozen: {
- color: euiThemeVars.euiColorVis4,
- label: 'Frozen',
- },
- delete: {
- color: 'default',
- label: 'Delete',
- },
-};
-
interface Props {
index: Index;
getUrlForApp: ApplicationStart['getUrlForApp'];
@@ -64,6 +40,34 @@ export const IndexLifecycleSummary: FunctionComponent
= ({ index, getUrlF
// only ILM managed indices render the ILM tab
const ilm = ilmData as IlmExplainLifecycleLifecycleExplainManaged;
+ const { euiTheme } = useEuiTheme();
+
+ const isBorealis = euiTheme.themeName === 'EUI_THEME_BOREALIS';
+
+ // Changing the mappings for the phases in Borealis as a mid-term solution. See https://github.com/elastic/kibana/issues/203664#issuecomment-2536593361.
+ const phaseToBadgeMapping: Record = {
+ hot: {
+ color: isBorealis ? euiTheme.colors.vis.euiColorVis6 : euiTheme.colors.vis.euiColorVis9,
+ label: 'Hot',
+ },
+ warm: {
+ color: isBorealis ? euiTheme.colors.vis.euiColorVis9 : euiTheme.colors.vis.euiColorVis5,
+ label: 'Warm',
+ },
+ cold: {
+ color: isBorealis ? euiTheme.colors.vis.euiColorVis2 : euiTheme.colors.vis.euiColorVis1,
+ label: 'Cold',
+ },
+ frozen: {
+ color: euiTheme.colors.vis.euiColorVis4,
+ label: 'Frozen',
+ },
+ delete: {
+ color: 'default',
+ label: 'Delete',
+ },
+ };
+
// if ilm.phase is an unexpected value, then display a default badge
const phaseBadgeConfig = phaseToBadgeMapping[ilm.phase as Phase] ?? {
color: 'default',
diff --git a/x-pack/platform/plugins/private/index_lifecycle_management/tsconfig.json b/x-pack/platform/plugins/private/index_lifecycle_management/tsconfig.json
index 5f061478f6a5d..34f3e7a95d191 100644
--- a/x-pack/platform/plugins/private/index_lifecycle_management/tsconfig.json
+++ b/x-pack/platform/plugins/private/index_lifecycle_management/tsconfig.json
@@ -35,13 +35,13 @@
"@kbn/core-http-browser",
"@kbn/config-schema",
"@kbn/shared-ux-router",
- "@kbn/ui-theme",
"@kbn/shared-ux-link-redirect-app",
"@kbn/index-management-shared-types",
"@kbn/react-kibana-context-render",
"@kbn/unsaved-changes-prompt",
"@kbn/shared-ux-table-persist",
"@kbn/index-lifecycle-management-common-shared",
+ "@kbn/ui-theme",
],
"exclude": [
"target/**/*",
diff --git a/x-pack/platform/plugins/shared/entity_manager/public/lib/entity_client.test.ts b/x-pack/platform/plugins/shared/entity_manager/public/lib/entity_client.test.ts
index 33363dea05e76..68d409abbbd9c 100644
--- a/x-pack/platform/plugins/shared/entity_manager/public/lib/entity_client.test.ts
+++ b/x-pack/platform/plugins/shared/entity_manager/public/lib/entity_client.test.ts
@@ -18,6 +18,8 @@ const commonEntityFields: EntityInstance = {
} as EntityInstance['entity'],
};
+type Entity = { [key: string]: any } & { entityIdentityFields: { [key: string]: string[] } };
+
describe('EntityClient', () => {
let entityClient: EntityClient;
@@ -27,127 +29,103 @@ describe('EntityClient', () => {
describe('asKqlFilter', () => {
it('should return the kql filter', () => {
- const entityLatest: EntityInstance = {
- entity: {
- ...commonEntityFields.entity,
- identity_fields: ['service.name', 'service.environment'],
- type: 'service',
- },
- service: {
- name: 'my-service',
- },
+ const entity: Entity = {
+ ...commonEntityFields.entity,
+ entityIdentityFields: { source1: ['service.name', 'service.environment'] },
+ type: 'service',
+ ['service.name']: 'my-service',
};
- const result = entityClient.asKqlFilter(entityLatest);
+ const result = entityClient.asKqlFilter({ entity });
expect(result).toEqual('service.name: "my-service"');
});
- it('should return the kql filter when an indentity field value contain special characters', () => {
- const entityLatest: EntityInstance = {
- entity: {
- ...commonEntityFields.entity,
- identity_fields: ['host.name', 'foo.bar'],
- },
- host: {
- name: 'my-host:some-value:some-other-value',
- },
+ it('should return the kql filter when an identity field value contain special characters', () => {
+ const entity: Entity = {
+ ...commonEntityFields.entity,
+ entityIdentityFields: { source1: ['host.name', 'foo.bar'] },
+ type: 'service',
+ ['host.name']: 'my-host:some-value:some-other-value',
};
- const result = entityClient.asKqlFilter(entityLatest);
+ const result = entityClient.asKqlFilter({ entity });
expect(result).toEqual('host.name: "my-host:some-value:some-other-value"');
});
- it('should return the kql filter when indentity_fields is composed by multiple fields', () => {
- const entityLatest: EntityInstance = {
- entity: {
- ...commonEntityFields.entity,
- identity_fields: ['service.name', 'service.environment'],
- type: 'service',
- },
- service: {
- name: 'my-service',
- environment: 'staging',
- },
+ it('should return the kql filter when identity_fields is composed by multiple fields', () => {
+ const entity: Entity = {
+ ...commonEntityFields.entity,
+ entityIdentityFields: { source1: ['service.name', 'service.environment'] },
+ type: 'service',
+ ['service.name']: 'my-service',
+ ['service.environment']: 'staging',
};
- const result = entityClient.asKqlFilter(entityLatest);
+ const result = entityClient.asKqlFilter({ entity });
expect(result).toEqual('(service.name: "my-service" AND service.environment: "staging")');
});
it('should ignore fields that are not present in the entity', () => {
- const entityLatest: EntityInstance = {
- entity: {
- ...commonEntityFields.entity,
- identity_fields: ['host.name', 'foo.bar'],
- },
- host: {
- name: 'my-host',
- },
+ const entity: Entity = {
+ ...commonEntityFields.entity,
+ entityIdentityFields: { source1: ['host.name', 'foo.bar'] },
+ ['host.name']: 'my-host',
};
- const result = entityClient.asKqlFilter(entityLatest);
+ const result = entityClient.asKqlFilter({ entity });
expect(result).toEqual('host.name: "my-host"');
});
});
describe('getIdentityFieldsValue', () => {
it('should return identity fields values', () => {
- const entityLatest: EntityInstance = {
- entity: {
- ...commonEntityFields.entity,
- identity_fields: ['service.name', 'service.environment'],
- type: 'service',
- },
- service: {
- name: 'my-service',
- },
+ const entity: Entity = {
+ ...commonEntityFields.entity,
+ entityIdentityFields: { source1: ['service.name', 'service.environment'] },
+ type: 'service',
+ ['service.name']: 'my-service',
};
- expect(entityClient.getIdentityFieldsValue(entityLatest)).toEqual({
+ expect(entityClient.getIdentityFieldsValue({ entity })).toEqual({
'service.name': 'my-service',
});
});
- it('should return identity fields values when indentity_fields is composed by multiple fields', () => {
- const entityLatest: EntityInstance = {
- entity: {
- ...commonEntityFields.entity,
- identity_fields: ['service.name', 'service.environment'],
- type: 'service',
- },
- service: {
- name: 'my-service',
- environment: 'staging',
- },
+ it('should return identity fields values when identity_fields is composed by multiple fields', () => {
+ const entity: Entity = {
+ ...commonEntityFields.entity,
+ entityIdentityFields: { source1: ['service.name', 'service.environment'] },
+ type: 'service',
+ ['service.name']: 'my-service',
+ ['service.environment']: 'staging',
};
- expect(entityClient.getIdentityFieldsValue(entityLatest)).toEqual({
+ expect(entityClient.getIdentityFieldsValue({ entity })).toEqual({
'service.name': 'my-service',
'service.environment': 'staging',
});
});
it('should return identity fields when field is in the root', () => {
- const entityLatest: EntityInstance = {
- entity: {
- ...commonEntityFields.entity,
- identity_fields: ['name'],
- type: 'service',
- },
+ const entity: Entity = {
+ ...commonEntityFields.entity,
+ entityIdentityFields: { source1: ['name'] },
+ type: 'service',
name: 'foo',
};
- expect(entityClient.getIdentityFieldsValue(entityLatest)).toEqual({
+ expect(entityClient.getIdentityFieldsValue({ entity })).toEqual({
name: 'foo',
});
});
it('should throw an error when identity fields are missing', () => {
- const entityLatest: EntityInstance = {
- ...commonEntityFields,
+ const entity: Entity = {
+ ...commonEntityFields.entity,
+ entityIdentityFields: {},
};
- expect(() => entityClient.getIdentityFieldsValue(entityLatest)).toThrow(
+ expect(() => entityClient.getIdentityFieldsValue({ entity })).toThrow(
'Identity fields are missing'
);
});
diff --git a/x-pack/platform/plugins/shared/entity_manager/public/lib/entity_client.ts b/x-pack/platform/plugins/shared/entity_manager/public/lib/entity_client.ts
index 43530b27df7f7..bff8521a8b33d 100644
--- a/x-pack/platform/plugins/shared/entity_manager/public/lib/entity_client.ts
+++ b/x-pack/platform/plugins/shared/entity_manager/public/lib/entity_client.ts
@@ -13,8 +13,7 @@ import {
isHttpFetchError,
} from '@kbn/server-route-repository-client';
import { type KueryNode, nodeTypes, toKqlExpression } from '@kbn/es-query';
-import type { EntityDefinition, EntityInstance, EntityMetadata } from '@kbn/entities-schema';
-import { castArray } from 'lodash';
+import type { EntityDefinition } from '@kbn/entities-schema';
import type { EntityDefinitionWithState } from '../../server/lib/entities/types';
import {
DisableManagedEntityResponse,
@@ -106,12 +105,12 @@ export class EntityClient {
}
}
- asKqlFilter(
- entityInstance: {
- entity: Pick;
- } & Required
- ) {
- const identityFieldsValue = this.getIdentityFieldsValue(entityInstance);
+ asKqlFilter({
+ entity,
+ }: {
+ entity: { [key: string]: any } & { entityIdentityFields: { [key: string]: string[] } };
+ }) {
+ const identityFieldsValue = this.getIdentityFieldsValue({ entity });
const nodes: KueryNode[] = Object.entries(identityFieldsValue).map(([identityField, value]) => {
return nodeTypes.function.buildNode('is', identityField, `"${value}"`);
@@ -124,26 +123,22 @@ export class EntityClient {
return toKqlExpression(kqlExpression);
}
- getIdentityFieldsValue(
- entityInstance: {
- entity: Pick;
- } & Required
- ) {
- const { identity_fields: identityFields } = entityInstance.entity;
-
- if (!identityFields) {
+ getIdentityFieldsValue({
+ entity,
+ }: {
+ entity: { [key: string]: any } & { entityIdentityFields: { [key: string]: string[] } };
+ }): Record {
+ const { entityIdentityFields: identityFields } = entity;
+ if (!Object.keys(identityFields || {}).length) {
throw new Error('Identity fields are missing');
}
- return castArray(identityFields).reduce((acc, field) => {
- const value = field.split('.').reduce((obj: any, part: string) => {
- return obj && typeof obj === 'object' ? (obj as Record)[part] : undefined;
- }, entityInstance);
-
- if (value) {
- acc[field] = value;
- }
-
+ return Object.values(identityFields).reduce((acc: Record, fields) => {
+ fields.forEach((field) => {
+ if (entity?.[field]) {
+ acc[field] = entity[field];
+ }
+ });
return acc;
}, {} as Record);
}
diff --git a/x-pack/platform/plugins/shared/entity_manager/server/lib/v2/definitions/identity_fields_by_source.test.ts b/x-pack/platform/plugins/shared/entity_manager/server/lib/v2/definitions/identity_fields_by_source.test.ts
new file mode 100644
index 0000000000000..e377230b38671
--- /dev/null
+++ b/x-pack/platform/plugins/shared/entity_manager/server/lib/v2/definitions/identity_fields_by_source.test.ts
@@ -0,0 +1,102 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { EntityV2 } from '@kbn/entities-schema';
+import { readSourceDefinitions } from './source_definition';
+import { loggerMock } from '@kbn/logging-mocks';
+import { elasticsearchServiceMock } from '@kbn/core-elasticsearch-server-mocks';
+import type { EntitySourceDefinition } from '../types';
+import { UnknownEntityType } from '../errors/unknown_entity_type';
+import { identityFieldsBySource } from './identity_fields_by_source';
+
+const readSourceDefinitionsMock = readSourceDefinitions as jest.Mock;
+jest.mock('./source_definition', () => ({
+ readSourceDefinitions: jest.fn(),
+}));
+const esClientMock = elasticsearchServiceMock.createClusterClient();
+const logger = loggerMock.create();
+
+describe('identityFieldsBySource', () => {
+ it('throws if no sources are found for the type', async () => {
+ const instance: EntityV2 = {
+ 'entity.type': 'my_type',
+ 'entity.id': 'whatever',
+ 'entity.display_name': 'Whatever',
+ };
+
+ const sources: EntitySourceDefinition[] = [];
+ readSourceDefinitionsMock.mockResolvedValue(sources);
+
+ await expect(
+ identityFieldsBySource(instance['entity.type'], esClientMock, logger)
+ ).rejects.toThrowError(UnknownEntityType);
+ });
+
+ it('returns the correct identity fields with a single source', async () => {
+ const instance: EntityV2 = {
+ 'entity.type': 'my_type',
+ 'entity.id': 'whatever',
+ 'entity.display_name': 'Whatever',
+ 'host.name': 'my_host',
+ };
+
+ const sources: EntitySourceDefinition[] = [
+ {
+ id: 'my_source',
+ type_id: 'my_type',
+ identity_fields: ['host.name'],
+ index_patterns: [],
+ metadata_fields: [],
+ filters: [],
+ },
+ ];
+ readSourceDefinitionsMock.mockResolvedValue(sources);
+
+ await expect(
+ identityFieldsBySource(instance['entity.type'], esClientMock, logger)
+ ).resolves.toEqual({
+ my_source: ['host.name'],
+ });
+ });
+
+ it('returns the correct identity fields with multiple sources', async () => {
+ const instance: EntityV2 = {
+ 'entity.type': 'my_type',
+ 'entity.id': 'whatever',
+ 'entity.display_name': 'Whatever',
+ 'host.name': 'my_host',
+ 'host.os': 'my_os',
+ };
+
+ const sources: EntitySourceDefinition[] = [
+ {
+ id: 'my_source_host',
+ type_id: 'my_type',
+ identity_fields: ['host.name'],
+ index_patterns: [],
+ metadata_fields: [],
+ filters: [],
+ },
+ {
+ id: 'my_source_os',
+ type_id: 'my_type',
+ identity_fields: ['host.os'],
+ index_patterns: [],
+ metadata_fields: [],
+ filters: [],
+ },
+ ];
+ readSourceDefinitionsMock.mockResolvedValue(sources);
+
+ await expect(
+ identityFieldsBySource(instance['entity.type'], esClientMock, logger)
+ ).resolves.toEqual({
+ my_source_host: ['host.name'],
+ my_source_os: ['host.os'],
+ });
+ });
+});
diff --git a/x-pack/platform/plugins/shared/entity_manager/server/lib/v2/definitions/identity_fields_by_source.ts b/x-pack/platform/plugins/shared/entity_manager/server/lib/v2/definitions/identity_fields_by_source.ts
new file mode 100644
index 0000000000000..786e65b0f1717
--- /dev/null
+++ b/x-pack/platform/plugins/shared/entity_manager/server/lib/v2/definitions/identity_fields_by_source.ts
@@ -0,0 +1,36 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { EntityV2 } from '@kbn/entities-schema';
+import { Logger } from '@kbn/core/server';
+import { readSourceDefinitions } from './source_definition';
+import { InternalClusterClient } from '../types';
+import { UnknownEntityType } from '../errors/unknown_entity_type';
+
+export async function identityFieldsBySource(
+ type: EntityV2['entity.type'],
+ clusterClient: InternalClusterClient,
+ logger: Logger
+) {
+ const sources = await readSourceDefinitions(clusterClient, logger, {
+ type,
+ });
+
+ if (sources.length === 0) {
+ throw new UnknownEntityType(`No sources found for type ${type}`);
+ }
+
+ const identityFields: { [key: string]: string[] } = {};
+
+ sources.forEach((source) => {
+ const { id, identity_fields: fields } = source;
+
+ identityFields[id] = fields;
+ });
+
+ return identityFields;
+}
diff --git a/x-pack/platform/plugins/shared/entity_manager/server/plugin.ts b/x-pack/platform/plugins/shared/entity_manager/server/plugin.ts
index 0d1463dde1318..8e19aa8821fc7 100644
--- a/x-pack/platform/plugins/shared/entity_manager/server/plugin.ts
+++ b/x-pack/platform/plugins/shared/entity_manager/server/plugin.ts
@@ -40,6 +40,7 @@ import { installBuiltInDefinitions } from './lib/v2/definitions/install_built_in
import { disableManagedEntityDiscovery } from './lib/entities/uninstall_entity_definition';
import { installEntityManagerTemplates } from './lib/manage_index_templates';
import { instanceAsFilter } from './lib/v2/definitions/instance_as_filter';
+import { identityFieldsBySource } from './lib/v2/definitions/identity_fields_by_source';
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface EntityManagerServerPluginSetup {}
@@ -47,6 +48,7 @@ export interface EntityManagerServerPluginStart {
getScopedClient: (options: { request: KibanaRequest }) => Promise;
v2: {
instanceAsFilter: typeof instanceAsFilter;
+ identityFieldsBySource: typeof identityFieldsBySource;
};
}
@@ -194,6 +196,7 @@ export class EntityManagerServerPlugin
},
v2: {
instanceAsFilter,
+ identityFieldsBySource,
},
};
}
diff --git a/x-pack/platform/plugins/shared/integration_assistant/server/util/painless.ts b/x-pack/platform/plugins/shared/integration_assistant/server/util/painless.ts
index 797a14422f7b2..55e9e123c2502 100644
--- a/x-pack/platform/plugins/shared/integration_assistant/server/util/painless.ts
+++ b/x-pack/platform/plugins/shared/integration_assistant/server/util/painless.ts
@@ -45,7 +45,7 @@ const INGEST_PIPELINE_PAINLESS_CONTEXT = 'ctx' as const as SafePainlessExpressio
* - Subsequent characters can be underscores, letters, or digits.
*
* This is the ID and DOTID regexp in the Painless grammar under the following link:
- * @link packages/kbn-monaco/src/painless/antlr/painless_parser.g4
+ * @link src/platform/packages/shared/kbn-monaco/src/painless/antlr/painless_parser.g4
*/
const PAINLESS_IDENTIFIER_REGEXP = /^[_a-zA-Z][_a-zA-Z0-9]*$/;
diff --git a/x-pack/platform/plugins/shared/stack_connectors/common/openai/schema.ts b/x-pack/platform/plugins/shared/stack_connectors/common/openai/schema.ts
index 8a08da157b163..7c3d4afcb8d1e 100644
--- a/x-pack/platform/plugins/shared/stack_connectors/common/openai/schema.ts
+++ b/x-pack/platform/plugins/shared/stack_connectors/common/openai/schema.ts
@@ -68,6 +68,41 @@ const AIMessage = schema.object({
export const InvokeAIActionParamsSchema = schema.object({
messages: schema.arrayOf(AIMessage),
model: schema.maybe(schema.string()),
+ tools: schema.maybe(
+ schema.arrayOf(
+ schema.object(
+ {
+ type: schema.literal('function'),
+ function: schema.object(
+ {
+ description: schema.maybe(schema.string()),
+ name: schema.string(),
+ parameters: schema.object({}, { unknowns: 'allow' }),
+ strict: schema.maybe(schema.boolean()),
+ },
+ { unknowns: 'allow' }
+ ),
+ },
+ // Not sure if this will include other properties, we should pass them if it does
+ { unknowns: 'allow' }
+ )
+ )
+ ),
+ tool_choice: schema.maybe(
+ schema.oneOf([
+ schema.literal('none'),
+ schema.literal('auto'),
+ schema.literal('required'),
+ schema.object(
+ {
+ type: schema.literal('function'),
+ function: schema.object({ name: schema.string() }, { unknowns: 'allow' }),
+ },
+ { unknowns: 'ignore' }
+ ),
+ ])
+ ),
+ // Deprecated in favor of tools
functions: schema.maybe(
schema.arrayOf(
schema.object(
@@ -89,6 +124,7 @@ export const InvokeAIActionParamsSchema = schema.object({
)
)
),
+ // Deprecated in favor of tool_choice
function_call: schema.maybe(
schema.oneOf([
schema.literal('none'),
diff --git a/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/inference/inference.ts b/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/inference/inference.ts
index 63d8904a6af8a..5bb52a3160a45 100644
--- a/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/inference/inference.ts
+++ b/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/inference/inference.ts
@@ -49,7 +49,7 @@ import { chunksIntoMessage, eventSourceStreamIntoObservable } from './helpers';
export class InferenceConnector extends SubActionConnector {
// Not using Axios
protected getResponseErrorMessage(error: AxiosError): string {
- throw new Error('Method not implemented.');
+ throw new Error(error.message || 'Method not implemented.');
}
private inferenceId;
@@ -128,11 +128,13 @@ export class InferenceConnector extends SubActionConnector {
const obs$ = from(eventSourceStreamIntoObservable(res as unknown as Readable)).pipe(
filter((line) => !!line && line !== '[DONE]'),
map((line) => {
- return JSON.parse(line) as OpenAI.ChatCompletionChunk | { error: { message: string } };
+ return JSON.parse(line) as
+ | OpenAI.ChatCompletionChunk
+ | { error: { message?: string; reason?: string } };
}),
tap((line) => {
if ('error' in line) {
- throw new Error(line.error.message);
+ throw new Error(line.error.message || line.error.reason || 'Unknown error');
}
if (
'choices' in line &&
diff --git a/x-pack/solutions/observability/plugins/apm/public/components/app/service_map/index.tsx b/x-pack/solutions/observability/plugins/apm/public/components/app/service_map/index.tsx
index e944277f26ca6..c2c82da25a28e 100644
--- a/x-pack/solutions/observability/plugins/apm/public/components/app/service_map/index.tsx
+++ b/x-pack/solutions/observability/plugins/apm/public/components/app/service_map/index.tsx
@@ -110,7 +110,7 @@ export function ServiceMap({
const { onPageReady } = usePerformanceContext();
const {
- data = { elements: [] },
+ data = { elements: [], nodesCount: 0, tracesCount: 0 },
status,
error,
} = useFetcher(
@@ -187,6 +187,12 @@ export function ServiceMap({
if (status === FETCH_STATUS.SUCCESS) {
onPageReady({
+ customMetrics: {
+ key1: 'num_of_nodes',
+ value1: data.nodesCount,
+ key2: 'num_of_traces',
+ value2: data.tracesCount,
+ },
meta: { rangeFrom: start, rangeTo: end },
});
}
diff --git a/x-pack/solutions/observability/plugins/apm/server/routes/service_map/get_service_map.ts b/x-pack/solutions/observability/plugins/apm/server/routes/service_map/get_service_map.ts
index debc88d3ddefd..901d6572f0ca5 100644
--- a/x-pack/solutions/observability/plugins/apm/server/routes/service_map/get_service_map.ts
+++ b/x-pack/solutions/observability/plugins/apm/server/routes/service_map/get_service_map.ts
@@ -32,6 +32,11 @@ export interface IEnvOptions {
kuery?: string;
}
+export interface ServiceMapTelemetry {
+ tracesCount: number;
+ nodesCount: number;
+}
+
async function getConnectionData({
config,
apmEventClient,
@@ -63,6 +68,8 @@ async function getConnectionData({
const init = {
connections: [],
discoveredServices: [],
+ tracesCount: 0,
+ servicesCount: 0,
};
if (!traceIds.length) {
@@ -99,16 +106,17 @@ async function getConnectionData({
logger.debug('Merged responses');
- return mergedResponses;
+ return { ...mergedResponses, tracesCount: traceIds.length };
});
}
export type ConnectionsResponse = Awaited>;
export type ServicesResponse = Awaited>;
+export type ServiceMapResponse = TransformServiceMapResponse & ServiceMapTelemetry;
export function getServiceMap(
options: IEnvOptions & { maxNumberOfServices: number }
-): Promise {
+): Promise {
return withApmSpan('get_service_map', async () => {
const { logger } = options;
const anomaliesPromise = getServiceAnomalies(
@@ -137,8 +145,10 @@ export function getServiceMap(
},
});
- logger.debug('Transformed service map response');
-
- return transformedResponse;
+ return {
+ ...transformedResponse,
+ tracesCount: connectionData.tracesCount,
+ nodesCount: transformedResponse.nodesCount,
+ };
});
}
diff --git a/x-pack/solutions/observability/plugins/apm/server/routes/service_map/group_resource_nodes.ts b/x-pack/solutions/observability/plugins/apm/server/routes/service_map/group_resource_nodes.ts
index 53deaba0864e1..be417b04c0e1d 100644
--- a/x-pack/solutions/observability/plugins/apm/server/routes/service_map/group_resource_nodes.ts
+++ b/x-pack/solutions/observability/plugins/apm/server/routes/service_map/group_resource_nodes.ts
@@ -39,6 +39,7 @@ interface GroupedEdge {
export interface GroupResourceNodesResponse {
elements: Array;
+ nodesCount: number;
}
export function groupResourceNodes(responseData: {
@@ -151,5 +152,6 @@ export function groupResourceNodes(responseData: {
return {
elements: [...ungroupedNodes, ...groupedNodes, ...ungroupedEdges, ...groupedEdges],
+ nodesCount: ungroupedNodes.length,
};
}
diff --git a/x-pack/solutions/observability/plugins/apm/server/routes/service_map/route.ts b/x-pack/solutions/observability/plugins/apm/server/routes/service_map/route.ts
index 100e65fb62d13..78a71d943f09d 100644
--- a/x-pack/solutions/observability/plugins/apm/server/routes/service_map/route.ts
+++ b/x-pack/solutions/observability/plugins/apm/server/routes/service_map/route.ts
@@ -23,7 +23,7 @@ import { environmentRt, rangeRt, kueryRt } from '../default_api_types';
import { getServiceGroup } from '../service_groups/get_service_group';
import { offsetRt } from '../../../common/comparison_rt';
import { getApmEventClient } from '../../lib/helpers/get_apm_event_client';
-import type { TransformServiceMapResponse } from './transform_service_map_responses';
+import type { ServiceMapResponse } from './get_service_map';
const serviceMapRoute = createApmServerRoute({
endpoint: 'GET /internal/apm/service-map',
@@ -39,7 +39,7 @@ const serviceMapRoute = createApmServerRoute({
]),
}),
security: { authz: { requiredPrivileges: ['apm'] } },
- handler: async (resources): Promise => {
+ handler: async (resources): Promise => {
const { config, context, params, logger } = resources;
if (!config.serviceMapEnabled) {
throw Boom.notFound();
diff --git a/x-pack/solutions/observability/plugins/apm/server/routes/service_map/transform_service_map_responses.test.ts b/x-pack/solutions/observability/plugins/apm/server/routes/service_map/transform_service_map_responses.test.ts
index 26b203d945eed..4bc0519217ab1 100644
--- a/x-pack/solutions/observability/plugins/apm/server/routes/service_map/transform_service_map_responses.test.ts
+++ b/x-pack/solutions/observability/plugins/apm/server/routes/service_map/transform_service_map_responses.test.ts
@@ -73,6 +73,7 @@ describe('transformServiceMapResponses', () => {
},
],
anomalies,
+ tracesCount: 10,
};
const { elements } = transformServiceMapResponses({ response });
@@ -106,6 +107,7 @@ describe('transformServiceMapResponses', () => {
},
],
anomalies,
+ tracesCount: 10,
};
const { elements } = transformServiceMapResponses({ response });
@@ -165,6 +167,7 @@ describe('transformServiceMapResponses', () => {
},
],
anomalies,
+ tracesCount: 10,
};
const { elements } = transformServiceMapResponses({ response });
@@ -203,6 +206,7 @@ describe('transformServiceMapResponses', () => {
},
],
anomalies,
+ tracesCount: 10,
};
const { elements } = transformServiceMapResponses({ response });
@@ -228,6 +232,7 @@ describe('transformServiceMapResponses', () => {
},
],
anomalies,
+ tracesCount: 10,
};
const { elements } = transformServiceMapResponses({ response });
diff --git a/x-pack/solutions/observability/plugins/infra/public/pages/metrics/hosts/components/hosts_table.tsx b/x-pack/solutions/observability/plugins/infra/public/pages/metrics/hosts/components/hosts_table.tsx
index ac782beb012d4..310ff912d503a 100644
--- a/x-pack/solutions/observability/plugins/infra/public/pages/metrics/hosts/components/hosts_table.tsx
+++ b/x-pack/solutions/observability/plugins/infra/public/pages/metrics/hosts/components/hosts_table.tsx
@@ -17,10 +17,13 @@ import { useHostCountContext } from '../hooks/use_host_count';
import { FlyoutWrapper } from './host_details_flyout/flyout_wrapper';
import { DEFAULT_PAGE_SIZE, PAGE_SIZE_OPTIONS } from '../constants';
import { FilterAction } from './table/filter_action';
+import { useUnifiedSearchContext } from '../hooks/use_unified_search';
export const HostsTable = () => {
const { loading } = useHostsViewContext();
- const { loading: hostCountLoading } = useHostCountContext();
+ const { loading: hostCountLoading, count } = useHostCountContext();
+ const { searchCriteria } = useUnifiedSearchContext();
+
const { onPageReady } = usePerformanceContext();
const {
@@ -40,9 +43,16 @@ export const HostsTable = () => {
useEffect(() => {
if (!loading && !hostCountLoading) {
- onPageReady();
+ onPageReady({
+ customMetrics: {
+ key1: 'num_of_hosts',
+ value1: count,
+ key2: `max_hosts_per_page`,
+ value2: searchCriteria.limit,
+ },
+ });
}
- }, [loading, hostCountLoading, onPageReady]);
+ }, [loading, hostCountLoading, onPageReady, count, searchCriteria]);
return (
<>
diff --git a/x-pack/solutions/observability/plugins/inventory/common/entities.ts b/x-pack/solutions/observability/plugins/inventory/common/entities.ts
index b006fa0c7f6d8..8a22f12279c7d 100644
--- a/x-pack/solutions/observability/plugins/inventory/common/entities.ts
+++ b/x-pack/solutions/observability/plugins/inventory/common/entities.ts
@@ -35,11 +35,8 @@ export type EntityGroup = {
export type InventoryEntity = {
entityId: string;
entityType: string;
- entityIdentityFields: string | string[];
entityDisplayName: string;
- entityDefinitionId: string;
entityLastSeenTimestamp: string;
- entityDefinitionVersion: string;
- entitySchemaVersion: string;
+ entityIdentityFields: Record;
alertsCount?: number;
} & EntityMetadata;
diff --git a/x-pack/solutions/observability/plugins/inventory/common/utils/check_entity_type.ts b/x-pack/solutions/observability/plugins/inventory/common/utils/check_entity_type.ts
new file mode 100644
index 0000000000000..168683e342693
--- /dev/null
+++ b/x-pack/solutions/observability/plugins/inventory/common/utils/check_entity_type.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 { BUILT_IN_ENTITY_TYPES } from '@kbn/observability-shared-plugin/common';
+import type { InventoryEntity } from '../entities';
+
+export const isBuiltinEntityOfType = (
+ type: (typeof BUILT_IN_ENTITY_TYPES)[keyof typeof BUILT_IN_ENTITY_TYPES],
+ entity: InventoryEntity
+): boolean => {
+ return entity.entityType === type;
+};
diff --git a/x-pack/solutions/observability/plugins/inventory/common/utils/entity_type_guards.ts b/x-pack/solutions/observability/plugins/inventory/common/utils/entity_type_guards.ts
deleted file mode 100644
index f9ace49b20d3a..0000000000000
--- a/x-pack/solutions/observability/plugins/inventory/common/utils/entity_type_guards.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import type { AgentName } from '@kbn/elastic-agent-utils';
-import type { InventoryEntity } from '../entities';
-
-interface BuiltinEntityMap {
- host: InventoryEntity & { cloud?: { provider?: string[] } };
- container: InventoryEntity & { cloud?: { provider?: string[] } };
- service: InventoryEntity & {
- agent?: { name: AgentName[] };
- service?: { environment?: string | string[] | null };
- };
-}
-
-export const isBuiltinEntityOfType = (
- type: T,
- entity: InventoryEntity
-): entity is BuiltinEntityMap[T] => {
- return entity.entityType === type;
-};
diff --git a/x-pack/solutions/observability/plugins/inventory/e2e/cypress/e2e/home.cy.ts b/x-pack/solutions/observability/plugins/inventory/e2e/cypress/e2e/home.cy.ts
index e86c673c2a6b7..aa1fce49d221d 100644
--- a/x-pack/solutions/observability/plugins/inventory/e2e/cypress/e2e/home.cy.ts
+++ b/x-pack/solutions/observability/plugins/inventory/e2e/cypress/e2e/home.cy.ts
@@ -87,13 +87,13 @@ describe.skip('Home page', () => {
cy.visitKibana('/app/inventory');
cy.wait('@getEEMStatus');
cy.contains('host');
- cy.getByTestSubj('inventoryGroupTitle_entity.type_host').click();
+ cy.getByTestSubj('inventoryGroupTitle_entityType_host').click();
cy.wait('@getEntities');
cy.contains('service');
- cy.getByTestSubj('inventoryGroupTitle_entity.type_service').click();
+ cy.getByTestSubj('inventoryGroupTitle_entityType_service').click();
cy.wait('@getEntities');
cy.contains('container');
- cy.getByTestSubj('inventoryGroupTitle_entity.type_container').click();
+ cy.getByTestSubj('inventoryGroupTitle_entityType_container').click();
cy.wait('@getEntities');
cy.contains('server1');
cy.contains('synth-node-trace-logs');
@@ -151,21 +151,19 @@ describe.skip('Home page', () => {
}).as('getEEMStatus');
cy.intercept('GET', '/internal/inventory/entities?**').as('getEntities');
cy.intercept('GET', '/internal/inventory/entities/types').as('getEntitiesTypes');
- cy.intercept('GET', '/internal/inventory/entities/group_by/**').as('getGroups');
cy.visitKibana('/app/inventory');
cy.wait('@getEntitiesTypes');
cy.wait('@getEEMStatus');
- cy.getByTestSubj('entityTypes_multiSelect_filter').click();
- cy.getByTestSubj('entityTypes_multiSelect_filter_selection_service').click();
- cy.wait('@getGroups');
- cy.getByTestSubj('inventoryGroupTitle_entity.type_service').click();
+ cy.getByTestSubj('entityType_multiSelect_filter').click();
+ cy.getByTestSubj('entityType_multiSelect_filter_selection_service').click();
+ cy.getByTestSubj('inventoryGroupTitle_entityType_service').click();
cy.wait('@getEntities');
cy.get('server1').should('not.exist');
cy.contains('synth-node-trace-logs');
cy.contains('foo').should('not.exist');
- cy.getByTestSubj('entityTypes_multiSelect_filter').click();
- cy.getByTestSubj('entityTypes_multiSelect_filter_selection_service').click();
- cy.getByTestSubj('inventoryGroupTitle_entity.type_service').should('not.exist');
+ cy.getByTestSubj('entityType_multiSelect_filter').click();
+ cy.getByTestSubj('entityType_multiSelect_filter_selection_service').click();
+ cy.getByTestSubj('inventoryGroupTitle_entityType_service').should('not.exist');
});
it('Filters entities by host type', () => {
@@ -174,21 +172,19 @@ describe.skip('Home page', () => {
}).as('getEEMStatus');
cy.intercept('GET', '/internal/inventory/entities?**').as('getEntities');
cy.intercept('GET', '/internal/inventory/entities/types').as('getEntitiesTypes');
- cy.intercept('GET', '/internal/inventory/entities/group_by/**').as('getGroups');
cy.visitKibana('/app/inventory');
cy.wait('@getEntitiesTypes');
cy.wait('@getEEMStatus');
- cy.getByTestSubj('entityTypes_multiSelect_filter').click();
- cy.getByTestSubj('entityTypes_multiSelect_filter_selection_host').click();
- cy.wait('@getGroups');
- cy.getByTestSubj('inventoryGroupTitle_entity.type_host').click();
+ cy.getByTestSubj('entityType_multiSelect_filter').click();
+ cy.getByTestSubj('entityType_multiSelect_filter_selection_host').click();
+ cy.getByTestSubj('inventoryGroupTitle_entityType_host').click();
cy.wait('@getEntities');
cy.contains('server1');
cy.contains('synth-node-trace-logs').should('not.exist');
cy.contains('foo').should('not.exist');
- cy.getByTestSubj('entityTypes_multiSelect_filter').click();
- cy.getByTestSubj('entityTypes_multiSelect_filter_selection_host').click();
- cy.getByTestSubj('inventoryGroupTitle_entity.type_host').should('not.exist');
+ cy.getByTestSubj('entityType_multiSelect_filter').click();
+ cy.getByTestSubj('entityType_multiSelect_filter_selection_host').click();
+ cy.getByTestSubj('inventoryGroupTitle_entityType_host').should('not.exist');
});
it('Filters entities by container type', () => {
@@ -197,21 +193,19 @@ describe.skip('Home page', () => {
}).as('getEEMStatus');
cy.intercept('GET', '/internal/inventory/entities?**').as('getEntities');
cy.intercept('GET', '/internal/inventory/entities/types').as('getEntitiesTypes');
- cy.intercept('GET', '/internal/inventory/entities/group_by/**').as('getGroups');
cy.visitKibana('/app/inventory');
cy.wait('@getEntitiesTypes');
cy.wait('@getEEMStatus');
- cy.getByTestSubj('entityTypes_multiSelect_filter').click();
- cy.getByTestSubj('entityTypes_multiSelect_filter_selection_container').click();
- cy.wait('@getGroups');
- cy.getByTestSubj('inventoryGroupTitle_entity.type_container').click();
+ cy.getByTestSubj('entityType_multiSelect_filter').click();
+ cy.getByTestSubj('entityType_multiSelect_filter_selection_container').click();
+ cy.getByTestSubj('inventoryGroupTitle_entityType_container').click();
cy.wait('@getEntities');
cy.contains('server1').should('not.exist');
cy.contains('synth-node-trace-logs').should('not.exist');
cy.contains('foo');
- cy.getByTestSubj('entityTypes_multiSelect_filter').click();
- cy.getByTestSubj('entityTypes_multiSelect_filter_selection_container').click();
- cy.getByTestSubj('inventoryGroupTitle_entity.type_container').should('not.exist');
+ cy.getByTestSubj('entityType_multiSelect_filter').click();
+ cy.getByTestSubj('entityType_multiSelect_filter_selection_container').click();
+ cy.getByTestSubj('inventoryGroupTitle_entityType_container').should('not.exist');
});
it('Navigates to discover with actions button in the entities list', () => {
@@ -222,7 +216,7 @@ describe.skip('Home page', () => {
cy.visitKibana('/app/inventory');
cy.wait('@getEEMStatus');
cy.contains('container');
- cy.getByTestSubj('inventoryGroupTitle_entity.type_container').click();
+ cy.getByTestSubj('inventoryGroupTitle_entityType_container').click();
cy.wait('@getEntities');
// cy.getByTestSubj('inventoryEntityActionsButton').click();
cy.getByTestSubj('inventoryEntityActionsButton-foo').click();
diff --git a/x-pack/solutions/observability/plugins/inventory/public/components/alerts_badge/alerts_badge.test.tsx b/x-pack/solutions/observability/plugins/inventory/public/components/alerts_badge/alerts_badge.test.tsx
index 0957fa4da8aea..cfdf6fee83170 100644
--- a/x-pack/solutions/observability/plugins/inventory/public/components/alerts_badge/alerts_badge.test.tsx
+++ b/x-pack/solutions/observability/plugins/inventory/public/components/alerts_badge/alerts_badge.test.tsx
@@ -47,17 +47,12 @@ describe('AlertsBadge', () => {
it('render alerts badge for a host entity', () => {
const entity: InventoryEntity = {
...(commonEntityFields as InventoryEntity),
- entityType: 'host',
+ entityType: 'built_in_hosts_from_ecs_data',
entityDisplayName: 'foo',
- entityIdentityFields: 'host.name',
- entityDefinitionId: 'host',
+ entityIdentityFields: { source1: ['host.name'] },
alertsCount: 1,
- host: {
- name: 'foo',
- },
- cloud: {
- provider: null,
- },
+ 'host.name': 'foo',
+ 'cloud.provider': null,
};
mockAsKqlFilter.mockReturnValue('host.name: "foo"');
@@ -70,20 +65,12 @@ describe('AlertsBadge', () => {
it('render alerts badge for a service entity', () => {
const entity: InventoryEntity = {
...(commonEntityFields as InventoryEntity),
- entityType: 'service',
+ entityType: 'built_in_services_from_ecs_data',
entityDisplayName: 'foo',
- entityIdentityFields: 'service.name',
- entityDefinitionId: 'service',
- service: {
- name: 'bar',
- },
- agent: {
- name: 'node',
- },
- cloud: {
- provider: null,
- },
-
+ entityIdentityFields: { source1: ['service.name'] },
+ 'service.name': 'bar',
+ 'agent.name': 'node',
+ 'cloud.provider': null,
alertsCount: 5,
};
mockAsKqlFilter.mockReturnValue('service.name: "bar"');
@@ -97,20 +84,13 @@ describe('AlertsBadge', () => {
it('render alerts badge for a service entity with multiple identity fields', () => {
const entity: InventoryEntity = {
...(commonEntityFields as InventoryEntity),
- entityType: 'service',
+ entityType: 'built_in_services_from_ecs_data',
entityDisplayName: 'foo',
- entityIdentityFields: ['service.name', 'service.environment'],
- entityDefinitionId: 'service',
- service: {
- name: 'bar',
- environment: 'prod',
- },
- agent: {
- name: 'node',
- },
- cloud: {
- provider: null,
- },
+ entityIdentityFields: { source1: ['service.name', 'service.environment'] },
+ 'service.name': 'bar',
+ 'service.environment': 'prod',
+ 'agent.name': 'node',
+ 'cloud.provider': null,
alertsCount: 2,
};
diff --git a/x-pack/solutions/observability/plugins/inventory/public/components/alerts_badge/alerts_badge.tsx b/x-pack/solutions/observability/plugins/inventory/public/components/alerts_badge/alerts_badge.tsx
index 301dcb63d1a17..b8376b34b9975 100644
--- a/x-pack/solutions/observability/plugins/inventory/public/components/alerts_badge/alerts_badge.tsx
+++ b/x-pack/solutions/observability/plugins/inventory/public/components/alerts_badge/alerts_badge.tsx
@@ -22,10 +22,7 @@ export function AlertsBadge({ entity }: { entity: InventoryEntity }) {
const activeAlertsHref = basePath.prepend(
`/app/observability/alerts?_a=${rison.encode({
kuery: entityManager.entityClient.asKqlFilter({
- entity: {
- identity_fields: entity.entityIdentityFields,
- },
- ...entity,
+ entity,
}),
status: 'active',
})}`
diff --git a/x-pack/solutions/observability/plugins/inventory/public/components/entities_grid/entity_name/entity_name.test.tsx b/x-pack/solutions/observability/plugins/inventory/public/components/entities_grid/entity_name/entity_name.test.tsx
index 29a862646c4c4..b5e0506d6d72e 100644
--- a/x-pack/solutions/observability/plugins/inventory/public/components/entities_grid/entity_name/entity_name.test.tsx
+++ b/x-pack/solutions/observability/plugins/inventory/public/components/entities_grid/entity_name/entity_name.test.tsx
@@ -21,7 +21,7 @@ describe('EntityName', () => {
entityId: '1',
entityType: 'service',
entityDisplayName: 'entity_name',
- entityIdentityFields: ['service.name', 'service.environment'],
+ entityIdentityFields: { source1: ['service.name', 'service.environment'] },
entityDefinitionId: 'entity_definition_id',
entitySchemaVersion: '1',
entityDefinitionVersion: '1',
diff --git a/x-pack/solutions/observability/plugins/inventory/public/components/entities_grid/index.tsx b/x-pack/solutions/observability/plugins/inventory/public/components/entities_grid/index.tsx
index bf97d5b4f1c38..7ad14e66153e4 100644
--- a/x-pack/solutions/observability/plugins/inventory/public/components/entities_grid/index.tsx
+++ b/x-pack/solutions/observability/plugins/inventory/public/components/entities_grid/index.tsx
@@ -75,7 +75,7 @@ export function EntitiesGrid({
}
const columnEntityTableId = columnId as EntityColumnIds;
- const entityType = entity.entityType;
+ const { entityType } = entity;
switch (columnEntityTableId) {
case 'alertsCount':
diff --git a/x-pack/solutions/observability/plugins/inventory/public/components/entities_grid/mock/entities_mock.ts b/x-pack/solutions/observability/plugins/inventory/public/components/entities_grid/mock/entities_mock.ts
index 1048b18f82e91..6245cd66b25e4 100644
--- a/x-pack/solutions/observability/plugins/inventory/public/components/entities_grid/mock/entities_mock.ts
+++ b/x-pack/solutions/observability/plugins/inventory/public/components/entities_grid/mock/entities_mock.ts
@@ -41,7 +41,7 @@ const getEntityLatest = (
entityId: generateId(),
entityDefinitionId: faker.string.uuid(),
entityDefinitionVersion: '1.0.0',
- entityIdentityFields: indentityFieldsPerType[entityType],
+ entityIdentityFields: { source1: indentityFieldsPerType[entityType] },
entitySchemaVersion: '1.0.0',
...overrides,
});
@@ -62,11 +62,11 @@ const alertsMock: InventoryEntity[] = [
];
const hostsMock = Array.from({ length: 20 }, () =>
- getEntityLatest('host', { cloud: { provider: 'gcp' } })
+ getEntityLatest('host', { 'cloud.provider': 'gcp' })
);
const containersMock = Array.from({ length: 20 }, () => getEntityLatest('container'));
const servicesMock = Array.from({ length: 20 }, () =>
- getEntityLatest('service', { agent: { name: 'java' } })
+ getEntityLatest('service', { 'agent.name': 'java' })
);
export const entitiesMock = [
diff --git a/x-pack/solutions/observability/plugins/inventory/public/components/entity_group_accordion/entity_group_accordion.test.tsx b/x-pack/solutions/observability/plugins/inventory/public/components/entity_group_accordion/entity_group_accordion.test.tsx
index 747124808df2e..bf41fb9766761 100644
--- a/x-pack/solutions/observability/plugins/inventory/public/components/entity_group_accordion/entity_group_accordion.test.tsx
+++ b/x-pack/solutions/observability/plugins/inventory/public/components/entity_group_accordion/entity_group_accordion.test.tsx
@@ -12,15 +12,16 @@ import { EntityGroupAccordion } from '.';
describe('EntityGroupAccordion', () => {
it('renders with correct values', () => {
const props = {
- groupBy: 'entity.type',
groups: [
{
count: 5999,
- 'entity.type': 'host',
+ 'entity.type': 'built_in_hosts_from_ecs_data',
+ label: 'Hosts',
},
{
count: 2001,
- 'entity.type': 'service',
+ 'entity.type': 'built_in_services_from_ecs_data',
+ label: 'Services',
},
],
};
@@ -28,11 +29,13 @@ describe('EntityGroupAccordion', () => {
);
- expect(screen.getByText(props.groups[0]['entity.type'])).toBeInTheDocument();
- const container = screen.getByTestId('entityCountBadge_entity.type_host');
+ expect(screen.getByText(props.groups[0].label)).toBeInTheDocument();
+ const container = screen.getByTestId(
+ 'entityCountBadge_entityType_built_in_hosts_from_ecs_data'
+ );
expect(within(container).getByText('Entities:')).toBeInTheDocument();
expect(within(container).getByText(props.groups[0].count)).toBeInTheDocument();
});
diff --git a/x-pack/solutions/observability/plugins/inventory/public/components/entity_group_accordion/grouped_entities_grid.tsx b/x-pack/solutions/observability/plugins/inventory/public/components/entity_group_accordion/grouped_entities_grid.tsx
index c2f280cb05912..71965a817c273 100644
--- a/x-pack/solutions/observability/plugins/inventory/public/components/entity_group_accordion/grouped_entities_grid.tsx
+++ b/x-pack/solutions/observability/plugins/inventory/public/components/entity_group_accordion/grouped_entities_grid.tsx
@@ -49,7 +49,7 @@ export function GroupedEntitiesGrid({ groupValue }: Props) {
sortDirection,
sortField,
kuery,
- entityTypes: groupValue?.length ? JSON.stringify([groupValue]) : undefined,
+ entityType: groupValue,
},
},
signal,
diff --git a/x-pack/solutions/observability/plugins/inventory/public/components/entity_group_accordion/index.tsx b/x-pack/solutions/observability/plugins/inventory/public/components/entity_group_accordion/index.tsx
index fa365625474b0..d1f95e794ea1b 100644
--- a/x-pack/solutions/observability/plugins/inventory/public/components/entity_group_accordion/index.tsx
+++ b/x-pack/solutions/observability/plugins/inventory/public/components/entity_group_accordion/index.tsx
@@ -17,13 +17,13 @@ const ENTITIES_COUNT_BADGE = i18n.translate(
);
export interface Props {
- groupBy: string;
groupValue: string;
+ groupLabel: string;
groupCount: number;
isLoading?: boolean;
}
-export function EntityGroupAccordion({ groupBy, groupValue, groupCount, isLoading }: Props) {
+export function EntityGroupAccordion({ groupValue, groupLabel, groupCount, isLoading }: Props) {
const { euiTheme } = useEuiTheme();
const [open, setOpen] = useState(false);
@@ -41,17 +41,17 @@ export function EntityGroupAccordion({ groupBy, groupValue, groupCount, isLoadin
`}
>
- {groupValue}
+ {groupLabel}
}
buttonElement="div"
extraAction={
diff --git a/x-pack/solutions/observability/plugins/inventory/public/components/entity_icon/index.tsx b/x-pack/solutions/observability/plugins/inventory/public/components/entity_icon/index.tsx
index 239d441b5d4e6..73cd654eafd05 100644
--- a/x-pack/solutions/observability/plugins/inventory/public/components/entity_icon/index.tsx
+++ b/x-pack/solutions/observability/plugins/inventory/public/components/entity_icon/index.tsx
@@ -10,8 +10,10 @@ import { type CloudProvider, CloudProviderIcon, AgentIcon } from '@kbn/custom-ic
import { EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui';
import { euiThemeVars } from '@kbn/ui-theme';
import { castArray } from 'lodash';
+import { BUILT_IN_ENTITY_TYPES } from '@kbn/observability-shared-plugin/common';
+import type { AgentName } from '@kbn/elastic-agent-utils';
import type { InventoryEntity } from '../../../common/entities';
-import { isBuiltinEntityOfType } from '../../../common/utils/entity_type_guards';
+import { isBuiltinEntityOfType } from '../../../common/utils/check_entity_type';
interface EntityIconProps {
entity: InventoryEntity;
@@ -20,8 +22,11 @@ interface EntityIconProps {
export function EntityIcon({ entity }: EntityIconProps) {
const defaultIconSize = euiThemeVars.euiSizeL;
- if (isBuiltinEntityOfType('host', entity) || isBuiltinEntityOfType('container', entity)) {
- const cloudProvider = castArray(entity.cloud?.provider)[0];
+ if (
+ isBuiltinEntityOfType(BUILT_IN_ENTITY_TYPES.HOST_V2, entity) ||
+ isBuiltinEntityOfType(BUILT_IN_ENTITY_TYPES.CONTAINER_V2, entity)
+ ) {
+ const cloudProvider = castArray(entity['cloud.provider'] as string)[0];
return (
;
+ if (isBuiltinEntityOfType(BUILT_IN_ENTITY_TYPES.SERVICE_V2, entity)) {
+ return (
+
+ );
}
if (entity.entityType.startsWith('k8s')) {
diff --git a/x-pack/solutions/observability/plugins/inventory/public/components/search_bar/entity_types_multi_select.tsx b/x-pack/solutions/observability/plugins/inventory/public/components/search_bar/entity_types_multi_select.tsx
index 1245e25c3d35b..94ddaddea465e 100644
--- a/x-pack/solutions/observability/plugins/inventory/public/components/search_bar/entity_types_multi_select.tsx
+++ b/x-pack/solutions/observability/plugins/inventory/public/components/search_bar/entity_types_multi_select.tsx
@@ -40,11 +40,12 @@ export function EntityTypesMultiSelect() {
const items = useMemo(
() =>
value?.entityTypes.map((type): EuiSelectableOption => {
- const checked = selectedEntityTypes?.[type];
+ const checked = selectedEntityTypes?.[type.id];
return {
- label: type,
+ label: type.display_name,
+ key: type.id,
checked,
- 'data-test-subj': `entityTypes_multiSelect_filter_selection_${type}`,
+ 'data-test-subj': `entityTypes_multiSelect_filter_selection_${type.id}`,
};
}) || [],
[selectedEntityTypes, value?.entityTypes]
@@ -88,7 +89,7 @@ export function EntityTypesMultiSelect() {
hasActiveFilters={!!items.find((item) => item.checked === 'on')}
numActiveFilters={items.filter((item) => item.checked === 'on').length}
>
- {i18n.translate('xpack.inventory.entityTypesMultSelect.typeFilterButtonLabel', {
+ {i18n.translate('xpack.inventory.entityTypesMultiSelect.typeFilterButtonLabel', {
defaultMessage: 'Type',
})}
@@ -102,13 +103,13 @@ export function EntityTypesMultiSelect() {
searchable
searchProps={{
placeholder: i18n.translate(
- 'xpack.inventory.entityTypesMultSelect.euiSelectable.placeholder',
+ 'xpack.inventory.entityTypesMultiSelect.euiSelectable.placeholder',
{ defaultMessage: 'Filter types' }
),
compressed: true,
}}
aria-label={i18n.translate(
- 'xpack.inventory.entityTypesMultSelect.euiSelectable.typeLabel',
+ 'xpack.inventory.entityTypesMultiSelect.euiSelectable.typeLabel',
{ defaultMessage: 'Entity type' }
)}
options={items}
@@ -116,20 +117,23 @@ export function EntityTypesMultiSelect() {
handleEntityTypeChecked(
newOptions
.filter((item) => item.checked)
- .reduce((acc, curr) => ({ ...acc, [curr.label]: curr.checked! }), {})
+ .reduce((acc, curr) => {
+ acc[curr.key as string] = curr.checked!;
+ return acc;
+ }, {} as EntityType)
);
}}
isLoading={loading}
loadingMessage={i18n.translate(
- 'xpack.inventory.entityTypesMultSelect.euiSelectable.loading',
+ 'xpack.inventory.entityTypesMultiSelect.euiSelectable.loading',
{ defaultMessage: 'Loading types' }
)}
emptyMessage={i18n.translate(
- 'xpack.inventory.entityTypesMultSelect.euiSelectable.empty',
+ 'xpack.inventory.entityTypesMultiSelect.euiSelectable.empty',
{ defaultMessage: 'No types available' }
)}
noMatchesMessage={i18n.translate(
- 'xpack.inventory.entityTypesMultSelect.euiSelectable.notFound',
+ 'xpack.inventory.entityTypesMultiSelect.euiSelectable.notFound',
{ defaultMessage: 'No types found' }
)}
>
diff --git a/x-pack/solutions/observability/plugins/inventory/public/hooks/use_detail_view_redirect.test.ts b/x-pack/solutions/observability/plugins/inventory/public/hooks/use_detail_view_redirect.test.ts
index f05cdcf21cb2e..858b8ff60882f 100644
--- a/x-pack/solutions/observability/plugins/inventory/public/hooks/use_detail_view_redirect.test.ts
+++ b/x-pack/solutions/observability/plugins/inventory/public/hooks/use_detail_view_redirect.test.ts
@@ -61,14 +61,10 @@ describe('useDetailViewRedirect', () => {
it('getEntityRedirectUrl should return the correct URL for host entity', () => {
const entity: InventoryEntity = {
...(commonEntityFields as InventoryEntity),
- entityType: 'host',
- entityIdentityFields: ['host.name'],
- host: {
- name: 'host-1',
- },
- cloud: {
- provider: null,
- },
+ entityType: 'built_in_hosts_from_ecs_data',
+ entityIdentityFields: { source1: ['host.name'] },
+ 'host.name': 'host-1',
+ 'cloud.provider': null,
};
mockGetIdentityFieldsValue.mockReturnValue({ [HOST_NAME]: 'host-1' });
@@ -84,14 +80,10 @@ describe('useDetailViewRedirect', () => {
it('getEntityRedirectUrl should return the correct URL for container entity', () => {
const entity: InventoryEntity = {
...(commonEntityFields as InventoryEntity),
- entityType: 'container',
- entityIdentityFields: ['container.id'],
- container: {
- id: 'container-1',
- },
- cloud: {
- provider: null,
- },
+ entityType: 'built_in_containers_from_ecs_data',
+ entityIdentityFields: { source1: ['container.id'] },
+ 'container.id': 'container-1',
+ 'cloud.provider': null,
};
mockGetIdentityFieldsValue.mockReturnValue({ [CONTAINER_ID]: 'container-1' });
@@ -110,15 +102,11 @@ describe('useDetailViewRedirect', () => {
it('getEntityRedirectUrl should return the correct URL for service entity', () => {
const entity: InventoryEntity = {
...(commonEntityFields as InventoryEntity),
- entityType: 'service',
- entityIdentityFields: ['service.name'],
- agent: {
- name: 'node',
- },
- service: {
- name: 'service-1',
- environment: 'prod',
- },
+ entityType: 'built_in_services_from_ecs_data',
+ entityIdentityFields: { source1: ['service.name'] },
+ 'service.name': 'service-1',
+ 'agent.name': 'node',
+ 'service.environment': 'prod',
};
mockGetIdentityFieldsValue.mockReturnValue({ [SERVICE_NAME]: 'service-1' });
mockGetRedirectUrl.mockReturnValue('service-overview-url');
@@ -134,27 +122,40 @@ describe('useDetailViewRedirect', () => {
[
[
- BUILT_IN_ENTITY_TYPES.KUBERNETES.CLUSTER.ecs,
+ BUILT_IN_ENTITY_TYPES.KUBERNETES_V2.CLUSTER.ecs,
'kubernetes-f4dc26db-1b53-4ea2-a78b-1bfab8ea267c',
],
- [BUILT_IN_ENTITY_TYPES.KUBERNETES.CLUSTER.semconv, 'kubernetes_otel-cluster-overview'],
+ [BUILT_IN_ENTITY_TYPES.KUBERNETES_V2.CLUSTER.semconv, 'kubernetes_otel-cluster-overview'],
[
- BUILT_IN_ENTITY_TYPES.KUBERNETES.CRONJOB.ecs,
+ BUILT_IN_ENTITY_TYPES.KUBERNETES_V2.CRON_JOB.ecs,
'kubernetes-0a672d50-bcb1-11ec-b64f-7dd6e8e82013',
],
[
- BUILT_IN_ENTITY_TYPES.KUBERNETES.DAEMONSET.ecs,
+ BUILT_IN_ENTITY_TYPES.KUBERNETES_V2.DAEMON_SET.ecs,
'kubernetes-85879010-bcb1-11ec-b64f-7dd6e8e82013',
],
[
- BUILT_IN_ENTITY_TYPES.KUBERNETES.DEPLOYMENT.ecs,
+ BUILT_IN_ENTITY_TYPES.KUBERNETES_V2.DEPLOYMENT.ecs,
'kubernetes-5be46210-bcb1-11ec-b64f-7dd6e8e82013',
],
- [BUILT_IN_ENTITY_TYPES.KUBERNETES.JOB.ecs, 'kubernetes-9bf990a0-bcb1-11ec-b64f-7dd6e8e82013'],
- [BUILT_IN_ENTITY_TYPES.KUBERNETES.NODE.ecs, 'kubernetes-b945b7b0-bcb1-11ec-b64f-7dd6e8e82013'],
- [BUILT_IN_ENTITY_TYPES.KUBERNETES.POD.ecs, 'kubernetes-3d4d9290-bcb1-11ec-b64f-7dd6e8e82013'],
[
- BUILT_IN_ENTITY_TYPES.KUBERNETES.STATEFULSET.ecs,
+ BUILT_IN_ENTITY_TYPES.KUBERNETES_V2.JOB.ecs,
+ 'kubernetes-9bf990a0-bcb1-11ec-b64f-7dd6e8e82013',
+ ],
+ [
+ BUILT_IN_ENTITY_TYPES.KUBERNETES_V2.NODE.ecs,
+ 'kubernetes-b945b7b0-bcb1-11ec-b64f-7dd6e8e82013',
+ ],
+ [
+ BUILT_IN_ENTITY_TYPES.KUBERNETES_V2.POD.ecs,
+ 'kubernetes-3d4d9290-bcb1-11ec-b64f-7dd6e8e82013',
+ ],
+ [
+ BUILT_IN_ENTITY_TYPES.KUBERNETES_V2.SERVICE,
+ 'kubernetes-ff1b3850-bcb1-11ec-b64f-7dd6e8e82013',
+ ],
+ [
+ BUILT_IN_ENTITY_TYPES.KUBERNETES_V2.STATEFUL_SET.ecs,
'kubernetes-21694370-bcb2-11ec-b64f-7dd6e8e82013',
],
].forEach(([entityType, dashboardId]) => {
@@ -162,10 +163,8 @@ describe('useDetailViewRedirect', () => {
const entity: InventoryEntity = {
...(commonEntityFields as InventoryEntity),
entityType,
- entityIdentityFields: ['some.field'],
- some: {
- field: 'some-value',
- },
+ entityIdentityFields: { source1: ['some.field'] },
+ 'some.field': 'some-value',
};
mockAsKqlFilter.mockReturnValue('kql-query');
diff --git a/x-pack/solutions/observability/plugins/inventory/public/hooks/use_detail_view_redirect.ts b/x-pack/solutions/observability/plugins/inventory/public/hooks/use_detail_view_redirect.ts
index ec193ae9dfcad..4ba96c0e5f7d6 100644
--- a/x-pack/solutions/observability/plugins/inventory/public/hooks/use_detail_view_redirect.ts
+++ b/x-pack/solutions/observability/plugins/inventory/public/hooks/use_detail_view_redirect.ts
@@ -14,24 +14,25 @@ import {
import { useCallback } from 'react';
import type { DashboardLocatorParams } from '@kbn/dashboard-plugin/public';
import { DASHBOARD_APP_LOCATOR } from '@kbn/deeplinks-analytics';
-import { castArray } from 'lodash';
-import { isBuiltinEntityOfType } from '../../common/utils/entity_type_guards';
+import { isBuiltinEntityOfType } from '../../common/utils/check_entity_type';
import type { InventoryEntity } from '../../common/entities';
import { useKibana } from './use_kibana';
const KUBERNETES_DASHBOARDS_IDS: Record = {
- [BUILT_IN_ENTITY_TYPES.KUBERNETES.CLUSTER.ecs]: 'kubernetes-f4dc26db-1b53-4ea2-a78b-1bfab8ea267c',
- [BUILT_IN_ENTITY_TYPES.KUBERNETES.CLUSTER.semconv]: 'kubernetes_otel-cluster-overview',
- [BUILT_IN_ENTITY_TYPES.KUBERNETES.CRONJOB.ecs]: 'kubernetes-0a672d50-bcb1-11ec-b64f-7dd6e8e82013',
- [BUILT_IN_ENTITY_TYPES.KUBERNETES.DAEMONSET.ecs]:
+ [BUILT_IN_ENTITY_TYPES.KUBERNETES_V2.CLUSTER.ecs]:
+ 'kubernetes-f4dc26db-1b53-4ea2-a78b-1bfab8ea267c',
+ [BUILT_IN_ENTITY_TYPES.KUBERNETES_V2.CLUSTER.semconv]: 'kubernetes_otel-cluster-overview',
+ [BUILT_IN_ENTITY_TYPES.KUBERNETES_V2.CRON_JOB.ecs]:
+ 'kubernetes-0a672d50-bcb1-11ec-b64f-7dd6e8e82013',
+ [BUILT_IN_ENTITY_TYPES.KUBERNETES_V2.DAEMON_SET.ecs]:
'kubernetes-85879010-bcb1-11ec-b64f-7dd6e8e82013',
- [BUILT_IN_ENTITY_TYPES.KUBERNETES.DEPLOYMENT.ecs]:
+ [BUILT_IN_ENTITY_TYPES.KUBERNETES_V2.DEPLOYMENT.ecs]:
'kubernetes-5be46210-bcb1-11ec-b64f-7dd6e8e82013',
- [BUILT_IN_ENTITY_TYPES.KUBERNETES.JOB.ecs]: 'kubernetes-9bf990a0-bcb1-11ec-b64f-7dd6e8e82013',
- [BUILT_IN_ENTITY_TYPES.KUBERNETES.NODE.ecs]: 'kubernetes-b945b7b0-bcb1-11ec-b64f-7dd6e8e82013',
- [BUILT_IN_ENTITY_TYPES.KUBERNETES.POD.ecs]: 'kubernetes-3d4d9290-bcb1-11ec-b64f-7dd6e8e82013',
- [BUILT_IN_ENTITY_TYPES.KUBERNETES.SERVICE.ecs]: 'kubernetes-ff1b3850-bcb1-11ec-b64f-7dd6e8e82013',
- [BUILT_IN_ENTITY_TYPES.KUBERNETES.STATEFULSET.ecs]:
+ [BUILT_IN_ENTITY_TYPES.KUBERNETES_V2.JOB.ecs]: 'kubernetes-9bf990a0-bcb1-11ec-b64f-7dd6e8e82013',
+ [BUILT_IN_ENTITY_TYPES.KUBERNETES_V2.NODE.ecs]: 'kubernetes-b945b7b0-bcb1-11ec-b64f-7dd6e8e82013',
+ [BUILT_IN_ENTITY_TYPES.KUBERNETES_V2.POD.ecs]: 'kubernetes-3d4d9290-bcb1-11ec-b64f-7dd6e8e82013',
+ [BUILT_IN_ENTITY_TYPES.KUBERNETES_V2.SERVICE]: 'kubernetes-ff1b3850-bcb1-11ec-b64f-7dd6e8e82013',
+ [BUILT_IN_ENTITY_TYPES.KUBERNETES_V2.STATEFUL_SET.ecs]:
'kubernetes-21694370-bcb2-11ec-b64f-7dd6e8e82013',
};
@@ -48,21 +49,23 @@ export const useDetailViewRedirect = () => {
const getDetailViewRedirectUrl = useCallback(
(entity: InventoryEntity) => {
const identityFieldsValue = entityManager.entityClient.getIdentityFieldsValue({
- entity: {
- identity_fields: entity.entityIdentityFields,
- },
- ...entity,
+ entity,
});
- const identityFields = castArray(entity.entityIdentityFields);
+ const identityFields = Object.keys(identityFieldsValue || {});
- if (isBuiltinEntityOfType('host', entity) || isBuiltinEntityOfType('container', entity)) {
+ if (
+ isBuiltinEntityOfType(BUILT_IN_ENTITY_TYPES.HOST_V2, entity) ||
+ isBuiltinEntityOfType(BUILT_IN_ENTITY_TYPES.CONTAINER_V2, entity)
+ ) {
return assetDetailsLocator?.getRedirectUrl({
assetId: identityFieldsValue[identityFields[0]],
- assetType: entity.entityType,
+ assetType: isBuiltinEntityOfType(BUILT_IN_ENTITY_TYPES.HOST_V2, entity)
+ ? 'host'
+ : 'container',
});
}
- if (isBuiltinEntityOfType('service', entity)) {
+ if (isBuiltinEntityOfType(BUILT_IN_ENTITY_TYPES.SERVICE_V2, entity)) {
return serviceOverviewLocator?.getRedirectUrl({
serviceName: identityFieldsValue[identityFields[0]],
});
@@ -75,7 +78,7 @@ export const useDetailViewRedirect = () => {
const getDashboardRedirectUrl = useCallback(
(entity: InventoryEntity) => {
- const type = entity.entityType;
+ const { entityType: type } = entity;
const dashboardId = KUBERNETES_DASHBOARDS_IDS[type];
return dashboardId
@@ -84,10 +87,7 @@ export const useDetailViewRedirect = () => {
query: {
language: 'kuery',
query: entityManager.entityClient.asKqlFilter({
- entity: {
- identity_fields: entity.entityIdentityFields,
- },
- ...entity,
+ entity,
}),
},
})
diff --git a/x-pack/solutions/observability/plugins/inventory/public/hooks/use_discover_redirect.ts b/x-pack/solutions/observability/plugins/inventory/public/hooks/use_discover_redirect.ts
index dc9f5bf4a4740..141b6d2fc97bc 100644
--- a/x-pack/solutions/observability/plugins/inventory/public/hooks/use_discover_redirect.ts
+++ b/x-pack/solutions/observability/plugins/inventory/public/hooks/use_discover_redirect.ts
@@ -15,7 +15,7 @@ export const useDiscoverRedirect = (entity: InventoryEntity) => {
services: { share, application, entityManager },
} = useKibana();
const { entityDefinitions, isEntityDefinitionLoading } = useFetchEntityDefinition(
- entity.entityDefinitionId
+ entity.entityDefinitionId as string
);
const title = useMemo(
@@ -33,10 +33,7 @@ export const useDiscoverRedirect = (entity: InventoryEntity) => {
const getDiscoverEntitiesRedirectUrl = useCallback(() => {
const entityKqlFilter = entity
? entityManager.entityClient.asKqlFilter({
- entity: {
- identity_fields: entity.entityIdentityFields,
- },
- ...entity,
+ entity,
})
: '';
diff --git a/x-pack/solutions/observability/plugins/inventory/public/pages/inventory_page/index.tsx b/x-pack/solutions/observability/plugins/inventory/public/pages/inventory_page/index.tsx
index 6eab905a40692..49fe1a965d94d 100644
--- a/x-pack/solutions/observability/plugins/inventory/public/pages/inventory_page/index.tsx
+++ b/x-pack/solutions/observability/plugins/inventory/public/pages/inventory_page/index.tsx
@@ -5,8 +5,6 @@
* 2.0.
*/
import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
-import { ENTITY_TYPE } from '@kbn/observability-shared-plugin/common';
-import { flattenObject } from '@kbn/observability-utils-common/object/flatten_object';
import React from 'react';
import useEffectOnce from 'react-use/lib/useEffectOnce';
import { EntitiesSummary } from '../../components/entities_summary';
@@ -28,19 +26,11 @@ export function InventoryPage() {
query: { kuery },
} = useInventoryParams('/');
const { entityTypes } = useInventoryDecodedQueryParams();
-
- const {
- value = { groupBy: ENTITY_TYPE, groups: [], entitiesCount: 0 },
- refresh,
- loading,
- } = useInventoryAbortableAsync(
+ const { value, refresh, loading } = useInventoryAbortableAsync(
({ signal }) => {
const { entityTypesOff, entityTypesOn } = groupEntityTypesByStatus(entityTypes);
- return inventoryAPIClient.fetch('GET /internal/inventory/entities/group_by/{field}', {
+ return inventoryAPIClient.fetch('GET /internal/inventory/entities/types', {
params: {
- path: {
- field: ENTITY_TYPE,
- },
query: {
includeEntityTypes: entityTypesOn.length ? JSON.stringify(entityTypesOn) : undefined,
excludeEntityTypes: entityTypesOff.length ? JSON.stringify(entityTypesOff) : undefined,
@@ -62,21 +52,23 @@ export function InventoryPage() {
<>
-
+
- {value.groups.map((group) => {
- const groupValue = flattenObject(group)[value.groupBy];
+ {value?.entityTypes.map((entityType) => {
return (
);
diff --git a/x-pack/solutions/observability/plugins/inventory/server/routes/entities/get_entity_groups.ts b/x-pack/solutions/observability/plugins/inventory/server/routes/entities/get_entity_groups.ts
deleted file mode 100644
index ead3109060d13..0000000000000
--- a/x-pack/solutions/observability/plugins/inventory/server/routes/entities/get_entity_groups.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import type { ScalarValue } from '@elastic/elasticsearch/lib/api/types';
-import { kqlQuery } from '@kbn/observability-plugin/server';
-import { ENTITY_TYPE } from '@kbn/observability-shared-plugin/common';
-import type { ObservabilityElasticsearchClient } from '@kbn/observability-utils-server/es/client/create_observability_es_client';
-import {
- ENTITIES_LATEST_ALIAS,
- MAX_NUMBER_OF_ENTITIES,
- type EntityGroup,
-} from '../../../common/entities';
-import { getBuiltinEntityDefinitionIdESQLWhereClause } from './query_helper';
-
-export async function getEntityGroupsBy({
- inventoryEsClient,
- field,
- kuery,
- includeEntityTypes = [],
- excludeEntityTypes = [],
-}: {
- inventoryEsClient: ObservabilityElasticsearchClient;
- field: string;
- includeEntityTypes?: string[];
- excludeEntityTypes?: string[];
- kuery?: string;
-}): Promise {
- const from = `FROM ${ENTITIES_LATEST_ALIAS}`;
- const where = [getBuiltinEntityDefinitionIdESQLWhereClause()];
- const params: ScalarValue[] = [];
-
- if (includeEntityTypes.length) {
- where.push(`WHERE ${ENTITY_TYPE} IN (${includeEntityTypes.map(() => '?').join()})`);
- params.push(...includeEntityTypes);
- }
-
- if (excludeEntityTypes.length) {
- where.push(`WHERE ${ENTITY_TYPE} NOT IN (${excludeEntityTypes.map(() => '?').join()})`);
- params.push(...excludeEntityTypes);
- }
-
- const group = `STATS count = COUNT(*) by ${field}`;
- const sort = `SORT ${field} asc`;
- const limit = `LIMIT ${MAX_NUMBER_OF_ENTITIES}`;
- const query = [from, ...where, group, sort, limit].join(' | ');
-
- const { hits } = await inventoryEsClient.esql(
- 'get_entities_groups',
- {
- query,
- filter: { bool: { filter: kqlQuery(kuery) } },
- params,
- },
- { transform: 'plain' }
- );
-
- return hits;
-}
diff --git a/x-pack/solutions/observability/plugins/inventory/server/routes/entities/get_entity_types.ts b/x-pack/solutions/observability/plugins/inventory/server/routes/entities/get_entity_types.ts
deleted file mode 100644
index c1f7894a178b1..0000000000000
--- a/x-pack/solutions/observability/plugins/inventory/server/routes/entities/get_entity_types.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { type ObservabilityElasticsearchClient } from '@kbn/observability-utils-server/es/client/create_observability_es_client';
-import { ENTITY_TYPE } from '@kbn/observability-shared-plugin/common';
-import { ENTITIES_LATEST_ALIAS } from '../../../common/entities';
-import { getBuiltinEntityDefinitionIdESQLWhereClause } from './query_helper';
-
-export async function getEntityTypes({
- inventoryEsClient,
-}: {
- inventoryEsClient: ObservabilityElasticsearchClient;
-}) {
- const entityTypesEsqlResponse = await inventoryEsClient.esql<
- {
- 'entity.type': string;
- },
- { transform: 'plain' }
- >(
- 'get_entity_types',
- {
- query: `FROM ${ENTITIES_LATEST_ALIAS}
- | ${getBuiltinEntityDefinitionIdESQLWhereClause()}
- | STATS count = COUNT(${ENTITY_TYPE}) BY ${ENTITY_TYPE}
- `,
- },
- { transform: 'plain' }
- );
-
- return entityTypesEsqlResponse.hits.map((response) => response['entity.type']);
-}
diff --git a/x-pack/solutions/observability/plugins/inventory/server/routes/entities/get_group_by_terms_agg.test.ts b/x-pack/solutions/observability/plugins/inventory/server/routes/entities/get_group_by_terms_agg.test.ts
index 33bdc5e8a00a5..45603c297da36 100644
--- a/x-pack/solutions/observability/plugins/inventory/server/routes/entities/get_group_by_terms_agg.test.ts
+++ b/x-pack/solutions/observability/plugins/inventory/server/routes/entities/get_group_by_terms_agg.test.ts
@@ -6,21 +6,22 @@
*/
import { getGroupByTermsAgg } from './get_group_by_terms_agg';
-import type { IdentityFieldsPerEntityType } from './get_identity_fields_per_entity_type';
+
+type Fields = Record;
describe('getGroupByTermsAgg', () => {
it('should return an empty object when fields is empty', () => {
- const fields: IdentityFieldsPerEntityType = new Map();
+ const fields: Fields = {};
const result = getGroupByTermsAgg(fields);
expect(result).toEqual({});
});
it('should correctly generate aggregation structure for service, host, and container entity types', () => {
- const fields: IdentityFieldsPerEntityType = new Map([
- ['service', ['service.name', 'service.environment']],
- ['host', ['host.name']],
- ['container', ['container.id', 'foo.bar']],
- ]);
+ const fields: Fields = {
+ service: ['service.name', 'service.environment'],
+ host: ['host.name'],
+ container: ['container.id', 'foo.bar'],
+ };
const result = getGroupByTermsAgg(fields);
@@ -58,7 +59,10 @@ describe('getGroupByTermsAgg', () => {
});
});
it('should override maxSize when provided', () => {
- const fields: IdentityFieldsPerEntityType = new Map([['host', ['host.name']]]);
+ const fields: Fields = {
+ host: ['host.name'],
+ };
+
const result = getGroupByTermsAgg(fields, 10);
expect(result.host.composite.size).toBe(10);
});
diff --git a/x-pack/solutions/observability/plugins/inventory/server/routes/entities/get_group_by_terms_agg.ts b/x-pack/solutions/observability/plugins/inventory/server/routes/entities/get_group_by_terms_agg.ts
index 71c6e8901c1b7..705c9ea43a642 100644
--- a/x-pack/solutions/observability/plugins/inventory/server/routes/entities/get_group_by_terms_agg.ts
+++ b/x-pack/solutions/observability/plugins/inventory/server/routes/entities/get_group_by_terms_agg.ts
@@ -5,11 +5,11 @@
* 2.0.
*/
-import type { IdentityFieldsPerEntityType } from './get_identity_fields_per_entity_type';
+import type { AggregationsCompositeAggregation } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
-export const getGroupByTermsAgg = (fields: IdentityFieldsPerEntityType, maxSize = 500) => {
- return Array.from(fields).reduce((acc, [entityType, identityFields]) => {
- acc[entityType] = {
+export const getGroupByTermsAgg = (fields: { [key: string]: string[] }, maxSize = 500) => {
+ return Object.entries(fields).reduce((acc, [sourceId, identityFields]) => {
+ acc[sourceId] = {
composite: {
size: maxSize,
sources: identityFields.map((field) => ({
@@ -22,5 +22,5 @@ export const getGroupByTermsAgg = (fields: IdentityFieldsPerEntityType, maxSize
},
};
return acc;
- }, {} as Record);
+ }, {} as Record);
};
diff --git a/x-pack/solutions/observability/plugins/inventory/server/routes/entities/get_identify_fields.test.ts b/x-pack/solutions/observability/plugins/inventory/server/routes/entities/get_identify_fields.test.ts
deleted file mode 100644
index 8b6b3b109352c..0000000000000
--- a/x-pack/solutions/observability/plugins/inventory/server/routes/entities/get_identify_fields.test.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import type { InventoryEntity } from '../../../common/entities';
-import { getIdentityFieldsPerEntityType } from './get_identity_fields_per_entity_type';
-
-const commonEntityFields: Partial = {
- entityLastSeenTimestamp: '2023-10-09T00:00:00Z',
- entityId: '1',
- entityDisplayName: 'entity_name',
- entityDefinitionId: 'entity_definition_id',
- alertsCount: 3,
-};
-
-describe('getIdentityFields', () => {
- it('should return an empty Map when no entities are provided', () => {
- const result = getIdentityFieldsPerEntityType([]);
- expect(result.size).toBe(0);
- });
- it('should return a Map with unique entity types and their respective identity fields', () => {
- const serviceEntity: InventoryEntity = {
- ...(commonEntityFields as InventoryEntity),
- entityIdentityFields: ['service.name', 'service.environment'],
- entityType: 'service',
- agent: {
- name: 'node',
- },
- service: {
- name: 'my-service',
- },
- };
-
- const hostEntity: InventoryEntity = {
- ...(commonEntityFields as InventoryEntity),
- entityIdentityFields: ['host.name'],
- entityType: 'host',
- cloud: {
- provider: null,
- },
- host: {
- name: 'my-host',
- },
- };
-
- const containerEntity: InventoryEntity = {
- ...(commonEntityFields as InventoryEntity),
- entityIdentityFields: ['container.id'],
- entityType: 'container',
- host: {
- name: 'my-host',
- },
- cloud: {
- provider: null,
- },
- container: {
- id: '123',
- },
- };
-
- const mockEntities = [serviceEntity, hostEntity, containerEntity];
- const result = getIdentityFieldsPerEntityType(mockEntities);
-
- expect(result.size).toBe(3);
-
- expect(result.get('service')).toEqual(['service.name', 'service.environment']);
- expect(result.get('host')).toEqual(['host.name']);
- expect(result.get('container')).toEqual(['container.id']);
- });
-});
diff --git a/x-pack/solutions/observability/plugins/inventory/server/routes/entities/get_identity_fields_per_entity_type.ts b/x-pack/solutions/observability/plugins/inventory/server/routes/entities/get_identity_fields_per_entity_type.ts
deleted file mode 100644
index 06070b66bad1f..0000000000000
--- a/x-pack/solutions/observability/plugins/inventory/server/routes/entities/get_identity_fields_per_entity_type.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { castArray } from 'lodash';
-import type { InventoryEntity } from '../../../common/entities';
-
-export type IdentityFieldsPerEntityType = Map;
-
-export const getIdentityFieldsPerEntityType = (latestEntities: InventoryEntity[]) => {
- const identityFieldsPerEntityType = new Map();
-
- latestEntities.forEach((entity) =>
- identityFieldsPerEntityType.set(entity.entityType, castArray(entity.entityIdentityFields))
- );
-
- return identityFieldsPerEntityType;
-};
diff --git a/x-pack/solutions/observability/plugins/inventory/server/routes/entities/get_latest_entities.ts b/x-pack/solutions/observability/plugins/inventory/server/routes/entities/get_latest_entities.ts
deleted file mode 100644
index 83f576220d12a..0000000000000
--- a/x-pack/solutions/observability/plugins/inventory/server/routes/entities/get_latest_entities.ts
+++ /dev/null
@@ -1,111 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import type { ScalarValue } from '@elastic/elasticsearch/lib/api/types';
-import { kqlQuery } from '@kbn/observability-plugin/server';
-import {
- ENTITY_DISPLAY_NAME,
- ENTITY_LAST_SEEN,
- ENTITY_TYPE,
-} from '@kbn/observability-shared-plugin/common';
-import { unflattenObject } from '@kbn/observability-utils-common/object/unflatten_object';
-import type { ObservabilityElasticsearchClient } from '@kbn/observability-utils-server/es/client/create_observability_es_client';
-import {
- ENTITIES_LATEST_ALIAS,
- MAX_NUMBER_OF_ENTITIES,
- type EntityColumnIds,
- type InventoryEntity,
-} from '../../../common/entities';
-import { getBuiltinEntityDefinitionIdESQLWhereClause } from './query_helper';
-
-type EntitySortableColumnIds = Extract<
- EntityColumnIds,
- 'entityLastSeenTimestamp' | 'entityDisplayName' | 'entityType'
->;
-const SORT_FIELDS_TO_ES_FIELDS: Record = {
- entityLastSeenTimestamp: ENTITY_LAST_SEEN,
- entityDisplayName: ENTITY_DISPLAY_NAME,
- entityType: ENTITY_TYPE,
-} as const;
-
-export async function getLatestEntities({
- inventoryEsClient,
- sortDirection,
- sortField,
- kuery,
- entityTypes,
-}: {
- inventoryEsClient: ObservabilityElasticsearchClient;
- sortDirection: 'asc' | 'desc';
- sortField: EntityColumnIds;
- kuery?: string;
- entityTypes?: string[];
-}): Promise {
- // alertsCount doesn't exist in entities index. Ignore it and sort by entity.lastSeenTimestamp by default.
- const entitiesSortField =
- SORT_FIELDS_TO_ES_FIELDS[sortField as EntitySortableColumnIds] ?? ENTITY_LAST_SEEN;
-
- const from = `FROM ${ENTITIES_LATEST_ALIAS}`;
- const where: string[] = [getBuiltinEntityDefinitionIdESQLWhereClause()];
- const params: ScalarValue[] = [];
-
- if (entityTypes) {
- where.push(`WHERE ${ENTITY_TYPE} IN (${entityTypes.map(() => '?').join()})`);
- params.push(...entityTypes);
- }
-
- const sort = `SORT ${entitiesSortField} ${sortDirection}`;
- const limit = `LIMIT ${MAX_NUMBER_OF_ENTITIES}`;
-
- const query = [from, ...where, sort, limit].join(' | ');
-
- const latestEntitiesEsqlResponse = await inventoryEsClient.esql<
- {
- 'entity.id': string;
- 'entity.type': string;
- 'entity.definition_id': string;
- 'entity.display_name': string;
- 'entity.identity_fields': string | string[];
- 'entity.last_seen_timestamp': string;
- 'entity.definition_version': string;
- 'entity.schema_version': string;
- } & Record,
- { transform: 'plain' }
- >(
- 'get_latest_entities',
- {
- query,
- filter: { bool: { filter: kqlQuery(kuery) } },
- params,
- },
- { transform: 'plain' }
- );
-
- return latestEntitiesEsqlResponse.hits.map((latestEntity) => {
- Object.keys(latestEntity).forEach((key) => {
- const keyOfObject = key as keyof typeof latestEntity;
- // strip out multi-field aliases
- if (keyOfObject.endsWith('.text') || keyOfObject.endsWith('.keyword')) {
- delete latestEntity[keyOfObject];
- }
- });
-
- const { entity, ...metadata } = unflattenObject(latestEntity);
-
- return {
- entityId: entity.id,
- entityType: entity.type,
- entityDefinitionId: entity.definition_id,
- entityDisplayName: entity.display_name,
- entityIdentityFields: entity.identity_fields,
- entityLastSeenTimestamp: entity.last_seen_timestamp,
- entityDefinitionVersion: entity.definition_version,
- entitySchemaVersion: entity.schema_version,
- ...metadata,
- };
- });
-}
diff --git a/x-pack/solutions/observability/plugins/inventory/server/routes/entities/get_latest_entities_alerts.ts b/x-pack/solutions/observability/plugins/inventory/server/routes/entities/get_latest_entities_alerts.ts
index c7291e772470b..e1959b9ba2d90 100644
--- a/x-pack/solutions/observability/plugins/inventory/server/routes/entities/get_latest_entities_alerts.ts
+++ b/x-pack/solutions/observability/plugins/inventory/server/routes/entities/get_latest_entities_alerts.ts
@@ -9,7 +9,6 @@ import { termQuery } from '@kbn/observability-plugin/server';
import { ALERT_STATUS, ALERT_STATUS_ACTIVE } from '@kbn/rule-data-utils';
import type { AlertsClient } from '../../lib/create_alerts_client/create_alerts_client';
import { getGroupByTermsAgg } from './get_group_by_terms_agg';
-import type { IdentityFieldsPerEntityType } from './get_identity_fields_per_entity_type';
interface Bucket {
key: Record;
@@ -20,12 +19,12 @@ type EntityTypeBucketsAggregation = Record;
export async function getLatestEntitiesAlerts({
alertsClient,
- identityFieldsPerEntityType,
+ identityFieldsBySource,
}: {
alertsClient: AlertsClient;
- identityFieldsPerEntityType: IdentityFieldsPerEntityType;
-}): Promise> {
- if (identityFieldsPerEntityType.size === 0) {
+ identityFieldsBySource: Record;
+}): Promise> {
+ if (Object.keys(identityFieldsBySource).length === 0) {
return [];
}
@@ -41,22 +40,23 @@ export async function getLatestEntitiesAlerts({
const response = await alertsClient.search({
...filter,
- aggs: getGroupByTermsAgg(identityFieldsPerEntityType),
+ aggs: getGroupByTermsAgg(identityFieldsBySource),
});
const aggregations = response.aggregations as EntityTypeBucketsAggregation;
- const alerts = Array.from(identityFieldsPerEntityType).flatMap(([entityType]) => {
- const entityAggregation = aggregations?.[entityType];
+ const alerts = Object.keys(identityFieldsBySource)
+ .map((sourceId) => {
+ const entityAggregation = aggregations?.[sourceId];
- const buckets = entityAggregation.buckets ?? [];
+ const buckets = entityAggregation.buckets ?? [];
- return buckets.map((bucket: Bucket) => ({
- alertsCount: bucket.doc_count,
- entityType,
- ...bucket.key,
- }));
- });
+ return buckets.map((bucket: Bucket) => ({
+ alertsCount: bucket.doc_count,
+ ...bucket.key,
+ }));
+ })
+ .flat();
return alerts;
}
diff --git a/x-pack/solutions/observability/plugins/inventory/server/routes/entities/route.ts b/x-pack/solutions/observability/plugins/inventory/server/routes/entities/route.ts
index 78d79105da6de..33899e8593795 100644
--- a/x-pack/solutions/observability/plugins/inventory/server/routes/entities/route.ts
+++ b/x-pack/solutions/observability/plugins/inventory/server/routes/entities/route.ts
@@ -4,40 +4,64 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-import { INVENTORY_APP_ID } from '@kbn/deeplinks-observability/constants';
import { jsonRt } from '@kbn/io-ts-utils';
-import { ENTITY_TYPE } from '@kbn/observability-shared-plugin/common';
-import { joinByKey } from '@kbn/observability-utils-common/array/join_by_key';
-import { createObservabilityEsClient } from '@kbn/observability-utils-server/es/client/create_observability_es_client';
import * as t from 'io-ts';
import { orderBy } from 'lodash';
-import type { InventoryEntity } from '../../../common/entities';
-import { entityColumnIdsRt } from '../../../common/entities';
-import { createAlertsClient } from '../../lib/create_alerts_client/create_alerts_client';
+import moment from 'moment';
+import { DATA_STREAM_TYPE } from '@kbn/dataset-quality-plugin/common/es_fields';
+import { joinByKey } from '@kbn/observability-utils-common/array/join_by_key';
+import { BUILT_IN_ENTITY_TYPES } from '@kbn/observability-shared-plugin/common';
+import {
+ type InventoryEntity,
+ entityColumnIdsRt,
+ MAX_NUMBER_OF_ENTITIES,
+} from '../../../common/entities';
import { createInventoryServerRoute } from '../create_inventory_server_route';
-import { getEntityGroupsBy } from './get_entity_groups';
-import { getEntityTypes } from './get_entity_types';
-import { getIdentityFieldsPerEntityType } from './get_identity_fields_per_entity_type';
-import { getLatestEntities } from './get_latest_entities';
+import { createAlertsClient } from '../../lib/create_alerts_client/create_alerts_client';
import { getLatestEntitiesAlerts } from './get_latest_entities_alerts';
export const getEntityTypesRoute = createInventoryServerRoute({
endpoint: 'GET /internal/inventory/entities/types',
+ params: t.partial({
+ query: t.partial({
+ includeEntityTypes: jsonRt.pipe(t.array(t.string)),
+ excludeEntityTypes: jsonRt.pipe(t.array(t.string)),
+ kuery: t.string,
+ }),
+ }),
security: {
authz: {
requiredPrivileges: ['inventory'],
},
},
- handler: async ({ context, logger }) => {
- const coreContext = await context.core;
- const inventoryEsClient = createObservabilityEsClient({
- client: coreContext.elasticsearch.client.asCurrentUser,
- logger,
- plugin: `@kbn/${INVENTORY_APP_ID}-plugin`,
+ handler: async ({ plugins, request, params }) => {
+ const entityManagerStart = await plugins.entityManager.start();
+
+ const entityManagerClient = await entityManagerStart.getScopedClient({ request });
+ const { includeEntityTypes, excludeEntityTypes, kuery } = params?.query ?? {};
+
+ const rawEntityTypes = await entityManagerClient.v2.readTypeDefinitions();
+ const hasIncludedEntityTypes = (includeEntityTypes ?? []).length > 0;
+ const entityTypes = rawEntityTypes.filter((entityType) =>
+ hasIncludedEntityTypes
+ ? includeEntityTypes?.includes(entityType.id)
+ : !excludeEntityTypes?.includes(entityType.id)
+ );
+ const entityCount = await entityManagerClient.v2.countEntities({
+ start: moment().subtract(15, 'm').toISOString(),
+ end: moment().toISOString(),
+ types: entityTypes.map((entityType) => entityType.id),
+ filters: kuery ? [kuery] : undefined,
});
- const entityTypes = await getEntityTypes({ inventoryEsClient });
- return { entityTypes };
+ const entityTypesWithCount = entityTypes
+ .map((entityType) => ({
+ ...entityType,
+ count: entityCount.types[entityType.id],
+ }))
+ .filter((entityType) => entityType.count > 0);
+
+ return { entityTypes: entityTypesWithCount, totalEntities: entityCount.total };
},
});
@@ -48,10 +72,10 @@ export const listLatestEntitiesRoute = createInventoryServerRoute({
t.type({
sortField: entityColumnIdsRt,
sortDirection: t.union([t.literal('asc'), t.literal('desc')]),
+ entityType: t.string,
}),
t.partial({
kuery: t.string,
- entityTypes: jsonRt.pipe(t.array(t.string)),
}),
]),
}),
@@ -62,42 +86,60 @@ export const listLatestEntitiesRoute = createInventoryServerRoute({
},
handler: async ({
params,
- context,
- logger,
plugins,
request,
+ logger,
+ context,
}): Promise<{ entities: InventoryEntity[] }> => {
- const coreContext = await context.core;
- const inventoryEsClient = createObservabilityEsClient({
- client: coreContext.elasticsearch.client.asCurrentUser,
- logger,
- plugin: `@kbn/${INVENTORY_APP_ID}-plugin`,
- });
+ const entityManagerStart = await plugins.entityManager.start();
+ const { client: clusterClient } = (await context.core).elasticsearch;
- const { sortDirection, sortField, kuery, entityTypes } = params.query;
+ const { sortDirection, sortField, kuery, entityType } = params.query;
- const [alertsClient, latestEntities] = await Promise.all([
+ const [entityManagerClient, alertsClient] = await Promise.all([
+ entityManagerStart.getScopedClient({ request }),
createAlertsClient({ plugins, request }),
- getLatestEntities({
- inventoryEsClient,
- sortDirection,
- sortField,
- kuery,
- entityTypes,
- }),
]);
- const identityFieldsPerEntityType = getIdentityFieldsPerEntityType(latestEntities);
+ const METADATA_BY_TYPE: { [key: string]: string[] } = {
+ default: [DATA_STREAM_TYPE],
+ [BUILT_IN_ENTITY_TYPES.CONTAINER_V2]: [DATA_STREAM_TYPE, 'cloud.provider'],
+ [BUILT_IN_ENTITY_TYPES.HOST_V2]: [DATA_STREAM_TYPE, 'cloud.provider'],
+ [BUILT_IN_ENTITY_TYPES.SERVICE_V2]: [DATA_STREAM_TYPE, 'agent.name'],
+ };
+
+ const [{ entities: rawEntities }, identityFieldsBySource] = await Promise.all([
+ entityManagerClient.v2.searchEntities({
+ start: moment().subtract(15, 'm').toISOString(),
+ end: moment().toISOString(),
+ limit: MAX_NUMBER_OF_ENTITIES,
+ type: entityType,
+ metadata_fields: METADATA_BY_TYPE[entityType] || METADATA_BY_TYPE.default,
+ filters: kuery ? [kuery] : [],
+ }),
+ entityManagerStart.v2.identityFieldsBySource(entityType, clusterClient, logger),
+ ]);
const alerts = await getLatestEntitiesAlerts({
- identityFieldsPerEntityType,
+ identityFieldsBySource,
alertsClient,
});
+ const entities: InventoryEntity[] = rawEntities.map((entity) => {
+ return {
+ entityId: entity['entity.id'],
+ entityType: entity['entity.type'],
+ entityDisplayName: entity['entity.display_name'],
+ entityLastSeenTimestamp: entity['entity.last_seen_timestamp'] as string,
+ entityIdentityFields: identityFieldsBySource,
+ ...entity,
+ };
+ });
+
const joined = joinByKey(
- [...latestEntities, ...alerts] as InventoryEntity[],
- [...identityFieldsPerEntityType.values()].flat()
- ).filter((latestEntity) => latestEntity.entityId);
+ [...entities, ...alerts] as InventoryEntity[],
+ [...Object.values(identityFieldsBySource)].flat()
+ ).filter((latestEntity: InventoryEntity) => latestEntity.entityId);
return {
entities:
@@ -112,50 +154,7 @@ export const listLatestEntitiesRoute = createInventoryServerRoute({
},
});
-export const groupEntitiesByRoute = createInventoryServerRoute({
- endpoint: 'GET /internal/inventory/entities/group_by/{field}',
- params: t.intersection([
- t.type({ path: t.type({ field: t.literal(ENTITY_TYPE) }) }),
- t.partial({
- query: t.partial({
- includeEntityTypes: jsonRt.pipe(t.array(t.string)),
- excludeEntityTypes: jsonRt.pipe(t.array(t.string)),
- kuery: t.string,
- }),
- }),
- ]),
- security: {
- authz: {
- requiredPrivileges: ['inventory'],
- },
- },
- handler: async ({ params, context, logger }) => {
- const coreContext = await context.core;
- const inventoryEsClient = createObservabilityEsClient({
- client: coreContext.elasticsearch.client.asCurrentUser,
- logger,
- plugin: `@kbn/${INVENTORY_APP_ID}-plugin`,
- });
-
- const { field } = params.path;
- const { kuery, includeEntityTypes, excludeEntityTypes } = params.query ?? {};
-
- const groups = await getEntityGroupsBy({
- inventoryEsClient,
- field,
- kuery,
- includeEntityTypes,
- excludeEntityTypes,
- });
-
- const entitiesCount = groups.reduce((acc, group) => acc + group.count, 0);
-
- return { groupBy: field, groups, entitiesCount };
- },
-});
-
export const entitiesRoutes = {
...listLatestEntitiesRoute,
...getEntityTypesRoute,
- ...groupEntitiesByRoute,
};
diff --git a/x-pack/solutions/observability/plugins/inventory/server/routes/has_data/get_has_data.ts b/x-pack/solutions/observability/plugins/inventory/server/routes/has_data/get_has_data.ts
deleted file mode 100644
index c3fd3971f09d2..0000000000000
--- a/x-pack/solutions/observability/plugins/inventory/server/routes/has_data/get_has_data.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-import type { Logger } from '@kbn/core/server';
-import { type ObservabilityElasticsearchClient } from '@kbn/observability-utils-server/es/client/create_observability_es_client';
-import { getBuiltinEntityDefinitionIdESQLWhereClause } from '../entities/query_helper';
-import { ENTITIES_LATEST_ALIAS } from '../../../common/entities';
-
-export async function getHasData({
- inventoryEsClient,
- logger,
-}: {
- inventoryEsClient: ObservabilityElasticsearchClient;
- logger: Logger;
-}) {
- try {
- const esqlResults = await inventoryEsClient.esql<{ _count: number }, { transform: 'plain' }>(
- 'get_has_data',
- {
- query: `FROM ${ENTITIES_LATEST_ALIAS}
- | ${getBuiltinEntityDefinitionIdESQLWhereClause()}
- | STATS _count = COUNT(*)
- | LIMIT 1`,
- },
- { transform: 'plain' }
- );
-
- const totalCount = esqlResults.hits[0]._count;
-
- return { hasData: totalCount > 0 };
- } catch (e) {
- logger.error(e);
- return { hasData: false };
- }
-}
diff --git a/x-pack/solutions/observability/plugins/inventory/server/routes/has_data/route.ts b/x-pack/solutions/observability/plugins/inventory/server/routes/has_data/route.ts
index 4f4e4e81f6225..b8ed2aa19ee09 100644
--- a/x-pack/solutions/observability/plugins/inventory/server/routes/has_data/route.ts
+++ b/x-pack/solutions/observability/plugins/inventory/server/routes/has_data/route.ts
@@ -4,10 +4,8 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-import { createObservabilityEsClient } from '@kbn/observability-utils-server/es/client/create_observability_es_client';
-import { INVENTORY_APP_ID } from '@kbn/deeplinks-observability/constants';
+import moment from 'moment';
import { createInventoryServerRoute } from '../create_inventory_server_route';
-import { getHasData } from './get_has_data';
export const hasDataRoute = createInventoryServerRoute({
endpoint: 'GET /internal/inventory/has_data',
@@ -16,18 +14,16 @@ export const hasDataRoute = createInventoryServerRoute({
requiredPrivileges: ['inventory'],
},
},
- handler: async ({ context, logger }) => {
- const coreContext = await context.core;
- const inventoryEsClient = createObservabilityEsClient({
- client: coreContext.elasticsearch.client.asCurrentUser,
- logger,
- plugin: `@kbn/${INVENTORY_APP_ID}-plugin`,
- });
+ handler: async ({ plugins, request }) => {
+ const entityManagerStart = await plugins.entityManager.start();
+ const entityManagerClient = await entityManagerStart.getScopedClient({ request });
- return getHasData({
- inventoryEsClient,
- logger,
+ const { total } = await entityManagerClient.v2.countEntities({
+ start: moment().subtract(15, 'm').toISOString(),
+ end: moment().toISOString(),
});
+
+ return { hasData: total > 0 };
},
});
diff --git a/x-pack/solutions/observability/plugins/inventory/tsconfig.json b/x-pack/solutions/observability/plugins/inventory/tsconfig.json
index 555d0d44b03dc..9305282d47d43 100644
--- a/x-pack/solutions/observability/plugins/inventory/tsconfig.json
+++ b/x-pack/solutions/observability/plugins/inventory/tsconfig.json
@@ -52,15 +52,13 @@
"@kbn/spaces-plugin",
"@kbn/cloud-plugin",
"@kbn/observability-utils-browser",
- "@kbn/observability-utils-server",
- "@kbn/observability-utils-common",
"@kbn/storybook",
"@kbn/dashboard-plugin",
"@kbn/deeplinks-analytics",
"@kbn/react-hooks",
- "@kbn/observability-utils-common",
"@kbn/observability-utils-browser",
- "@kbn/observability-utils-server",
- "@kbn/kibana-utils-plugin"
+ "@kbn/kibana-utils-plugin",
+ "@kbn/dataset-quality-plugin",
+ "@kbn/observability-utils-common",
]
}
diff --git a/x-pack/solutions/observability/plugins/observability_shared/common/entity/entity_types.ts b/x-pack/solutions/observability/plugins/observability_shared/common/entity/entity_types.ts
index 222780a1fc31a..77832801dcd80 100644
--- a/x-pack/solutions/observability/plugins/observability_shared/common/entity/entity_types.ts
+++ b/x-pack/solutions/observability/plugins/observability_shared/common/entity/entity_types.ts
@@ -10,11 +10,30 @@ const createKubernetesEntity = (base: T) => ({
semconv: `k8s.${base}.semconv` as const,
});
+const createKubernetesV2Entity = (base: T) => ({
+ ecs: `built_in_kubernetes_${base}_ecs` as const,
+ semconv: `built_in_kubernetes_${base}_semconv` as const,
+});
+
export const BUILT_IN_ENTITY_TYPES = {
+ HOST_V2: 'built_in_hosts_from_ecs_data',
+ CONTAINER_V2: 'built_in_containers_from_ecs_data',
+ SERVICE_V2: 'built_in_services_from_ecs_data',
+ KUBERNETES_V2: {
+ CLUSTER: createKubernetesV2Entity('cluster'),
+ CRON_JOB: createKubernetesV2Entity('cron_job'),
+ DAEMON_SET: createKubernetesV2Entity('daemon_set'),
+ DEPLOYMENT: createKubernetesV2Entity('deployment'),
+ JOB: createKubernetesV2Entity('job'),
+ NODE: createKubernetesV2Entity('node'),
+ POD: createKubernetesV2Entity('pod'),
+ REPLICA_SET: createKubernetesV2Entity('replica_set'),
+ STATEFUL_SET: createKubernetesV2Entity('stateful_set'),
+ SERVICE: 'built_in_kubernetes_service_ecs',
+ },
HOST: 'host',
CONTAINER: 'container',
SERVICE: 'service',
- SERVICE_V2: 'built_in_services_from_ecs_data',
KUBERNETES: {
CLUSTER: createKubernetesEntity('cluster'),
CONTAINER: createKubernetesEntity('container'),
diff --git a/x-pack/solutions/observability/plugins/profiling/server/routes/apm.ts b/x-pack/solutions/observability/plugins/profiling/server/routes/apm.ts
index 9dc85ac8969b8..f795bd25295bc 100644
--- a/x-pack/solutions/observability/plugins/profiling/server/routes/apm.ts
+++ b/x-pack/solutions/observability/plugins/profiling/server/routes/apm.ts
@@ -85,7 +85,7 @@ export function registerTopNFunctionsAPMTransactionsRoute({
],
},
},
- aggregationField: 'transaction.name',
+ aggregationFields: ['transaction.name'],
indices: transactionIndices.split(','),
stacktraceIdsField: 'transaction.profiler_stack_trace_ids',
limit: 1000,
diff --git a/x-pack/solutions/observability/plugins/profiling/server/routes/functions.ts b/x-pack/solutions/observability/plugins/profiling/server/routes/functions.ts
index 9acd9026b0893..cccb9430e3bc1 100644
--- a/x-pack/solutions/observability/plugins/profiling/server/routes/functions.ts
+++ b/x-pack/solutions/observability/plugins/profiling/server/routes/functions.ts
@@ -89,7 +89,7 @@ export function registerTopNFunctionsSearchRoute({
core,
esClient,
query,
- aggregationField: 'service.name',
+ aggregationFields: ['service.name'],
totalSeconds,
});
diff --git a/x-pack/solutions/observability/plugins/profiling_data_access/common/profiling_es_client.ts b/x-pack/solutions/observability/plugins/profiling_data_access/common/profiling_es_client.ts
index 639b5ac5174cd..c5cf84389f951 100644
--- a/x-pack/solutions/observability/plugins/profiling_data_access/common/profiling_es_client.ts
+++ b/x-pack/solutions/observability/plugins/profiling_data_access/common/profiling_es_client.ts
@@ -57,7 +57,7 @@ export interface ProfilingESClient {
sampleSize?: number;
indices?: string[];
stacktraceIdsField?: string;
- aggregationField?: AggregationField;
+ aggregationFields?: AggregationField[];
co2PerKWH?: number;
datacenterPUE?: number;
pervCPUWattX86?: number;
diff --git a/x-pack/solutions/observability/plugins/profiling_data_access/server/services/functions/es_functions.ts b/x-pack/solutions/observability/plugins/profiling_data_access/server/services/functions/es_functions.ts
index abda08a1a6a62..9c712b4283052 100644
--- a/x-pack/solutions/observability/plugins/profiling_data_access/server/services/functions/es_functions.ts
+++ b/x-pack/solutions/observability/plugins/profiling_data_access/server/services/functions/es_functions.ts
@@ -27,7 +27,7 @@ export interface FetchFunctionsParams {
indices?: string[];
stacktraceIdsField?: string;
query: QueryDslQueryContainer;
- aggregationField?: AggregationField;
+ aggregationFields?: AggregationField[];
limit?: number;
totalSeconds: number;
}
@@ -41,7 +41,7 @@ export function createFetchESFunctions({ createProfilingEsClient }: RegisterServ
indices,
stacktraceIdsField,
query,
- aggregationField,
+ aggregationFields,
limit,
totalSeconds,
}: FetchFunctionsParams) => {
@@ -72,7 +72,7 @@ export function createFetchESFunctions({ createProfilingEsClient }: RegisterServ
query,
indices,
stacktraceIdsField,
- aggregationField,
+ aggregationFields,
co2PerKWH,
datacenterPUE,
pervCPUWattX86,
diff --git a/x-pack/solutions/observability/plugins/profiling_data_access/server/utils/create_profiling_es_client.ts b/x-pack/solutions/observability/plugins/profiling_data_access/server/utils/create_profiling_es_client.ts
index 5e595a3ce0eb0..0f31f78e741ae 100644
--- a/x-pack/solutions/observability/plugins/profiling_data_access/server/utils/create_profiling_es_client.ts
+++ b/x-pack/solutions/observability/plugins/profiling_data_access/server/utils/create_profiling_es_client.ts
@@ -153,7 +153,7 @@ export function createProfilingEsClient({
},
topNFunctions({
query,
- aggregationField,
+ aggregationFields,
indices,
stacktraceIdsField,
co2PerKWH,
@@ -180,7 +180,7 @@ export function createProfilingEsClient({
limit,
indices,
stacktrace_ids_field: stacktraceIdsField,
- aggregation_field: aggregationField,
+ aggregation_fields: aggregationFields,
co2_per_kwh: co2PerKWH,
per_core_watt_x86: pervCPUWattX86,
per_core_watt_arm64: pervCPUWattArm64,
diff --git a/x-pack/solutions/observability/plugins/slo/server/routes/slo/route.ts b/x-pack/solutions/observability/plugins/slo/server/routes/slo/route.ts
index a7589de5d0909..b93076ace737f 100644
--- a/x-pack/solutions/observability/plugins/slo/server/routes/slo/route.ts
+++ b/x-pack/solutions/observability/plugins/slo/server/routes/slo/route.ts
@@ -57,7 +57,7 @@ import { ManageSLO } from '../../services/manage_slo';
import { ResetSLO } from '../../services/reset_slo';
import { SloDefinitionClient } from '../../services/slo_definition_client';
import { getSloSettings, storeSloSettings } from '../../services/slo_settings';
-import { DefaultSummarySearchClient } from '../../services/summary_search_client';
+import { DefaultSummarySearchClient } from '../../services/summary_search_client/summary_search_client';
import { DefaultSummaryTransformGenerator } from '../../services/summary_transform_generator/summary_transform_generator';
import { createTransformGenerators } from '../../services/transform_generators';
import { createSloServerRoute } from '../create_slo_server_route';
diff --git a/x-pack/solutions/observability/plugins/slo/server/services/find_slo.test.ts b/x-pack/solutions/observability/plugins/slo/server/services/find_slo.test.ts
index bb26ab235c9f4..b4e3656bb8e39 100644
--- a/x-pack/solutions/observability/plugins/slo/server/services/find_slo.test.ts
+++ b/x-pack/solutions/observability/plugins/slo/server/services/find_slo.test.ts
@@ -12,7 +12,7 @@ import { FindSLO } from './find_slo';
import { createSLO } from './fixtures/slo';
import { createSLORepositoryMock, createSummarySearchClientMock } from './mocks';
import { SLORepository } from './slo_repository';
-import { SummaryResult, SummarySearchClient } from './summary_search_client';
+import type { SummaryResult, SummarySearchClient } from './summary_search_client/types';
describe('FindSLO', () => {
let mockRepository: jest.Mocked;
@@ -151,16 +151,27 @@ describe('FindSLO', () => {
});
describe('validation', () => {
- it("throws an error when 'perPage > 5000'", async () => {
+ beforeEach(() => {
const slo = createSLO();
mockSummarySearchClient.search.mockResolvedValueOnce(summarySearchResult(slo));
mockRepository.findAllByIds.mockResolvedValueOnce([slo]);
+ });
+ it("throws an error when 'perPage' > 5000", async () => {
await expect(findSLO.execute({ perPage: '5000' })).resolves.not.toThrow();
await expect(findSLO.execute({ perPage: '5001' })).rejects.toThrowError(
'perPage limit set to 5000'
);
});
+
+ describe('Cursor Pagination', () => {
+ it("throws an error when 'size' > 5000", async () => {
+ await expect(findSLO.execute({ size: '5000' })).resolves.not.toThrow();
+ await expect(findSLO.execute({ size: '5001' })).rejects.toThrowError(
+ 'size limit set to 5000'
+ );
+ });
+ });
});
});
diff --git a/x-pack/solutions/observability/plugins/slo/server/services/find_slo.ts b/x-pack/solutions/observability/plugins/slo/server/services/find_slo.ts
index dcd7fe44d0783..4c240dae77af7 100644
--- a/x-pack/solutions/observability/plugins/slo/server/services/find_slo.ts
+++ b/x-pack/solutions/observability/plugins/slo/server/services/find_slo.ts
@@ -5,16 +5,22 @@
* 2.0.
*/
-import { FindSLOParams, FindSLOResponse, findSLOResponseSchema, Pagination } from '@kbn/slo-schema';
+import { FindSLOParams, FindSLOResponse, findSLOResponseSchema } from '@kbn/slo-schema';
import { keyBy } from 'lodash';
import { SLODefinition } from '../domain/models';
import { IllegalArgumentError } from '../errors';
import { SLORepository } from './slo_repository';
-import { Sort, SummaryResult, SummarySearchClient } from './summary_search_client';
+import type {
+ Pagination,
+ Sort,
+ SummaryResult,
+ SummarySearchClient,
+} from './summary_search_client/types';
const DEFAULT_PAGE = 1;
const DEFAULT_PER_PAGE = 25;
-const MAX_PER_PAGE = 5000;
+const DEFAULT_SIZE = 100;
+const MAX_PER_PAGE_OR_SIZE = 5000;
export class FindSLO {
constructor(
@@ -38,8 +44,10 @@ export class FindSLO {
);
return findSLOResponseSchema.encode({
- page: summaryResults.page,
- perPage: summaryResults.perPage,
+ page: 'page' in summaryResults ? summaryResults.page : DEFAULT_PAGE,
+ perPage: 'perPage' in summaryResults ? summaryResults.perPage : DEFAULT_PER_PAGE,
+ size: 'size' in summaryResults ? summaryResults.size : undefined,
+ searchAfter: 'searchAfter' in summaryResults ? summaryResults.searchAfter : undefined,
total: summaryResults.total,
results: mergeSloWithSummary(localSloDefinitions, summaryResults.results),
});
@@ -78,16 +86,29 @@ function mergeSloWithSummary(
}
function toPagination(params: FindSLOParams): Pagination {
+ const isCursorBased = !!params.searchAfter || !!params.size;
+
+ if (isCursorBased) {
+ const size = Number(params.size);
+ if (!isNaN(size) && size > MAX_PER_PAGE_OR_SIZE) {
+ throw new IllegalArgumentError('size limit set to 5000');
+ }
+
+ return {
+ searchAfter: params.searchAfter,
+ size: !isNaN(size) && size > 0 ? size : DEFAULT_SIZE,
+ };
+ }
+
const page = Number(params.page);
const perPage = Number(params.perPage);
-
- if (!isNaN(perPage) && perPage > MAX_PER_PAGE) {
- throw new IllegalArgumentError(`perPage limit set to ${MAX_PER_PAGE}`);
+ if (!isNaN(perPage) && perPage > MAX_PER_PAGE_OR_SIZE) {
+ throw new IllegalArgumentError('perPage limit set to 5000');
}
return {
page: !isNaN(page) && page >= 1 ? page : DEFAULT_PAGE,
- perPage: !isNaN(perPage) && perPage >= 0 ? perPage : DEFAULT_PER_PAGE,
+ perPage: !isNaN(perPage) && perPage > 0 ? perPage : DEFAULT_PER_PAGE,
};
}
diff --git a/x-pack/solutions/observability/plugins/slo/server/services/fixtures/summary_search_document.ts b/x-pack/solutions/observability/plugins/slo/server/services/fixtures/summary_search_document.ts
index 8837e567c97cc..1f59a964a3e1b 100644
--- a/x-pack/solutions/observability/plugins/slo/server/services/fixtures/summary_search_document.ts
+++ b/x-pack/solutions/observability/plugins/slo/server/services/fixtures/summary_search_document.ts
@@ -25,7 +25,7 @@ export const aSummaryDocument = (
export const aHitFromSummaryIndex = (_source: any) => {
return {
- _index: '.slo-observability.summary-v2',
+ _index: '.slo-observability.summary-v3.3',
_id: uuidv4(),
_score: 1,
_source,
@@ -34,7 +34,7 @@ export const aHitFromSummaryIndex = (_source: any) => {
export const aHitFromTempSummaryIndex = (_source: any) => {
return {
- _index: '.slo-observability.summary-v2.temp',
+ _index: '.slo-observability.summary-v3.3.temp',
_id: uuidv4(),
_score: 1,
_source,
diff --git a/x-pack/solutions/observability/plugins/slo/server/services/index.ts b/x-pack/solutions/observability/plugins/slo/server/services/index.ts
index 4688a34740c63..c229226a290d2 100644
--- a/x-pack/solutions/observability/plugins/slo/server/services/index.ts
+++ b/x-pack/solutions/observability/plugins/slo/server/services/index.ts
@@ -22,3 +22,4 @@ export * from './summary_client';
export * from './get_slo_groupings';
export * from './find_slo_groups';
export * from './get_slo_health';
+export * from './summary_search_client/summary_search_client';
diff --git a/x-pack/solutions/observability/plugins/slo/server/services/mocks/index.ts b/x-pack/solutions/observability/plugins/slo/server/services/mocks/index.ts
index ab8230cfec463..c6fa4a3d949f3 100644
--- a/x-pack/solutions/observability/plugins/slo/server/services/mocks/index.ts
+++ b/x-pack/solutions/observability/plugins/slo/server/services/mocks/index.ts
@@ -9,7 +9,7 @@ import { ResourceInstaller } from '../resource_installer';
import { BurnRatesClient } from '../burn_rates_client';
import { SLORepository } from '../slo_repository';
import { SummaryClient } from '../summary_client';
-import { SummarySearchClient } from '../summary_search_client';
+import { SummarySearchClient } from '../summary_search_client/types';
import { TransformManager } from '../transform_manager';
const createResourceInstallerMock = (): jest.Mocked => {
diff --git a/x-pack/solutions/observability/plugins/slo/server/services/__snapshots__/summary_search_client.test.ts.snap b/x-pack/solutions/observability/plugins/slo/server/services/summary_search_client/__snapshots__/summary_search_client.test.ts.snap
similarity index 100%
rename from x-pack/solutions/observability/plugins/slo/server/services/__snapshots__/summary_search_client.test.ts.snap
rename to x-pack/solutions/observability/plugins/slo/server/services/summary_search_client/__snapshots__/summary_search_client.test.ts.snap
diff --git a/x-pack/solutions/observability/plugins/slo/server/services/summary_search_client.test.ts b/x-pack/solutions/observability/plugins/slo/server/services/summary_search_client/summary_search_client.test.ts
similarity index 90%
rename from x-pack/solutions/observability/plugins/slo/server/services/summary_search_client.test.ts
rename to x-pack/solutions/observability/plugins/slo/server/services/summary_search_client/summary_search_client.test.ts
index a522bd287d045..d1fd1ffe0ce25 100644
--- a/x-pack/solutions/observability/plugins/slo/server/services/summary_search_client.test.ts
+++ b/x-pack/solutions/observability/plugins/slo/server/services/summary_search_client/summary_search_client.test.ts
@@ -8,13 +8,14 @@
import { ElasticsearchClientMock, elasticsearchServiceMock } from '@kbn/core/server/mocks';
import { loggerMock } from '@kbn/logging-mocks';
import { Pagination } from '@kbn/slo-schema/src/models/pagination';
-import { createSLO } from './fixtures/slo';
+import { createSLO } from '../fixtures/slo';
import {
aHitFromSummaryIndex,
aHitFromTempSummaryIndex,
aSummaryDocument,
-} from './fixtures/summary_search_document';
-import { DefaultSummarySearchClient, Sort, SummarySearchClient } from './summary_search_client';
+} from '../fixtures/summary_search_document';
+import { DefaultSummarySearchClient } from './summary_search_client';
+import type { Sort, SummarySearchClient } from './types';
const defaultSort: Sort = {
field: 'sli_value',
@@ -169,6 +170,12 @@ describe('Summary Search Client', () => {
sliValue: {
order: 'asc',
},
+ 'slo.id': {
+ order: 'asc',
+ },
+ 'slo.instanceId': {
+ order: 'asc',
+ },
},
track_total_hits: true,
},
@@ -202,6 +209,12 @@ describe('Summary Search Client', () => {
sliValue: {
order: 'asc',
},
+ 'slo.id': {
+ order: 'asc',
+ },
+ 'slo.instanceId': {
+ order: 'asc',
+ },
},
track_total_hits: true,
},
@@ -229,7 +242,16 @@ describe('Summary Search Client', () => {
},
},
size: 40,
- sort: { isTempDoc: { order: 'asc' }, sliValue: { order: 'asc' } },
+ sort: {
+ isTempDoc: { order: 'asc' },
+ sliValue: { order: 'asc' },
+ 'slo.id': {
+ order: 'asc',
+ },
+ 'slo.instanceId': {
+ order: 'asc',
+ },
+ },
track_total_hits: true,
},
]);
diff --git a/x-pack/solutions/observability/plugins/slo/server/services/summary_search_client.ts b/x-pack/solutions/observability/plugins/slo/server/services/summary_search_client/summary_search_client.ts
similarity index 76%
rename from x-pack/solutions/observability/plugins/slo/server/services/summary_search_client.ts
rename to x-pack/solutions/observability/plugins/slo/server/services/summary_search_client/summary_search_client.ts
index c4f0de5b38b25..5c1c0e9e780cb 100644
--- a/x-pack/solutions/observability/plugins/slo/server/services/summary_search_client.ts
+++ b/x-pack/solutions/observability/plugins/slo/server/services/summary_search_client/summary_search_client.ts
@@ -5,58 +5,30 @@
* 2.0.
*/
-import { SearchTotalHits } from '@elastic/elasticsearch/lib/api/types';
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { ElasticsearchClient, Logger, SavedObjectsClientContract } from '@kbn/core/server';
import { isCCSRemoteIndexName } from '@kbn/es-query';
-import { ALL_VALUE, Paginated, Pagination } from '@kbn/slo-schema';
+import { ALL_VALUE } from '@kbn/slo-schema';
import { assertNever } from '@kbn/std';
import { partition } from 'lodash';
-import { SLO_SUMMARY_DESTINATION_INDEX_PATTERN } from '../../common/constants';
-import { Groupings, SLODefinition, SLOId, StoredSLOSettings, Summary } from '../domain/models';
-import { toHighPrecision } from '../utils/number';
-import { createEsParams, typedSearch } from '../utils/queries';
-import { getListOfSummaryIndices, getSloSettings } from './slo_settings';
-import { EsSummaryDocument } from './summary_transform_generator/helpers/create_temp_summary';
-import { getElasticsearchQueryOrThrow, parseStringFilters } from './transform_generators';
-import { fromRemoteSummaryDocumentToSloDefinition } from './unsafe_federated/remote_summary_doc_to_slo';
-import { getFlattenedGroupings } from './utils';
-
-export interface SummaryResult {
- sloId: SLOId;
- instanceId: string;
- summary: Summary;
- groupings: Groupings;
- remote?: {
- kibanaUrl: string;
- remoteName: string;
- slo: SLODefinition;
- };
-}
-
-type SortField =
- | 'error_budget_consumed'
- | 'error_budget_remaining'
- | 'sli_value'
- | 'status'
- | 'burn_rate_5m'
- | 'burn_rate_1h'
- | 'burn_rate_1d';
-
-export interface Sort {
- field: SortField;
- direction: 'asc' | 'desc';
-}
-
-export interface SummarySearchClient {
- search(
- kqlQuery: string,
- filters: string,
- sort: Sort,
- pagination: Pagination,
- hideStale?: boolean
- ): Promise>;
-}
+import { SLO_SUMMARY_DESTINATION_INDEX_PATTERN } from '../../../common/constants';
+import { StoredSLOSettings } from '../../domain/models';
+import { toHighPrecision } from '../../utils/number';
+import { createEsParams, typedSearch } from '../../utils/queries';
+import { getListOfSummaryIndices, getSloSettings } from '../slo_settings';
+import { EsSummaryDocument } from '../summary_transform_generator/helpers/create_temp_summary';
+import { getElasticsearchQueryOrThrow, parseStringFilters } from '../transform_generators';
+import { fromRemoteSummaryDocumentToSloDefinition } from '../unsafe_federated/remote_summary_doc_to_slo';
+import { getFlattenedGroupings } from '../utils';
+import type {
+ Paginated,
+ Pagination,
+ Sort,
+ SortField,
+ SummaryResult,
+ SummarySearchClient,
+} from './types';
+import { isCursorPagination } from './types';
export class DefaultSummarySearchClient implements SummarySearchClient {
constructor(
@@ -76,6 +48,7 @@ export class DefaultSummarySearchClient implements SummarySearchClient {
const parsedFilters = parseStringFilters(filters, this.logger);
const settings = await getSloSettings(this.soClient);
const { indices } = await getListOfSummaryIndices(this.esClient, settings);
+
const esParams = createEsParams({
index: indices,
track_total_hits: true,
@@ -98,9 +71,14 @@ export class DefaultSummarySearchClient implements SummarySearchClient {
[toDocumentSortField(sort.field)]: {
order: sort.direction,
},
+ 'slo.id': {
+ order: 'asc',
+ },
+ 'slo.instanceId': {
+ order: 'asc',
+ },
},
- from: (pagination.page - 1) * pagination.perPage,
- size: pagination.perPage * 2, // twice as much as we return, in case they are all duplicate temp/non-temp summary
+ ...toPaginationQuery(pagination),
});
try {
@@ -109,9 +87,9 @@ export class DefaultSummarySearchClient implements SummarySearchClient {
esParams
);
- const total = (summarySearch.hits.total as SearchTotalHits).value ?? 0;
+ const total = summarySearch.hits.total.value ?? 0;
if (total === 0) {
- return { total: 0, perPage: pagination.perPage, page: pagination.page, results: [] };
+ return { total: 0, ...pagination, results: [] };
}
const [tempSummaryDocuments, summaryDocuments] = partition(
@@ -129,12 +107,16 @@ export class DefaultSummarySearchClient implements SummarySearchClient {
const finalResults = summaryDocuments
.concat(tempSummaryDocumentsDeduped)
- .slice(0, pagination.perPage);
+ .slice(0, isCursorPagination(pagination) ? pagination.size : pagination.perPage);
const finalTotal = total - (tempSummaryDocuments.length - tempSummaryDocumentsDeduped.length);
+ const paginationResults = isCursorPagination(pagination)
+ ? { searchAfter: finalResults[finalResults.length - 1].sort, size: pagination.size }
+ : pagination;
+
return {
- ...pagination,
+ ...paginationResults,
total: finalTotal,
results: finalResults.map((doc) => {
const summaryDoc = doc._source;
@@ -179,7 +161,7 @@ export class DefaultSummarySearchClient implements SummarySearchClient {
};
} catch (err) {
this.logger.error(`Error while searching SLO summary documents. ${err}`);
- return { total: 0, perPage: pagination.perPage, page: pagination.page, results: [] };
+ return { total: 0, ...pagination, results: [] };
}
}
@@ -251,3 +233,19 @@ function toDocumentSortField(field: SortField) {
assertNever(field);
}
}
+
+function toPaginationQuery(
+ pagination: Pagination
+): { size: number; search_after?: Array } | { size: number; from: number } {
+ if (isCursorPagination(pagination)) {
+ return {
+ size: pagination.size * 2, // Potential duplicates between temp and non-temp summaries
+ search_after: pagination.searchAfter,
+ };
+ }
+
+ return {
+ size: pagination.perPage * 2, // Potential duplicates between temp and non-temp summaries
+ from: (pagination.page - 1) * pagination.perPage,
+ };
+}
diff --git a/x-pack/solutions/observability/plugins/slo/server/services/summary_search_client/types.ts b/x-pack/solutions/observability/plugins/slo/server/services/summary_search_client/types.ts
new file mode 100644
index 0000000000000..098184de99077
--- /dev/null
+++ b/x-pack/solutions/observability/plugins/slo/server/services/summary_search_client/types.ts
@@ -0,0 +1,90 @@
+/*
+ * 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 { Groupings, SLODefinition, SLOId, Summary } from '../../domain/models';
+
+interface SummaryResult {
+ sloId: SLOId;
+ instanceId: string;
+ summary: Summary;
+ groupings: Groupings;
+ remote?: {
+ kibanaUrl: string;
+ remoteName: string;
+ slo: SLODefinition;
+ };
+}
+
+type SortField =
+ | 'error_budget_consumed'
+ | 'error_budget_remaining'
+ | 'sli_value'
+ | 'status'
+ | 'burn_rate_5m'
+ | 'burn_rate_1h'
+ | 'burn_rate_1d';
+
+interface Sort {
+ field: SortField;
+ direction: 'asc' | 'desc';
+}
+
+type Pagination = CursorPagination | OffsetPagination;
+
+interface CursorPagination {
+ searchAfter?: Array;
+ size: number;
+}
+
+function isCursorPagination(pagination: Pagination): pagination is CursorPagination {
+ return (pagination as CursorPagination).size !== undefined;
+}
+
+interface OffsetPagination {
+ page: number;
+ perPage: number;
+}
+
+type Paginated = CursorPaginated | OffsetPaginated;
+
+interface CursorPaginated {
+ total: number;
+ searchAfter?: Array;
+ size: number;
+ results: T[];
+}
+
+interface OffsetPaginated {
+ total: number;
+ page: number;
+ perPage: number;
+ results: T[];
+}
+
+interface SummarySearchClient {
+ search(
+ kqlQuery: string,
+ filters: string,
+ sort: Sort,
+ pagination: Pagination,
+ hideStale?: boolean
+ ): Promise>;
+}
+
+export type {
+ SummaryResult,
+ SortField,
+ Sort,
+ Pagination,
+ CursorPagination,
+ OffsetPagination as PagePagination,
+ Paginated,
+ CursorPaginated,
+ OffsetPaginated as PagePaginated,
+ SummarySearchClient,
+};
+export { isCursorPagination };
diff --git a/x-pack/solutions/observability/plugins/slo/server/services/transform_generators/common.ts b/x-pack/solutions/observability/plugins/slo/server/services/transform_generators/common.ts
index 4958a65a17a15..a663b24780953 100644
--- a/x-pack/solutions/observability/plugins/slo/server/services/transform_generators/common.ts
+++ b/x-pack/solutions/observability/plugins/slo/server/services/transform_generators/common.ts
@@ -40,9 +40,8 @@ export function parseStringFilters(filters: string, logger: Logger) {
return JSON.parse(filters);
} catch (e) {
logger.info(`Failed to parse filters: ${e}`);
+ return {};
}
-
- return {};
}
export function parseIndex(index: string): string | string[] {
diff --git a/x-pack/solutions/observability/plugins/streams/server/lib/streams/errors/index.ts b/x-pack/solutions/observability/plugins/streams/server/lib/streams/errors/index.ts
index 73842ef3018fe..605465d7bcac7 100644
--- a/x-pack/solutions/observability/plugins/streams/server/lib/streams/errors/index.ts
+++ b/x-pack/solutions/observability/plugins/streams/server/lib/streams/errors/index.ts
@@ -13,3 +13,4 @@ export * from './security_exception';
export * from './index_template_not_found';
export * from './fork_condition_missing';
export * from './component_template_not_found';
+export * from './root_stream_immutability_exception';
diff --git a/x-pack/solutions/observability/plugins/streams/server/lib/streams/errors/root_stream_immutability_exception.ts b/x-pack/solutions/observability/plugins/streams/server/lib/streams/errors/root_stream_immutability_exception.ts
new file mode 100644
index 0000000000000..4b1573f0ff01b
--- /dev/null
+++ b/x-pack/solutions/observability/plugins/streams/server/lib/streams/errors/root_stream_immutability_exception.ts
@@ -0,0 +1,13 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export class RootStreamImmutabilityException extends Error {
+ constructor(message: string) {
+ super(message);
+ this.name = 'RootStreamImmutabilityException';
+ }
+}
diff --git a/x-pack/solutions/observability/plugins/streams/server/routes/streams/edit.ts b/x-pack/solutions/observability/plugins/streams/server/routes/streams/edit.ts
index 1ed24898f2d03..2701e44a8d5c1 100644
--- a/x-pack/solutions/observability/plugins/streams/server/routes/streams/edit.ts
+++ b/x-pack/solutions/observability/plugins/streams/server/routes/streams/edit.ts
@@ -10,6 +10,7 @@ import { IScopedClusterClient } from '@kbn/core-elasticsearch-server';
import { Logger } from '@kbn/logging';
import { badRequest, internal, notFound } from '@hapi/boom';
import {
+ isRootStream,
isWiredStream,
isWiredStreamConfig,
streamConfigDefinitionSchema,
@@ -17,10 +18,12 @@ import {
WiredStreamConfigDefinition,
WiredStreamDefinition,
} from '@kbn/streams-schema';
+import { isEqual } from 'lodash';
import {
DefinitionNotFound,
ForkConditionMissing,
IndexTemplateNotFound,
+ RootStreamImmutabilityException,
SecurityException,
} from '../../lib/streams/errors';
import { createServerRoute } from '../create_server_route';
@@ -71,7 +74,25 @@ export const editStreamRoute = createServerRoute({
return { acknowledged: true };
}
- await validateStreamChildren(scopedClusterClient, params.path.id, params.body.ingest.routing);
+ const currentStreamDefinition = (await readStream({
+ scopedClusterClient,
+ id: params.path.id,
+ })) as WiredStreamDefinition;
+
+ if (isRootStream(streamDefinition)) {
+ await validateRootStreamChanges(
+ scopedClusterClient,
+ currentStreamDefinition,
+ streamDefinition
+ );
+ }
+
+ await validateStreamChildren(
+ scopedClusterClient,
+ currentStreamDefinition,
+ params.body.ingest.routing
+ );
+
if (isWiredStreamConfig(params.body)) {
await validateAncestorFields(
scopedClusterClient,
@@ -148,7 +169,8 @@ export const editStreamRoute = createServerRoute({
if (
e instanceof SecurityException ||
e instanceof ForkConditionMissing ||
- e instanceof MalformedStreamId
+ e instanceof MalformedStreamId ||
+ e instanceof RootStreamImmutabilityException
) {
throw badRequest(e);
}
@@ -189,15 +211,11 @@ async function updateParentStream(
async function validateStreamChildren(
scopedClusterClient: IScopedClusterClient,
- id: string,
+ currentStreamDefinition: WiredStreamDefinition,
children: WiredStreamConfigDefinition['ingest']['routing']
) {
try {
- const oldDefinition = await readStream({
- scopedClusterClient,
- id,
- });
- const oldChildren = oldDefinition.stream.ingest.routing.map((child) => child.name);
+ const oldChildren = currentStreamDefinition.stream.ingest.routing.map((child) => child.name);
const newChildren = new Set(children.map((child) => child.name));
children.forEach((child) => {
validateCondition(child.condition);
@@ -214,3 +232,31 @@ async function validateStreamChildren(
}
}
}
+
+/*
+ * Changes to mappings (fields) and processing rules are not allowed on the root stream.
+ * Changes to routing rules are allowed.
+ */
+async function validateRootStreamChanges(
+ scopedClusterClient: IScopedClusterClient,
+ currentStreamDefinition: WiredStreamDefinition,
+ nextStreamDefinition: WiredStreamDefinition
+) {
+ const hasFieldChanges = !isEqual(
+ currentStreamDefinition.stream.ingest.wired.fields,
+ nextStreamDefinition.stream.ingest.wired.fields
+ );
+
+ if (hasFieldChanges) {
+ throw new RootStreamImmutabilityException('Root stream fields cannot be changed');
+ }
+
+ const hasProcessingChanges = !isEqual(
+ currentStreamDefinition.stream.ingest.processing,
+ nextStreamDefinition.stream.ingest.processing
+ );
+
+ if (hasProcessingChanges) {
+ throw new RootStreamImmutabilityException('Root stream processing rules cannot be changed');
+ }
+}
diff --git a/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_schema_editor/fields_table.tsx b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_schema_editor/fields_table.tsx
index 1b6d0131a6bc2..1f068d9b0ac16 100644
--- a/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_schema_editor/fields_table.tsx
+++ b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_schema_editor/fields_table.tsx
@@ -22,7 +22,7 @@ import type {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import useToggle from 'react-use/lib/useToggle';
-import { isWiredReadStream, ReadStreamDefinition } from '@kbn/streams-schema';
+import { isRootStream, isWiredReadStream, ReadStreamDefinition } from '@kbn/streams-schema';
import { FieldType } from './field_type';
import { FieldStatus } from './field_status';
import { FieldEntry, SchemaEditorEditingState } from './hooks/use_editing_state';
@@ -155,111 +155,113 @@ const FieldsTable = ({ definition, fields, editingState, unpromotingState }: Fie
const [visibleColumns, setVisibleColumns] = useState(Object.keys(COLUMNS));
const trailingColumns = useMemo(() => {
- return [
- {
- id: 'actions',
- width: 40,
- headerCellRender: () => null,
- rowCellRender: ({ rowIndex }) => {
- const field = fields[rowIndex];
+ return !isRootStream(definition)
+ ? ([
+ {
+ id: 'actions',
+ width: 40,
+ headerCellRender: () => null,
+ rowCellRender: ({ rowIndex }) => {
+ const field = fields[rowIndex];
- let actions: ActionsCellActionsDescriptor[] = [];
+ let actions: ActionsCellActionsDescriptor[] = [];
- switch (field.status) {
- case 'mapped':
- actions = [
- {
- name: i18n.translate('xpack.streams.actions.viewFieldLabel', {
- defaultMessage: 'View field',
- }),
- disabled: editingState.isSaving,
- onClick: (fieldEntry: FieldEntry) => {
- editingState.selectField(fieldEntry, false);
- },
- },
- {
- name: i18n.translate('xpack.streams.actions.editFieldLabel', {
- defaultMessage: 'Edit field',
- }),
- disabled: editingState.isSaving,
- onClick: (fieldEntry: FieldEntry) => {
- editingState.selectField(fieldEntry, true);
- },
- },
- {
- name: i18n.translate('xpack.streams.actions.unpromoteFieldLabel', {
- defaultMessage: 'Unmap field',
- }),
- disabled: unpromotingState.isUnpromotingField,
- onClick: (fieldEntry: FieldEntry) => {
- unpromotingState.setSelectedField(fieldEntry.name);
- },
- },
- ];
- break;
- case 'unmapped':
- actions = [
- {
- name: i18n.translate('xpack.streams.actions.viewFieldLabel', {
- defaultMessage: 'View field',
- }),
- disabled: editingState.isSaving,
- onClick: (fieldEntry: FieldEntry) => {
- editingState.selectField(fieldEntry, false);
- },
- },
- {
- name: i18n.translate('xpack.streams.actions.mapFieldLabel', {
- defaultMessage: 'Map field',
- }),
- disabled: editingState.isSaving,
- onClick: (fieldEntry: FieldEntry) => {
- editingState.selectField(fieldEntry, true);
- },
- },
- ];
- break;
- case 'inherited':
- actions = [
- {
- name: i18n.translate('xpack.streams.actions.viewFieldLabel', {
- defaultMessage: 'View field',
- }),
- disabled: editingState.isSaving,
- onClick: (fieldEntry: FieldEntry) => {
- editingState.selectField(fieldEntry, false);
- },
- },
- ];
- break;
- }
+ switch (field.status) {
+ case 'mapped':
+ actions = [
+ {
+ name: i18n.translate('xpack.streams.actions.viewFieldLabel', {
+ defaultMessage: 'View field',
+ }),
+ disabled: editingState.isSaving,
+ onClick: (fieldEntry: FieldEntry) => {
+ editingState.selectField(fieldEntry, false);
+ },
+ },
+ {
+ name: i18n.translate('xpack.streams.actions.editFieldLabel', {
+ defaultMessage: 'Edit field',
+ }),
+ disabled: editingState.isSaving,
+ onClick: (fieldEntry: FieldEntry) => {
+ editingState.selectField(fieldEntry, true);
+ },
+ },
+ {
+ name: i18n.translate('xpack.streams.actions.unpromoteFieldLabel', {
+ defaultMessage: 'Unmap field',
+ }),
+ disabled: unpromotingState.isUnpromotingField,
+ onClick: (fieldEntry: FieldEntry) => {
+ unpromotingState.setSelectedField(fieldEntry.name);
+ },
+ },
+ ];
+ break;
+ case 'unmapped':
+ actions = [
+ {
+ name: i18n.translate('xpack.streams.actions.viewFieldLabel', {
+ defaultMessage: 'View field',
+ }),
+ disabled: editingState.isSaving,
+ onClick: (fieldEntry: FieldEntry) => {
+ editingState.selectField(fieldEntry, false);
+ },
+ },
+ {
+ name: i18n.translate('xpack.streams.actions.mapFieldLabel', {
+ defaultMessage: 'Map field',
+ }),
+ disabled: editingState.isSaving,
+ onClick: (fieldEntry: FieldEntry) => {
+ editingState.selectField(fieldEntry, true);
+ },
+ },
+ ];
+ break;
+ case 'inherited':
+ actions = [
+ {
+ name: i18n.translate('xpack.streams.actions.viewFieldLabel', {
+ defaultMessage: 'View field',
+ }),
+ disabled: editingState.isSaving,
+ onClick: (fieldEntry: FieldEntry) => {
+ editingState.selectField(fieldEntry, false);
+ },
+ },
+ ];
+ break;
+ }
- return (
- ({
- name: action.name,
- icon: action.icon,
- onClick: (event) => {
- action.onClick(field);
+ id: 0,
+ title: i18n.translate(
+ 'xpack.streams.streamDetailSchemaEditorFieldsTableActionsTitle',
+ {
+ defaultMessage: 'Actions',
+ }
+ ),
+ items: actions.map((action) => ({
+ name: action.name,
+ icon: action.icon,
+ onClick: (event) => {
+ action.onClick(field);
+ },
+ })),
},
- })),
- },
- ]}
- />
- );
- },
- },
- ] as EuiDataGridProps['trailingControlColumns'];
- }, [editingState, fields, unpromotingState]);
+ ]}
+ />
+ );
+ },
+ },
+ ] as EuiDataGridProps['trailingControlColumns'])
+ : undefined;
+ }, [definition, editingState, fields, unpromotingState]);
return (
{
[BEGIN rubric]
1. Is the submission non-empty and not null?
2. Is the submission well-formed JSON?
-3. Evaluate the value of the "detailsMarkdown" field of all the "attackDiscoveries" in the submission json. Do the values of "detailsMarkdown" in the submission capture the essence of the "expected response", regardless of the order in which they appear, and highlight the same incident(s)?
-4. Evaluate the value of the "entitySummaryMarkdown" field of all the "attackDiscoveries" in the submission json. Does the value of "entitySummaryMarkdown" in the submission mention at least 50% the same entities as in the "expected response"?
-5. Evaluate the value of the "summaryMarkdown" field of all the "attackDiscoveries" in the submission json. Do the values of "summaryMarkdown" in the submission at least partially similar to that of the "expected response", regardless of the order in which they appear, and summarize the same incident(s)?
-6. Evaluate the value of the "title" field of all the "attackDiscoveries" in the submission json. Are the "title" values in the submission at least partially similar to the tile(s) of the "expected response", regardless of the order in which they appear, and mention the same incident(s)?
-7. Evaluate the value of the "alertIds" field of all the "attackDiscoveries" in the submission json. Do they match at least 100% of the "alertIds" in the submission?
+3. Evaluate the "detailsMarkdown" field of all "attackDiscoveries" in the submission JSON. Do the values of "detailsMarkdown" in the submission capture the overall essence of the "expected response", regardless of the order in which they appear? Slight differences in details are acceptable as long as the overall intent and meaning remain consistent, and no key incidents are omitted or misrepresented.
+4. Evaluate the value of the "entitySummaryMarkdown" field of all the "attackDiscoveries" in the submission JSON. Does the submission mention at least half of the same entities (i.e. either host or user) as in the "expected response"?
+5. Evaluate the value of the "summaryMarkdown" field of all the "attackDiscoveries" in the submission JSON. Are the values of "summaryMarkdown" in the "submission" at least partially similar to that of the "expected response", regardless of the order in which they appear, and summarize the same incident(s)? Summarize each summary, and explain your answer with lettered steps.
+6. Evaluate the value of the "title" field of all the "attackDiscoveries" in the submission json. Are the "title" values in the submission at least partially similar to the title(s) of the "expected response", regardless of the order in which they appear, and mention the same incident(s)?
+7. Compare the submission json and the expected response json. Do over half of "alertIds" field in the "attackDiscoveries" in the submission .json overlap with the "alertsIds" field in the expected responses .json? Respond with yes or no.
[END rubric]
[BEGIN DATA]
diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/helpers/get_custom_evaluator/get_default_prompt_template/index.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/helpers/get_custom_evaluator/get_default_prompt_template/index.ts
index 08e10f00e7f77..395d6ab723704 100644
--- a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/helpers/get_custom_evaluator/get_default_prompt_template/index.ts
+++ b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/helpers/get_custom_evaluator/get_default_prompt_template/index.ts
@@ -11,11 +11,11 @@ export const getDefaultPromptTemplate =
[BEGIN rubric]
1. Is the submission non-empty and not null?
2. Is the submission well-formed JSON?
-3. Evaluate the value of the "detailsMarkdown" field of all the "attackDiscoveries" in the submission json. Do the values of "detailsMarkdown" in the submission capture the essence of the "expected response", regardless of the order in which they appear, and highlight the same incident(s)?
-4. Evaluate the value of the "entitySummaryMarkdown" field of all the "attackDiscoveries" in the submission json. Does the value of "entitySummaryMarkdown" in the submission mention at least 50% the same entities as in the "expected response"?
-5. Evaluate the value of the "summaryMarkdown" field of all the "attackDiscoveries" in the submission json. Do the values of "summaryMarkdown" in the submission at least partially similar to that of the "expected response", regardless of the order in which they appear, and summarize the same incident(s)?
-6. Evaluate the value of the "title" field of all the "attackDiscoveries" in the submission json. Are the "title" values in the submission at least partially similar to the tile(s) of the "expected response", regardless of the order in which they appear, and mention the same incident(s)?
-7. Evaluate the value of the "alertIds" field of all the "attackDiscoveries" in the submission json. Do they match at least 100% of the "alertIds" in the submission?
+3. Evaluate the "detailsMarkdown" field of all "attackDiscoveries" in the submission JSON. Do the values of "detailsMarkdown" in the submission capture the overall essence of the "expected response", regardless of the order in which they appear? Slight differences in details are acceptable as long as the overall intent and meaning remain consistent, and no key incidents are omitted or misrepresented.
+4. Evaluate the value of the "entitySummaryMarkdown" field of all the "attackDiscoveries" in the submission JSON. Does the submission mention at least half of the same entities (i.e. either host or user) as in the "expected response"?
+5. Evaluate the value of the "summaryMarkdown" field of all the "attackDiscoveries" in the submission JSON. Are the values of "summaryMarkdown" in the "submission" at least partially similar to that of the "expected response", regardless of the order in which they appear, and summarize the same incident(s)? Summarize each summary, and explain your answer with lettered steps.
+6. Evaluate the value of the "title" field of all the "attackDiscoveries" in the submission json. Are the "title" values in the submission at least partially similar to the title(s) of the "expected response", regardless of the order in which they appear, and mention the same incident(s)?
+7. Compare the submission json and the expected response json. Do over half of "alertIds" field in the "attackDiscoveries" in the submission .json overlap with the "alertsIds" field in the expected responses .json? Respond with yes or no.
[END rubric]
[BEGIN DATA]
diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/index.test.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/index.test.ts
new file mode 100644
index 0000000000000..1953dd4d45bf5
--- /dev/null
+++ b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/index.test.ts
@@ -0,0 +1,211 @@
+/*
+ * 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 { actionsClientMock } from '@kbn/actions-plugin/server/actions_client/actions_client.mock';
+import { callAssistantGraph } from '.';
+import { getDefaultAssistantGraph } from './graph';
+import { invokeGraph, streamGraph } from './helpers';
+import { loggerMock } from '@kbn/logging-mocks';
+import { AgentExecutorParams, AssistantDataClients } from '../../executors/types';
+import { elasticsearchClientMock } from '@kbn/core-elasticsearch-client-server-mocks';
+import { getFindAnonymizationFieldsResultWithSingleHit } from '../../../../__mocks__/response';
+import {
+ createOpenAIToolsAgent,
+ createStructuredChatAgent,
+ createToolCallingAgent,
+} from 'langchain/agents';
+jest.mock('./graph');
+jest.mock('./helpers');
+jest.mock('langchain/agents');
+jest.mock('@kbn/langchain/server/tracers/apm');
+jest.mock('@kbn/langchain/server/tracers/telemetry');
+const getDefaultAssistantGraphMock = getDefaultAssistantGraph as jest.Mock;
+describe('callAssistantGraph', () => {
+ const mockDataClients = {
+ anonymizationFieldsDataClient: {
+ findDocuments: jest.fn(),
+ },
+ kbDataClient: {
+ isInferenceEndpointExists: jest.fn(),
+ getAssistantTools: jest.fn(),
+ },
+ } as unknown as AssistantDataClients;
+
+ const mockRequest = {
+ body: {
+ model: 'test-model',
+ },
+ };
+
+ const defaultParams = {
+ actionsClient: actionsClientMock.create(),
+ alertsIndexPattern: 'test-pattern',
+ assistantTools: [],
+ connectorId: 'test-connector',
+ conversationId: 'test-conversation',
+ dataClients: mockDataClients,
+ esClient: elasticsearchClientMock.createScopedClusterClient().asCurrentUser,
+ inference: {},
+ langChainMessages: [{ content: 'test message' }],
+ llmTasks: { retrieveDocumentationAvailable: jest.fn(), retrieveDocumentation: jest.fn() },
+ llmType: 'openai',
+ isOssModel: false,
+ logger: loggerMock.create(),
+ isStream: false,
+ onLlmResponse: jest.fn(),
+ onNewReplacements: jest.fn(),
+ replacements: [],
+ request: mockRequest,
+ size: 1,
+ systemPrompt: 'test-prompt',
+ telemetry: {},
+ telemetryParams: {},
+ traceOptions: {},
+ responseLanguage: 'English',
+ } as unknown as AgentExecutorParams;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ (mockDataClients?.kbDataClient?.isInferenceEndpointExists as jest.Mock).mockResolvedValue(true);
+ getDefaultAssistantGraphMock.mockReturnValue({});
+ (invokeGraph as jest.Mock).mockResolvedValue({
+ output: 'test-output',
+ traceData: {},
+ conversationId: 'new-conversation-id',
+ });
+ (streamGraph as jest.Mock).mockResolvedValue({});
+ (mockDataClients?.anonymizationFieldsDataClient?.findDocuments as jest.Mock).mockResolvedValue(
+ getFindAnonymizationFieldsResultWithSingleHit()
+ );
+ });
+
+ it('calls invokeGraph with correct parameters for non-streaming', async () => {
+ const result = await callAssistantGraph(defaultParams);
+
+ expect(invokeGraph).toHaveBeenCalledWith(
+ expect.objectContaining({
+ inputs: expect.objectContaining({
+ input: 'test message',
+ }),
+ })
+ );
+ expect(result.body).toEqual({
+ connector_id: 'test-connector',
+ data: 'test-output',
+ trace_data: {},
+ replacements: [],
+ status: 'ok',
+ conversationId: 'new-conversation-id',
+ });
+ });
+
+ it('calls streamGraph with correct parameters for streaming', async () => {
+ const params = { ...defaultParams, isStream: true };
+ await callAssistantGraph(params);
+
+ expect(streamGraph).toHaveBeenCalledWith(
+ expect.objectContaining({
+ inputs: expect.objectContaining({
+ input: 'test message',
+ }),
+ })
+ );
+ });
+
+ it('calls getDefaultAssistantGraph without signal for openai', async () => {
+ await callAssistantGraph(defaultParams);
+ expect(getDefaultAssistantGraphMock.mock.calls[0][0]).not.toHaveProperty('signal');
+ });
+
+ it('calls getDefaultAssistantGraph with signal for bedrock', async () => {
+ await callAssistantGraph({ ...defaultParams, llmType: 'bedrock' });
+ expect(getDefaultAssistantGraphMock.mock.calls[0][0]).toHaveProperty('signal');
+ });
+
+ it('handles error when anonymizationFieldsDataClient.findDocuments fails', async () => {
+ (mockDataClients?.anonymizationFieldsDataClient?.findDocuments as jest.Mock).mockRejectedValue(
+ new Error('test error')
+ );
+
+ await expect(callAssistantGraph(defaultParams)).rejects.toThrow('test error');
+ });
+
+ it('handles error when kbDataClient.isInferenceEndpointExists fails', async () => {
+ (mockDataClients?.kbDataClient?.isInferenceEndpointExists as jest.Mock).mockRejectedValue(
+ new Error('test error')
+ );
+
+ await expect(callAssistantGraph(defaultParams)).rejects.toThrow('test error');
+ });
+
+ it('returns correct response when no conversationId is returned', async () => {
+ (invokeGraph as jest.Mock).mockResolvedValue({ output: 'test-output', traceData: {} });
+
+ const result = await callAssistantGraph(defaultParams);
+
+ expect(result.body).toEqual({
+ connector_id: 'test-connector',
+ data: 'test-output',
+ trace_data: {},
+ replacements: [],
+ status: 'ok',
+ });
+ });
+
+ describe('agentRunnable', () => {
+ it('creates OpenAIToolsAgent for openai llmType', async () => {
+ const params = { ...defaultParams, llmType: 'openai' };
+ await callAssistantGraph(params);
+
+ expect(createOpenAIToolsAgent).toHaveBeenCalled();
+ expect(createStructuredChatAgent).not.toHaveBeenCalled();
+ expect(createToolCallingAgent).not.toHaveBeenCalled();
+ });
+
+ it('creates OpenAIToolsAgent for inference llmType', async () => {
+ const params = { ...defaultParams, llmType: 'inference' };
+ await callAssistantGraph(params);
+
+ expect(createOpenAIToolsAgent).toHaveBeenCalled();
+ expect(createStructuredChatAgent).not.toHaveBeenCalled();
+ expect(createToolCallingAgent).not.toHaveBeenCalled();
+ });
+
+ it('creates ToolCallingAgent for bedrock llmType', async () => {
+ const params = { ...defaultParams, llmType: 'bedrock' };
+ await callAssistantGraph(params);
+
+ expect(createToolCallingAgent).toHaveBeenCalled();
+ expect(createOpenAIToolsAgent).not.toHaveBeenCalled();
+ expect(createStructuredChatAgent).not.toHaveBeenCalled();
+ });
+
+ it('creates ToolCallingAgent for gemini llmType', async () => {
+ const params = {
+ ...defaultParams,
+ request: {
+ body: { model: 'gemini-1.5-flash' },
+ } as unknown as AgentExecutorParams['request'],
+ llmType: 'gemini',
+ };
+ await callAssistantGraph(params);
+
+ expect(createToolCallingAgent).toHaveBeenCalled();
+ expect(createOpenAIToolsAgent).not.toHaveBeenCalled();
+ expect(createStructuredChatAgent).not.toHaveBeenCalled();
+ });
+
+ it('creates StructuredChatAgent for oss model', async () => {
+ const params = { ...defaultParams, llmType: 'openai', isOssModel: true };
+ await callAssistantGraph(params);
+
+ expect(createStructuredChatAgent).toHaveBeenCalled();
+ expect(createOpenAIToolsAgent).not.toHaveBeenCalled();
+ expect(createToolCallingAgent).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/index.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/index.ts
index cfcd0f49071b3..2e94e4bcd4ea0 100644
--- a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/index.ts
+++ b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/index.ts
@@ -8,7 +8,7 @@
import { StructuredTool } from '@langchain/core/tools';
import { getDefaultArguments } from '@kbn/langchain/server';
import {
- createOpenAIFunctionsAgent,
+ createOpenAIToolsAgent,
createStructuredChatAgent,
createToolCallingAgent,
} from 'langchain/agents';
@@ -130,30 +130,31 @@ export const callAssistantGraph: AgentExecutor = async ({
}
}
- const agentRunnable = isOpenAI
- ? await createOpenAIFunctionsAgent({
- llm: createLlmInstance(),
- tools,
- prompt: formatPrompt(systemPrompts.openai, systemPrompt),
- streamRunnable: isStream,
- })
- : llmType && ['bedrock', 'gemini'].includes(llmType)
- ? await createToolCallingAgent({
- llm: createLlmInstance(),
- tools,
- prompt:
- llmType === 'bedrock'
- ? formatPrompt(systemPrompts.bedrock, systemPrompt)
- : formatPrompt(systemPrompts.gemini, systemPrompt),
- streamRunnable: isStream,
- })
- : // used with OSS models
- await createStructuredChatAgent({
- llm: createLlmInstance(),
- tools,
- prompt: formatPromptStructured(systemPrompts.structuredChat, systemPrompt),
- streamRunnable: isStream,
- });
+ const agentRunnable =
+ isOpenAI || llmType === 'inference'
+ ? await createOpenAIToolsAgent({
+ llm: createLlmInstance(),
+ tools,
+ prompt: formatPrompt(systemPrompts.openai, systemPrompt),
+ streamRunnable: isStream,
+ })
+ : llmType && ['bedrock', 'gemini'].includes(llmType)
+ ? await createToolCallingAgent({
+ llm: createLlmInstance(),
+ tools,
+ prompt:
+ llmType === 'bedrock'
+ ? formatPrompt(systemPrompts.bedrock, systemPrompt)
+ : formatPrompt(systemPrompts.gemini, systemPrompt),
+ streamRunnable: isStream,
+ })
+ : // used with OSS models
+ await createStructuredChatAgent({
+ llm: createLlmInstance(),
+ tools,
+ prompt: formatPromptStructured(systemPrompts.structuredChat, systemPrompt),
+ streamRunnable: isStream,
+ });
const apmTracer = new APMTracer({ projectName: traceOptions?.projectName ?? 'default' }, logger);
const telemetryTracer = telemetryParams
diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/utils.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/utils.ts
index 4cc213f0e0db8..cb38ea78e27bc 100644
--- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/utils.ts
+++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/utils.ts
@@ -177,6 +177,7 @@ export const getLlmType = (actionTypeId: string): string | undefined => {
[`.gen-ai`]: `openai`,
[`.bedrock`]: `bedrock`,
[`.gemini`]: `gemini`,
+ [`.inference`]: `inference`,
};
return llmTypeDictionary[actionTypeId];
};
diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/quickstart_client.gen.ts b/x-pack/solutions/security/plugins/security_solution/common/api/quickstart_client.gen.ts
index fa315e3c421aa..86599d48267a7 100644
--- a/x-pack/solutions/security/plugins/security_solution/common/api/quickstart_client.gen.ts
+++ b/x-pack/solutions/security/plugins/security_solution/common/api/quickstart_client.gen.ts
@@ -367,6 +367,7 @@ import type {
GetRuleMigrationRequestQueryInput,
GetRuleMigrationRequestParamsInput,
GetRuleMigrationResponse,
+ GetRuleMigrationIntegrationsResponse,
GetRuleMigrationPrebuiltRulesRequestParamsInput,
GetRuleMigrationPrebuiltRulesResponse,
GetRuleMigrationResourcesRequestQueryInput,
@@ -383,6 +384,9 @@ import type {
InstallMigrationRulesResponse,
InstallTranslatedMigrationRulesRequestParamsInput,
InstallTranslatedMigrationRulesResponse,
+ RetryRuleMigrationRequestParamsInput,
+ RetryRuleMigrationRequestBodyInput,
+ RetryRuleMigrationResponse,
StartRuleMigrationRequestParamsInput,
StartRuleMigrationRequestBodyInput,
StartRuleMigrationResponse,
@@ -1455,6 +1459,21 @@ finalize it.
})
.catch(catchAxiosErrorFormatAndThrow);
}
+ /**
+ * Retrieves all related integrations
+ */
+ async getRuleMigrationIntegrations() {
+ this.log.info(`${new Date().toISOString()} Calling API GetRuleMigrationIntegrations`);
+ return this.kbnClient
+ .request({
+ path: '/internal/siem_migrations/rules/integrations',
+ headers: {
+ [ELASTIC_HTTP_VERSION_HEADER]: '1',
+ },
+ method: 'GET',
+ })
+ .catch(catchAxiosErrorFormatAndThrow);
+ }
/**
* Retrieves all available prebuilt rules (installed and installable)
*/
@@ -2010,6 +2029,22 @@ detection engine rules.
})
.catch(catchAxiosErrorFormatAndThrow);
}
+ /**
+ * Retries a SIEM rules migration using the migration id provided
+ */
+ async retryRuleMigration(props: RetryRuleMigrationProps) {
+ this.log.info(`${new Date().toISOString()} Calling API RetryRuleMigration`);
+ return this.kbnClient
+ .request({
+ path: replaceParams('/internal/siem_migrations/rules/{migration_id}/retry', props.params),
+ headers: {
+ [ELASTIC_HTTP_VERSION_HEADER]: '1',
+ },
+ method: 'PUT',
+ body: props.body,
+ })
+ .catch(catchAxiosErrorFormatAndThrow);
+ }
async riskEngineGetPrivileges() {
this.log.info(`${new Date().toISOString()} Calling API RiskEngineGetPrivileges`);
return this.kbnClient
@@ -2545,6 +2580,10 @@ export interface ReadRuleProps {
export interface ResolveTimelineProps {
query: ResolveTimelineRequestQueryInput;
}
+export interface RetryRuleMigrationProps {
+ params: RetryRuleMigrationRequestParamsInput;
+ body: RetryRuleMigrationRequestBodyInput;
+}
export interface RulePreviewProps {
query: RulePreviewRequestQueryInput;
body: RulePreviewRequestBodyInput;
diff --git a/x-pack/solutions/security/plugins/security_solution/common/experimental_features.ts b/x-pack/solutions/security/plugins/security_solution/common/experimental_features.ts
index 14daad66a7c23..c94167964b71b 100644
--- a/x-pack/solutions/security/plugins/security_solution/common/experimental_features.ts
+++ b/x-pack/solutions/security/plugins/security_solution/common/experimental_features.ts
@@ -190,12 +190,6 @@ export const allowedExperimentalValues = Object.freeze({
*/
timelineEsqlTabDisabled: false,
- /*
- * Disables date pickers and sourcerer in analyzer if needed.
- *
- */
- analyzerDatePickersAndSourcererDisabled: false,
-
/**
* Enables graph visualization in alerts flyout
*/
diff --git a/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/constants.ts b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/constants.ts
index 88ed777c21d69..019766d96e78f 100644
--- a/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/constants.ts
+++ b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/constants.ts
@@ -11,6 +11,8 @@ export const SIEM_MIGRATIONS_PATH = '/internal/siem_migrations' as const;
export const SIEM_RULE_MIGRATIONS_PATH = `${SIEM_MIGRATIONS_PATH}/rules` as const;
export const SIEM_RULE_MIGRATIONS_ALL_STATS_PATH = `${SIEM_RULE_MIGRATIONS_PATH}/stats` as const;
+export const SIEM_RULE_MIGRATIONS_INTEGRATIONS_PATH =
+ `${SIEM_RULE_MIGRATIONS_PATH}/integrations` as const;
export const SIEM_RULE_MIGRATION_CREATE_PATH =
`${SIEM_RULE_MIGRATIONS_PATH}/{migration_id?}` as const;
export const SIEM_RULE_MIGRATION_PATH = `${SIEM_RULE_MIGRATIONS_PATH}/{migration_id}` as const;
diff --git a/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.gen.ts b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.gen.ts
index 47c06e1e02c7a..661ef1d1cbb4b 100644
--- a/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.gen.ts
+++ b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.gen.ts
@@ -28,6 +28,7 @@ import {
RuleMigrationResourceType,
RuleMigrationResource,
} from '../../rule_migration.gen';
+import { RelatedIntegration } from '../../../../api/detection_engine/model/rule_schema/common_attributes.gen';
import { NonEmptyString } from '../../../../api/model/primitives.gen';
import { ConnectorId, LangSmithOptions } from '../../common.gen';
@@ -79,6 +80,14 @@ export const GetRuleMigrationResponse = z.object({
data: z.array(RuleMigration),
});
+/**
+ * The map of related integrations, with the integration id as a key
+ */
+export type GetRuleMigrationIntegrationsResponse = z.infer<
+ typeof GetRuleMigrationIntegrationsResponse
+>;
+export const GetRuleMigrationIntegrationsResponse = z.object({}).catchall(RelatedIntegration);
+
export type GetRuleMigrationPrebuiltRulesRequestParams = z.infer<
typeof GetRuleMigrationPrebuiltRulesRequestParams
>;
@@ -214,6 +223,35 @@ export const InstallTranslatedMigrationRulesResponse = z.object({
installed: z.boolean(),
});
+export type RetryRuleMigrationRequestParams = z.infer;
+export const RetryRuleMigrationRequestParams = z.object({
+ migration_id: NonEmptyString,
+});
+export type RetryRuleMigrationRequestParamsInput = z.input;
+
+export type RetryRuleMigrationRequestBody = z.infer;
+export const RetryRuleMigrationRequestBody = z.object({
+ connector_id: ConnectorId,
+ langsmith_options: LangSmithOptions.optional(),
+ /**
+ * The indicator to retry only failed rules
+ */
+ failed: z.boolean().optional(),
+ /**
+ * The indicator to retry only not fully translated rules
+ */
+ not_fully_translated: z.boolean().optional(),
+});
+export type RetryRuleMigrationRequestBodyInput = z.input;
+
+export type RetryRuleMigrationResponse = z.infer;
+export const RetryRuleMigrationResponse = z.object({
+ /**
+ * Indicates the migration retry has been started. `false` means the migration does not need to be retried.
+ */
+ started: z.boolean(),
+});
+
export type StartRuleMigrationRequestParams = z.infer;
export const StartRuleMigrationRequestParams = z.object({
migration_id: NonEmptyString,
diff --git a/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.schema.yaml b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.schema.yaml
index 69e43b57dabd3..823e2b32d2fce 100644
--- a/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.schema.yaml
+++ b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.schema.yaml
@@ -54,6 +54,26 @@ paths:
items:
$ref: '../../rule_migration.schema.yaml#/components/schemas/RuleMigrationTaskStats'
+ /internal/siem_migrations/rules/integrations:
+ get:
+ summary: Retrieves all related integrations for a specific migration
+ operationId: GetRuleMigrationIntegrations
+ x-codegen-enabled: true
+ x-internal: true
+ description: Retrieves all related integrations
+ tags:
+ - SIEM Rule Migrations
+ responses:
+ 200:
+ description: Indicates that related integrations have been retrieved correctly.
+ content:
+ application/json:
+ schema:
+ type: object
+ description: The map of related integrations, with the integration id as a key
+ additionalProperties:
+ $ref: '../../../../../common/api/detection_engine/model/rule_schema/common_attributes.schema.yaml#/components/schemas/RelatedIntegration'
+
## Specific rule migration APIs
/internal/siem_migrations/rules/{migration_id}:
@@ -286,6 +306,57 @@ paths:
204:
description: Indicates the migration id was not found.
+ /internal/siem_migrations/rules/{migration_id}/retry:
+ put:
+ summary: Retries a rule migration
+ operationId: RetryRuleMigration
+ x-codegen-enabled: true
+ x-internal: true
+ description: Retries a SIEM rules migration using the migration id provided
+ tags:
+ - SIEM Rule Migrations
+ parameters:
+ - name: migration_id
+ in: path
+ required: true
+ schema:
+ description: The migration id to retry
+ $ref: '../../../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString'
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ required:
+ - connector_id
+ properties:
+ connector_id:
+ $ref: '../../common.schema.yaml#/components/schemas/ConnectorId'
+ langsmith_options:
+ $ref: '../../common.schema.yaml#/components/schemas/LangSmithOptions'
+ failed:
+ type: boolean
+ description: The indicator to retry only failed rules
+ not_fully_translated:
+ type: boolean
+ description: The indicator to retry only not fully translated rules
+ responses:
+ 200:
+ description: Indicates the migration retry request has been processed successfully.
+ content:
+ application/json:
+ schema:
+ type: object
+ required:
+ - started
+ properties:
+ started:
+ type: boolean
+ description: Indicates the migration retry has been started. `false` means the migration does not need to be retried.
+ 204:
+ description: Indicates the migration id was not found.
+
/internal/siem_migrations/rules/{migration_id}/stats:
get:
summary: Gets a rule migration task stats
diff --git a/x-pack/solutions/security/plugins/security_solution/public/assistant/provider.tsx b/x-pack/solutions/security/plugins/security_solution/public/assistant/provider.tsx
index a6551e8915e6d..910cd9c32515e 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/assistant/provider.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/assistant/provider.tsx
@@ -145,6 +145,16 @@ export const AssistantProvider: FC> = ({ children })
userProfile,
chrome,
} = useKibana().services;
+
+ let inferenceEnabled = false;
+ try {
+ actionTypeRegistry.get('.inference');
+ inferenceEnabled = true;
+ } catch (e) {
+ // swallow error
+ // inferenceEnabled will be false
+ }
+
const basePath = useBasePath();
const baseConversations = useBaseConversations();
@@ -223,6 +233,7 @@ export const AssistantProvider: FC> = ({ children })
baseConversations={baseConversations}
getComments={getComments}
http={http}
+ inferenceEnabled={inferenceEnabled}
navigateToApp={navigateToApp}
title={ASSISTANT_TITLE}
toasts={toasts}
diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_header_buttons.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_header_buttons.tsx
index 58a8f7b779a15..bbffd25b4f8f8 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_header_buttons.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_header_buttons.tsx
@@ -25,10 +25,9 @@ export const AddPrebuiltRulesHeaderButtons = () => {
const {
state: {
selectedRules,
- loadingRules,
isRefetching,
isUpgradingSecurityPackages,
- isInstallingAllRules,
+ isAnyRuleInstalling,
hasRulesToInstall,
},
actions: { installAllRules, installSelectedRules },
@@ -39,8 +38,7 @@ export const AddPrebuiltRulesHeaderButtons = () => {
const numberOfSelectedRules = selectedRules.length ?? 0;
const shouldDisplayInstallSelectedRulesButton = numberOfSelectedRules > 0;
- const isRuleInstalling = loadingRules.length > 0 || isInstallingAllRules;
- const isRequestInProgress = isRuleInstalling || isRefetching || isUpgradingSecurityPackages;
+ const isRequestInProgress = isAnyRuleInstalling || isRefetching || isUpgradingSecurityPackages;
const [isOverflowPopoverOpen, setOverflowPopover] = useBoolean(false);
@@ -81,7 +79,7 @@ export const AddPrebuiltRulesHeaderButtons = () => {
data-test-subj="installSelectedRulesButton"
>
{i18n.INSTALL_SELECTED_RULES(numberOfSelectedRules)}
- {isRuleInstalling && }
+ {isAnyRuleInstalling && }
@@ -116,7 +114,7 @@ export const AddPrebuiltRulesHeaderButtons = () => {
aria-label={i18n.INSTALL_ALL_ARIA_LABEL}
>
{i18n.INSTALL_ALL}
- {isRuleInstalling && }
+ {isAnyRuleInstalling && }
diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_install_button.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_install_button.tsx
index 4d239458e6a57..bbed7dcd2961f 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_install_button.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_install_button.tsx
@@ -19,7 +19,10 @@ import React, { useCallback, useMemo } from 'react';
import useBoolean from 'react-use/lib/useBoolean';
import type { Rule } from '../../../../rule_management/logic';
import type { RuleSignatureId } from '../../../../../../common/api/detection_engine';
-import { type AddPrebuiltRulesTableActions } from './add_prebuilt_rules_table_context';
+import {
+ useAddPrebuiltRulesTableContext,
+ type AddPrebuiltRulesTableActions,
+} from './add_prebuilt_rules_table_context';
import * as i18n from './translations';
export interface PrebuiltRulesInstallButtonProps {
@@ -28,7 +31,6 @@ export interface PrebuiltRulesInstallButtonProps {
installOneRule: AddPrebuiltRulesTableActions['installOneRule'];
loadingRules: RuleSignatureId[];
isDisabled: boolean;
- isInstallingAllRules: boolean;
}
export const PrebuiltRulesInstallButton = ({
@@ -37,8 +39,10 @@ export const PrebuiltRulesInstallButton = ({
installOneRule,
loadingRules,
isDisabled,
- isInstallingAllRules,
}: PrebuiltRulesInstallButtonProps) => {
+ const {
+ state: { isInstallingAllRules },
+ } = useAddPrebuiltRulesTableContext();
const isRuleInstalling = loadingRules.includes(ruleId) || isInstallingAllRules;
const isInstallButtonDisabled = isRuleInstalling || isDisabled;
const [isPopoverOpen, setPopover] = useBoolean(false);
diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_table_context.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_table_context.tsx
index dca4809001971..cf5ba8aa5967b 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_table_context.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_table_context.tsx
@@ -61,11 +61,14 @@ export interface AddPrebuiltRulesTableState {
* package in background
*/
isUpgradingSecurityPackages: boolean;
-
/**
* Is true when performing Install All Rules mutation
*/
isInstallingAllRules: boolean;
+ /**
+ * Is true when any rule is currently being installed
+ */
+ isAnyRuleInstalling: boolean;
/**
* List of rule IDs that are currently being upgraded
*/
@@ -145,6 +148,8 @@ export const AddPrebuiltRulesTableContextProvider = ({
}),
});
+ const isAnyRuleInstalling = loadingRules.length > 0 || isInstallingAllRules;
+
const { mutateAsync: installAllRulesRequest } = usePerformInstallAllRules();
const { mutateAsync: installSpecificRulesRequest } = usePerformInstallSpecificRules();
@@ -281,6 +286,7 @@ export const AddPrebuiltRulesTableContextProvider = ({
isRefetching,
isUpgradingSecurityPackages,
isInstallingAllRules,
+ isAnyRuleInstalling,
selectedRules,
lastUpdated: dataUpdatedAt,
},
@@ -297,6 +303,7 @@ export const AddPrebuiltRulesTableContextProvider = ({
isRefetching,
isUpgradingSecurityPackages,
isInstallingAllRules,
+ isAnyRuleInstalling,
selectedRules,
dataUpdatedAt,
actions,
diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/use_add_prebuilt_rules_table_columns.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/use_add_prebuilt_rules_table_columns.tsx
index 4e15de4011fab..8501f595839c7 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/use_add_prebuilt_rules_table_columns.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/use_add_prebuilt_rules_table_columns.tsx
@@ -110,8 +110,7 @@ const INTEGRATIONS_COLUMN: TableColumn = {
const createInstallButtonColumn = (
installOneRule: AddPrebuiltRulesTableActions['installOneRule'],
loadingRules: RuleSignatureId[],
- isDisabled: boolean,
- isInstallingAllRules: boolean
+ isDisabled: boolean
): TableColumn => ({
field: 'rule_id',
name: ,
@@ -122,7 +121,6 @@ const createInstallButtonColumn = (
installOneRule={installOneRule}
loadingRules={loadingRules}
isDisabled={isDisabled}
- isInstallingAllRules={isInstallingAllRules}
/>
),
width: '10%',
@@ -166,23 +164,9 @@ export const useAddPrebuiltRulesTableColumns = (): TableColumn[] => {
width: '12%',
},
...(hasCRUDPermissions
- ? [
- createInstallButtonColumn(
- installOneRule,
- loadingRules,
- isDisabled,
- isInstallingAllRules
- ),
- ]
+ ? [createInstallButtonColumn(installOneRule, loadingRules, isDisabled)]
: []),
],
- [
- hasCRUDPermissions,
- installOneRule,
- loadingRules,
- isDisabled,
- showRelatedIntegrations,
- isInstallingAllRules,
- ]
+ [hasCRUDPermissions, installOneRule, loadingRules, isDisabled, showRelatedIntegrations]
);
};
diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/translations.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/translations.tsx
index 4bcf0bd0a3b68..09f92d3ef9223 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/translations.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/translations.tsx
@@ -140,3 +140,37 @@ export const RULE_MODIFIED_BADGE_DESCRIPTION = i18n.translate(
'The rule was edited after installation and field values differs from the values upon installation',
}
);
+
+export const RULE_NEW_REVISION_DETECTED_WARNING = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.upgradeFlyout.ruleNewRevisionDetectedWarning',
+ {
+ defaultMessage: 'Installed rule changed',
+ }
+);
+
+export const RULE_NEW_REVISION_DETECTED_WARNING_DESCRIPTION = (ruleName: string) =>
+ i18n.translate(
+ 'xpack.securitySolution.detectionEngine.upgradeFlyout.ruleNewVersionDetectedWarningDescription',
+ {
+ defaultMessage:
+ 'Someone edited the installed rule "{ruleName}". Upgrade resolved conflicts were reset.',
+ values: { ruleName },
+ }
+ );
+
+export const RULE_NEW_VERSION_DETECTED_WARNING = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.upgradeFlyout.ruleNewRevisionDetectedWarning',
+ {
+ defaultMessage: 'New prebuilt rules package was installed',
+ }
+);
+
+export const RULE_NEW_VERSION_DETECTED_WARNING_DESCRIPTION = (ruleName: string) =>
+ i18n.translate(
+ 'xpack.securitySolution.detectionEngine.upgradeFlyout.ruleNewRevisionDetectedWarningDescription',
+ {
+ defaultMessage:
+ 'Newer prebuilt rules package were installed in background. It contains a newer rule version for "{ruleName}". Upgrade resolved conflicts were reset.',
+ values: { ruleName },
+ }
+ );
diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_context.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_context.tsx
index 751b24f865e61..762b5e45ed630 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_context.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_context.tsx
@@ -39,6 +39,8 @@ import { UpgradeFlyoutSubHeader } from './upgrade_flyout_subheader';
import * as ruleDetailsI18n from '../../../../rule_management/components/rule_details/translations';
import * as i18n from './translations';
+const REVIEW_PREBUILT_RULES_UPGRADE_REFRESH_INTERVAL = 5 * 60 * 1000;
+
export interface UpgradePrebuiltRulesTableState {
/**
* Rule upgrade state after applying `filterOptions`
@@ -110,6 +112,13 @@ interface UpgradePrebuiltRulesTableContextProviderProps {
children: React.ReactNode;
}
+/**
+ * Provides necessary data and actions for Rules Upgrade table.
+ *
+ * It periodically re-fetches prebuilt rules upgrade review data to detect possible cases of:
+ * - editing prebuilt rules (revision change)
+ * - releasing a new prebuilt rules package (version change)
+ */
export const UpgradePrebuiltRulesTableContextProvider = ({
children,
}: UpgradePrebuiltRulesTableContextProviderProps) => {
@@ -135,7 +144,7 @@ export const UpgradePrebuiltRulesTableContextProvider = ({
isLoading,
isRefetching,
} = usePrebuiltRulesUpgradeReview({
- refetchInterval: false, // Disable automatic refetching since request is expensive
+ refetchInterval: REVIEW_PREBUILT_RULES_UPGRADE_REFRESH_INTERVAL,
keepPreviousData: true, // Use this option so that the state doesn't jump between "success" and "loading" on page change
});
const { rulesUpgradeState, setRuleFieldResolvedValue } =
diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade_state.test.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade_state.test.ts
new file mode 100644
index 0000000000000..cc8cf9511473c
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade_state.test.ts
@@ -0,0 +1,436 @@
+/*
+ * 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 { FieldUpgradeStateEnum } from '../../../../rule_management/model/prebuilt_rule_upgrade';
+import type { RuleResponse } from '../../../../../../common/api/detection_engine';
+import { useAppToasts } from '../../../../../common/hooks/use_app_toasts';
+import {
+ type RuleUpgradeInfoForReview,
+ ThreeWayDiffConflict,
+ ThreeWayDiffOutcome,
+ ThreeWayMergeOutcome,
+} from '../../../../../../common/api/detection_engine';
+import { act, renderHook } from '@testing-library/react';
+import { usePrebuiltRulesUpgradeState } from './use_prebuilt_rules_upgrade_state';
+
+jest.mock('../../../../rule_management/hooks/use_is_prebuilt_rules_customization_enabled', () => ({
+ useIsPrebuiltRulesCustomizationEnabled: jest.fn(() => true),
+}));
+
+jest.mock('../../../../../common/hooks/use_app_toasts', () => ({
+ useAppToasts: jest.fn().mockReturnValue({
+ addWarning: jest.fn(),
+ }),
+}));
+
+describe('usePrebuiltRulesUpgradeState', () => {
+ it('returns rule upgrade state', () => {
+ const ruleUpgradeInfosMock: RuleUpgradeInfoForReview[] = [createRuleUpgradeInfoMock()];
+
+ const {
+ result: {
+ current: { rulesUpgradeState },
+ },
+ } = renderHook(usePrebuiltRulesUpgradeState, {
+ initialProps: ruleUpgradeInfosMock,
+ });
+
+ expect(rulesUpgradeState).toEqual({
+ 'test-rule-id-1': expect.any(Object),
+ });
+ });
+
+ describe('fields upgrade state', () => {
+ it('returns empty state when there are no fields to upgrade', () => {
+ const ruleUpgradeInfosMock: RuleUpgradeInfoForReview[] = [createRuleUpgradeInfoMock()];
+
+ const { result } = renderHook(usePrebuiltRulesUpgradeState, {
+ initialProps: ruleUpgradeInfosMock,
+ });
+
+ expect(result.current.rulesUpgradeState).toEqual({
+ 'test-rule-id-1': expect.objectContaining({
+ fieldsUpgradeState: {},
+ }),
+ });
+ });
+
+ it('returns NO CONFLICT fields', () => {
+ const ruleUpgradeInfosMock: RuleUpgradeInfoForReview[] = [
+ createRuleUpgradeInfoMock({
+ diff: {
+ num_fields_with_updates: 1,
+ num_fields_with_conflicts: 0,
+ num_fields_with_non_solvable_conflicts: 0,
+ fields: {
+ name: {
+ base_version: 'base',
+ current_version: 'base',
+ target_version: 'target',
+ merged_version: 'target',
+ diff_outcome: ThreeWayDiffOutcome.StockValueCanUpdate,
+ merge_outcome: ThreeWayMergeOutcome.Target,
+ has_base_version: true,
+ has_update: true,
+ conflict: ThreeWayDiffConflict.NONE,
+ },
+ },
+ },
+ }),
+ ];
+
+ const { result } = renderHook(usePrebuiltRulesUpgradeState, {
+ initialProps: ruleUpgradeInfosMock,
+ });
+
+ expect(result.current.rulesUpgradeState).toEqual({
+ 'test-rule-id-1': expect.objectContaining({
+ fieldsUpgradeState: {
+ name: { state: FieldUpgradeStateEnum.NoConflict },
+ },
+ }),
+ });
+ });
+
+ it('returns SOLVABLE CONFLICT fields', () => {
+ const ruleUpgradeInfosMock: RuleUpgradeInfoForReview[] = [
+ createRuleUpgradeInfoMock({
+ diff: {
+ num_fields_with_updates: 1,
+ num_fields_with_conflicts: 1,
+ num_fields_with_non_solvable_conflicts: 0,
+ fields: {
+ name: {
+ base_version: 'base',
+ current_version: 'current',
+ target_version: 'target',
+ merged_version: 'target',
+ diff_outcome: ThreeWayDiffOutcome.StockValueCanUpdate,
+ merge_outcome: ThreeWayMergeOutcome.Merged,
+ has_base_version: true,
+ has_update: true,
+ conflict: ThreeWayDiffConflict.SOLVABLE,
+ },
+ },
+ },
+ }),
+ ];
+
+ const { result } = renderHook(usePrebuiltRulesUpgradeState, {
+ initialProps: ruleUpgradeInfosMock,
+ });
+
+ expect(result.current.rulesUpgradeState).toEqual({
+ 'test-rule-id-1': expect.objectContaining({
+ fieldsUpgradeState: {
+ name: { state: FieldUpgradeStateEnum.SolvableConflict },
+ },
+ }),
+ });
+ });
+
+ it('returns NON SOLVABLE CONFLICT fields', () => {
+ const ruleUpgradeInfosMock: RuleUpgradeInfoForReview[] = [
+ createRuleUpgradeInfoMock({
+ diff: {
+ num_fields_with_updates: 1,
+ num_fields_with_conflicts: 1,
+ num_fields_with_non_solvable_conflicts: 1,
+ fields: {
+ name: {
+ base_version: 'base',
+ current_version: 'current',
+ target_version: 'target',
+ merged_version: 'target',
+ diff_outcome: ThreeWayDiffOutcome.StockValueCanUpdate,
+ merge_outcome: ThreeWayMergeOutcome.Merged,
+ has_base_version: true,
+ has_update: true,
+ conflict: ThreeWayDiffConflict.NON_SOLVABLE,
+ },
+ },
+ },
+ }),
+ ];
+
+ const { result } = renderHook(usePrebuiltRulesUpgradeState, {
+ initialProps: ruleUpgradeInfosMock,
+ });
+
+ expect(result.current.rulesUpgradeState).toEqual({
+ 'test-rule-id-1': expect.objectContaining({
+ fieldsUpgradeState: {
+ name: { state: FieldUpgradeStateEnum.NonSolvableConflict },
+ },
+ }),
+ });
+ });
+
+ it('returns ACCEPTED fields after resolving a conflict', () => {
+ const ruleUpgradeInfosMock: RuleUpgradeInfoForReview[] = [
+ createRuleUpgradeInfoMock({
+ rule_id: 'test-rule-id-1',
+ diff: {
+ num_fields_with_updates: 1,
+ num_fields_with_conflicts: 1,
+ num_fields_with_non_solvable_conflicts: 1,
+ fields: {
+ name: {
+ base_version: 'base',
+ current_version: 'current',
+ target_version: 'target',
+ merged_version: 'target',
+ diff_outcome: ThreeWayDiffOutcome.StockValueCanUpdate,
+ merge_outcome: ThreeWayMergeOutcome.Merged,
+ has_base_version: true,
+ has_update: true,
+ conflict: ThreeWayDiffConflict.NON_SOLVABLE,
+ },
+ },
+ },
+ }),
+ ];
+
+ const { result } = renderHook(usePrebuiltRulesUpgradeState, {
+ initialProps: ruleUpgradeInfosMock,
+ });
+
+ act(() => {
+ result.current.setRuleFieldResolvedValue({
+ ruleId: 'test-rule-id-1',
+ fieldName: 'name',
+ resolvedValue: 'resolved',
+ });
+ });
+
+ expect(result.current.rulesUpgradeState).toEqual({
+ 'test-rule-id-1': expect.objectContaining({
+ fieldsUpgradeState: {
+ name: { state: FieldUpgradeStateEnum.Accepted, resolvedValue: 'resolved' },
+ },
+ }),
+ });
+ });
+ });
+
+ // Test handling revision and version changes
+ // - user edited a rule (revision change)
+ // - a new prebuilt rules package got released (version change)
+ describe('concurrency control', () => {
+ describe('revision change', () => {
+ const createMock = ({ revision }: { revision: number }) => [
+ createRuleUpgradeInfoMock({
+ rule_id: 'test-rule-id-1',
+ revision,
+ current_rule: createRuleResponseMock({
+ name: 'current',
+ revision,
+ }),
+ diff: {
+ num_fields_with_updates: 1,
+ num_fields_with_conflicts: 1,
+ num_fields_with_non_solvable_conflicts: 1,
+ fields: {
+ name: {
+ base_version: 'base',
+ current_version: 'current',
+ target_version: 'target',
+ merged_version: 'target',
+ diff_outcome: ThreeWayDiffOutcome.StockValueCanUpdate,
+ merge_outcome: ThreeWayMergeOutcome.Merged,
+ has_base_version: true,
+ has_update: true,
+ conflict: ThreeWayDiffConflict.NON_SOLVABLE,
+ },
+ },
+ },
+ }),
+ ];
+
+ it('invalidates resolved conflicts', () => {
+ const { result, rerender } = renderHook(usePrebuiltRulesUpgradeState, {
+ initialProps: createMock({ revision: 1 }),
+ });
+
+ act(() => {
+ result.current.setRuleFieldResolvedValue({
+ ruleId: 'test-rule-id-1',
+ fieldName: 'name',
+ resolvedValue: 'resolved',
+ });
+ });
+
+ expect(result.current.rulesUpgradeState).toEqual({
+ 'test-rule-id-1': expect.objectContaining({
+ fieldsUpgradeState: {
+ name: { state: FieldUpgradeStateEnum.Accepted, resolvedValue: 'resolved' },
+ },
+ }),
+ });
+
+ rerender(createMock({ revision: 2 }));
+
+ expect(result.current.rulesUpgradeState).toEqual({
+ 'test-rule-id-1': expect.objectContaining({
+ fieldsUpgradeState: {
+ name: { state: FieldUpgradeStateEnum.NonSolvableConflict },
+ },
+ }),
+ });
+ });
+
+ it('shows a notification', () => {
+ const addWarningMock = jest.fn();
+ (useAppToasts as jest.Mock).mockImplementation(() => ({
+ addWarning: addWarningMock,
+ }));
+
+ const { result, rerender } = renderHook(usePrebuiltRulesUpgradeState, {
+ initialProps: createMock({ revision: 1 }),
+ });
+
+ act(() => {
+ result.current.setRuleFieldResolvedValue({
+ ruleId: 'test-rule-id-1',
+ fieldName: 'name',
+ resolvedValue: 'resolved',
+ });
+ });
+
+ rerender(createMock({ revision: 2 }));
+
+ expect(addWarningMock).toHaveBeenCalled();
+ });
+ });
+
+ describe('version change', () => {
+ const createMock = ({ version }: { version: number }) => [
+ createRuleUpgradeInfoMock({
+ rule_id: 'test-rule-id-1',
+ revision: 1,
+ target_rule: createRuleResponseMock({
+ name: 'target',
+ version,
+ }),
+ diff: {
+ num_fields_with_updates: 1,
+ num_fields_with_conflicts: 1,
+ num_fields_with_non_solvable_conflicts: 1,
+ fields: {
+ name: {
+ base_version: 'base',
+ current_version: 'current',
+ target_version: 'target',
+ merged_version: 'target',
+ diff_outcome: ThreeWayDiffOutcome.StockValueCanUpdate,
+ merge_outcome: ThreeWayMergeOutcome.Merged,
+ has_base_version: true,
+ has_update: true,
+ conflict: ThreeWayDiffConflict.NON_SOLVABLE,
+ },
+ },
+ },
+ }),
+ ];
+
+ it('invalidates resolved conflicts upon version change', () => {
+ const addWarningMock = jest.fn();
+ (useAppToasts as jest.Mock).mockImplementation(() => ({
+ addWarning: addWarningMock,
+ }));
+
+ const { result, rerender } = renderHook(usePrebuiltRulesUpgradeState, {
+ initialProps: createMock({ version: 1 }),
+ });
+
+ act(() => {
+ result.current.setRuleFieldResolvedValue({
+ ruleId: 'test-rule-id-1',
+ fieldName: 'name',
+ resolvedValue: 'resolved',
+ });
+ });
+
+ expect(result.current.rulesUpgradeState).toEqual({
+ 'test-rule-id-1': expect.objectContaining({
+ fieldsUpgradeState: {
+ name: { state: FieldUpgradeStateEnum.Accepted, resolvedValue: 'resolved' },
+ },
+ }),
+ });
+
+ rerender(createMock({ version: 2 }));
+
+ expect(result.current.rulesUpgradeState).toEqual({
+ 'test-rule-id-1': expect.objectContaining({
+ fieldsUpgradeState: {
+ name: { state: FieldUpgradeStateEnum.NonSolvableConflict },
+ },
+ }),
+ });
+ });
+
+ it('shows a notification', () => {
+ const addWarningMock = jest.fn();
+ (useAppToasts as jest.Mock).mockImplementation(() => ({
+ addWarning: addWarningMock,
+ }));
+
+ const { result, rerender } = renderHook(usePrebuiltRulesUpgradeState, {
+ initialProps: createMock({ version: 1 }),
+ });
+
+ act(() => {
+ result.current.setRuleFieldResolvedValue({
+ ruleId: 'test-rule-id-1',
+ fieldName: 'name',
+ resolvedValue: 'resolved',
+ });
+ });
+
+ expect(result.current.rulesUpgradeState).toEqual({
+ 'test-rule-id-1': expect.objectContaining({
+ fieldsUpgradeState: {
+ name: { state: FieldUpgradeStateEnum.Accepted, resolvedValue: 'resolved' },
+ },
+ }),
+ });
+
+ rerender(createMock({ version: 2 }));
+
+ expect(addWarningMock).toHaveBeenCalled();
+ });
+ });
+ });
+});
+
+function createRuleUpgradeInfoMock(
+ rewrites?: Partial
+): RuleUpgradeInfoForReview {
+ return {
+ id: 'test-id-1',
+ rule_id: 'test-rule-id-1',
+ current_rule: createRuleResponseMock(),
+ target_rule: createRuleResponseMock(),
+ diff: {
+ num_fields_with_updates: 0,
+ num_fields_with_conflicts: 0,
+ num_fields_with_non_solvable_conflicts: 0,
+ fields: {},
+ },
+ revision: 1,
+ ...rewrites,
+ };
+}
+
+function createRuleResponseMock(rewrites?: Partial): RuleResponse {
+ return {
+ version: 1,
+ revision: 1,
+ ...rewrites,
+ } as RuleResponse;
+}
diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade_state.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade_state.ts
index 3412947426301..ed4ef38935ade 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade_state.ts
+++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade_state.ts
@@ -5,7 +5,8 @@
* 2.0.
*/
-import { useCallback, useMemo, useState } from 'react';
+import { useCallback, useMemo, useState, useRef, useEffect } from 'react';
+import { useAppToasts } from '../../../../../common/hooks/use_app_toasts';
import { useIsPrebuiltRulesCustomizationEnabled } from '../../../../rule_management/hooks/use_is_prebuilt_rules_customization_enabled';
import type {
RulesUpgradeState,
@@ -23,10 +24,18 @@ import {
ThreeWayDiffOutcome,
} from '../../../../../../common/api/detection_engine';
import { assertUnreachable } from '../../../../../../common/utility_types';
+import * as i18n from './translations';
type RuleResolvedConflicts = Partial;
type RulesResolvedConflicts = Record;
+interface RuleConcurrencyControl {
+ version: number;
+ revision: number;
+}
+
+type RulesConcurrencyControl = Record;
+
interface UseRulesUpgradeStateResult {
rulesUpgradeState: RulesUpgradeState;
setRuleFieldResolvedValue: SetRuleFieldResolvedValueFn;
@@ -36,11 +45,22 @@ export function usePrebuiltRulesUpgradeState(
ruleUpgradeInfos: RuleUpgradeInfoForReview[]
): UseRulesUpgradeStateResult {
const isPrebuiltRulesCustomizationEnabled = useIsPrebuiltRulesCustomizationEnabled();
- const [rulesResolvedConflicts, setRulesResolvedConflicts] = useState({});
+ const [rulesResolvedValues, setRulesResolvedValues] = useState({});
+ const resetRuleResolvedValues = useCallback(
+ (ruleId: RuleSignatureId) => {
+ setRulesResolvedValues((prevRulesResolvedConflicts) => ({
+ ...prevRulesResolvedConflicts,
+ [ruleId]: {},
+ }));
+ },
+ [setRulesResolvedValues]
+ );
+ const concurrencyControl = useRef({});
+ const { addWarning } = useAppToasts();
const setRuleFieldResolvedValue = useCallback(
(...[params]: Parameters) => {
- setRulesResolvedConflicts((prevRulesResolvedConflicts) => ({
+ setRulesResolvedValues((prevRulesResolvedConflicts) => ({
...prevRulesResolvedConflicts,
[params.ruleId]: {
...(prevRulesResolvedConflicts[params.ruleId] ?? {}),
@@ -51,13 +71,61 @@ export function usePrebuiltRulesUpgradeState(
[]
);
+ // Implements concurrency control.
+ // Rule may be edited or a new prebuilt rules package version gets released.
+ // In any case current rule's `revision` or target rule's version
+ // will have higher values.
+ // Reset resolved conflicts in case of revision`s or version`s mismatch.
+ useEffect(() => {
+ for (const {
+ rule_id: ruleId,
+ current_rule: { revision: nextRevision, name },
+ target_rule: { version: nextVersion },
+ } of ruleUpgradeInfos) {
+ const cc = concurrencyControl.current[ruleId];
+ const hasNewerRevision = cc ? nextRevision > cc.revision : false;
+ const hasNewerVersion = cc ? nextVersion > cc.version : false;
+ const hasResolvedValues = Object.keys(rulesResolvedValues[ruleId] ?? {}).length > 0;
+
+ if (hasNewerRevision && hasResolvedValues) {
+ addWarning({
+ title: i18n.RULE_NEW_REVISION_DETECTED_WARNING,
+ text: i18n.RULE_NEW_REVISION_DETECTED_WARNING_DESCRIPTION(name),
+ });
+ }
+
+ if (hasNewerVersion && hasResolvedValues) {
+ addWarning({
+ title: i18n.RULE_NEW_VERSION_DETECTED_WARNING,
+ text: i18n.RULE_NEW_VERSION_DETECTED_WARNING_DESCRIPTION(name),
+ });
+ }
+
+ if ((hasNewerRevision || hasNewerVersion) && hasResolvedValues) {
+ resetRuleResolvedValues(ruleId);
+ }
+
+ concurrencyControl.current[ruleId] = {
+ version: nextVersion,
+ revision: nextRevision,
+ };
+ }
+ }, [
+ ruleUpgradeInfos,
+ concurrencyControl,
+ rulesResolvedValues,
+ setRulesResolvedValues,
+ resetRuleResolvedValues,
+ addWarning,
+ ]);
+
const rulesUpgradeState = useMemo(() => {
const state: RulesUpgradeState = {};
for (const ruleUpgradeInfo of ruleUpgradeInfos) {
const fieldsUpgradeState = calcFieldsState(
ruleUpgradeInfo.diff.fields,
- rulesResolvedConflicts[ruleUpgradeInfo.rule_id] ?? {}
+ rulesResolvedValues[ruleUpgradeInfo.rule_id] ?? {}
);
const hasRuleTypeChange = Boolean(ruleUpgradeInfo.diff.fields.type);
@@ -77,7 +145,7 @@ export function usePrebuiltRulesUpgradeState(
}
return state;
- }, [ruleUpgradeInfos, rulesResolvedConflicts, isPrebuiltRulesCustomizationEnabled]);
+ }, [ruleUpgradeInfos, rulesResolvedValues, isPrebuiltRulesCustomizationEnabled]);
return {
rulesUpgradeState,
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/host_entity_overview.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/host_entity_overview.tsx
index eb1ca093fbb03..3b47aeaa412e6 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/host_entity_overview.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/host_entity_overview.tsx
@@ -15,7 +15,7 @@ import {
useEuiFontSize,
EuiSkeletonText,
} from '@elastic/eui';
-import { css } from '@emotion/css';
+import { css } from '@emotion/react';
import { getOr } from 'lodash/fp';
import { i18n } from '@kbn/i18n';
import { HOST_NAME_FIELD_NAME } from '../../../../timelines/components/timeline/body/renderers/constants';
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/user_entity_overview.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/user_entity_overview.tsx
index 22f889a61c54e..658eca81219d4 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/user_entity_overview.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/user_entity_overview.tsx
@@ -15,7 +15,7 @@ import {
useEuiFontSize,
EuiSkeletonText,
} from '@elastic/eui';
-import { css } from '@emotion/css';
+import { css } from '@emotion/react';
import { getOr } from 'lodash/fp';
import { i18n } from '@kbn/i18n';
import { useDocumentDetailsContext } from '../../shared/context';
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/components/insight_distribution_bar.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/components/insight_distribution_bar.tsx
index 083738e6766bc..fd6ed66e9b78b 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/components/insight_distribution_bar.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/components/insight_distribution_bar.tsx
@@ -6,7 +6,7 @@
*/
import React, { useMemo } from 'react';
-import { css } from '@emotion/css';
+import { css } from '@emotion/react';
import {
EuiFlexGroup,
EuiFlexItem,
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/components/misconfiguration_insight.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/components/misconfiguration_insight.tsx
index 6ccada3134f9b..74374fe23c6bf 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/components/misconfiguration_insight.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/components/misconfiguration_insight.tsx
@@ -8,7 +8,7 @@
import React, { useEffect, useMemo } from 'react';
import { EuiFlexItem, type EuiFlexGroupProps, useEuiTheme } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
-import { css } from '@emotion/css';
+import { css } from '@emotion/react';
import { useMisconfigurationPreview } from '@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview';
import { buildGenericEntityFlyoutPreviewQuery } from '@kbn/cloud-security-posture-common';
import {
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/components/vulnerabilities_insight.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/components/vulnerabilities_insight.tsx
index 8991124a77e24..125e7e9985edb 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/components/vulnerabilities_insight.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/components/vulnerabilities_insight.tsx
@@ -8,7 +8,7 @@
import React, { useEffect, useMemo } from 'react';
import { EuiFlexItem, type EuiFlexGroupProps, useEuiTheme } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
-import { css } from '@emotion/css';
+import { css } from '@emotion/react';
import { useVulnerabilitiesPreview } from '@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_preview';
import { buildGenericEntityFlyoutPreviewQuery } from '@kbn/cloud-security-posture-common';
import { getVulnerabilityStats, hasVulnerabilitiesData } from '@kbn/cloud-security-posture';
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/components/expandable_panel.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/components/expandable_panel.tsx
index 88318ee2f2cfc..813aa6ad74e8e 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/components/expandable_panel.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/components/expandable_panel.tsx
@@ -20,6 +20,7 @@ import {
useEuiTheme,
EuiToolTip,
EuiSkeletonText,
+ useEuiFontSize,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import type { EuiPanelProps, IconType } from '@elastic/eui';
@@ -126,6 +127,7 @@ export const ExpandablePanel: FC> =
);
const { euiTheme } = useEuiTheme();
+ const xsFontSize = useEuiFontSize('xs').fontSize;
const headerLeftSection = useMemo(
() => (
@@ -159,8 +161,8 @@ export const ExpandablePanel: FC> =
> =
),
[
+ euiTheme.size.xl,
+ euiTheme.size.s,
+ euiTheme.font.weight.bold,
dataTestSubj,
expandable,
children,
toggleIcon,
- link?.callback,
iconType,
- euiTheme.size.s,
- euiTheme.size.xl,
+ link?.callback,
link?.tooltip,
+ xsFontSize,
title,
]
);
diff --git a/x-pack/solutions/security/plugins/security_solution/public/resolver/view/controls/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/resolver/view/controls/index.tsx
index 58146c2c8ce1c..0b9e608c04d7e 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/resolver/view/controls/index.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/resolver/view/controls/index.tsx
@@ -22,7 +22,6 @@ import {
userSetPositionOfCamera,
} from '../../store/camera/action';
import type { State } from '../../../common/store/types';
-import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features';
import { SourcererButton } from './sourcerer_selection';
import { DateSelectionButton } from './date_picker';
import { StyledGraphControls, StyledGraphControlsColumn, StyledEuiRange } from './styles';
@@ -61,9 +60,6 @@ export const GraphControls = React.memo(
selectors.scalingFactor(state.analyzer[id])
);
const { timestamp } = useContext(SideEffectContext);
- const isDatePickerAndSourcererDisabled = useIsExperimentalFeatureEnabled(
- 'analyzerDatePickersAndSourcererDisabled'
- );
const [activePopover, setPopover] = useState(null);
const colorMap = useColors();
@@ -143,22 +139,18 @@ export const GraphControls = React.memo(
isOpen={activePopover === 'nodeLegend'}
setActivePopover={setActivePopover}
/>
- {!isDatePickerAndSourcererDisabled ? (
- <>
-
-
- >
- ) : null}
+
+
{isSplitPanel && showPanelOnClick && (
)}
diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/api/index.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/api/index.ts
index 02fb423b05279..707915f75b124 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/api/index.ts
+++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/api/index.ts
@@ -23,6 +23,8 @@ import {
SIEM_RULE_MIGRATION_RESOURCES_MISSING_PATH,
SIEM_RULE_MIGRATION_RESOURCES_PATH,
SIEM_RULE_MIGRATIONS_PREBUILT_RULES_PATH,
+ SIEM_RULE_MIGRATION_RETRY_PATH,
+ SIEM_RULE_MIGRATIONS_INTEGRATIONS_PATH,
} from '../../../../common/siem_migrations/constants';
import type {
CreateRuleMigrationRequestBody,
@@ -39,6 +41,10 @@ import type {
UpsertRuleMigrationResourcesResponse,
GetRuleMigrationPrebuiltRulesResponse,
UpdateRuleMigrationResponse,
+ RetryRuleMigrationRequestBody,
+ StartRuleMigrationResponse,
+ RetryRuleMigrationResponse,
+ GetRuleMigrationIntegrationsResponse,
} from '../../../../common/siem_migrations/model/api/rules/rule_migration.gen';
export interface GetRuleMigrationStatsParams {
@@ -146,17 +152,54 @@ export const startRuleMigration = async ({
connectorId,
langSmithOptions,
signal,
-}: StartRuleMigrationParams): Promise => {
+}: StartRuleMigrationParams): Promise => {
const body: StartRuleMigrationRequestBody = { connector_id: connectorId };
if (langSmithOptions) {
body.langsmith_options = langSmithOptions;
}
- return KibanaServices.get().http.put(
+ return KibanaServices.get().http.put(
replaceParams(SIEM_RULE_MIGRATION_START_PATH, { migration_id: migrationId }),
{ body: JSON.stringify(body), version: '1', signal }
);
};
+export interface RetryRuleMigrationParams {
+ /** `id` of the migration to reprocess rules for */
+ migrationId: string;
+ /** The connector id to use for the reprocessing */
+ connectorId: string;
+ /** Optional LangSmithOptions to use for the for the reprocessing */
+ langSmithOptions?: LangSmithOptions;
+ /** Optional indicator to retry only failed rules */
+ failed?: boolean;
+ /** Optional indicator to retry only not fully translated rules */
+ notFullyTranslated?: boolean;
+ /** Optional AbortSignal for cancelling request */
+ signal?: AbortSignal;
+}
+/** Starts a reprocessing of migration rules in a specific migration. */
+export const retryRuleMigration = async ({
+ migrationId,
+ connectorId,
+ langSmithOptions,
+ failed,
+ notFullyTranslated,
+ signal,
+}: RetryRuleMigrationParams): Promise => {
+ const body: RetryRuleMigrationRequestBody = {
+ connector_id: connectorId,
+ failed,
+ not_fully_translated: notFullyTranslated,
+ };
+ if (langSmithOptions) {
+ body.langsmith_options = langSmithOptions;
+ }
+ return KibanaServices.get().http.put(
+ replaceParams(SIEM_RULE_MIGRATION_RETRY_PATH, { migration_id: migrationId }),
+ { body: JSON.stringify(body), version: '1', signal }
+ );
+};
+
export interface GetRuleMigrationParams {
/** `id` of the migration to get rules documents for */
migrationId: string;
@@ -279,6 +322,20 @@ export const getRuleMigrationsPrebuiltRules = async ({
);
};
+export interface GetIntegrationsParams {
+ /** Optional AbortSignal for cancelling request */
+ signal?: AbortSignal;
+}
+/** Retrieves existing integrations. */
+export const getIntegrations = async ({
+ signal,
+}: GetIntegrationsParams): Promise => {
+ return KibanaServices.get().http.get(
+ SIEM_RULE_MIGRATIONS_INTEGRATIONS_PATH,
+ { version: '1', signal }
+ );
+};
+
export interface UpdateRulesParams {
/** The list of migration rules data to update */
rulesToUpdate: UpdateRuleMigrationData[];
diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table/bulk_actions.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table/bulk_actions.tsx
index 8f32308ed52c4..60cd2418c8bf8 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table/bulk_actions.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table/bulk_actions.tsx
@@ -6,21 +6,17 @@
*/
import React from 'react';
-import {
- EuiButton,
- EuiButtonEmpty,
- EuiFlexGroup,
- EuiFlexItem,
- EuiLoadingSpinner,
-} from '@elastic/eui';
+import { EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import * as i18n from './translations';
export interface BulkActionsProps {
isTableLoading: boolean;
+ numberOfFailedRules: number;
numberOfTranslatedRules: number;
numberOfSelectedRules: number;
installTranslatedRule?: () => void;
installSelectedRule?: () => void;
+ reprocessFailedRules?: () => void;
}
/**
@@ -29,43 +25,60 @@ export interface BulkActionsProps {
export const BulkActions: React.FC = React.memo(
({
isTableLoading,
+ numberOfFailedRules,
numberOfTranslatedRules,
numberOfSelectedRules,
installTranslatedRule,
installSelectedRule,
+ reprocessFailedRules,
}) => {
const disableInstallTranslatedRulesButton = isTableLoading || !numberOfTranslatedRules;
- const showInstallSelectedRulesButton = isTableLoading || numberOfSelectedRules > 0;
+ const showInstallSelectedRulesButton = numberOfSelectedRules > 0;
+ const showRetryFailedRulesButton = numberOfFailedRules > 0;
return (
- {showInstallSelectedRulesButton ? (
+ {showInstallSelectedRulesButton && (
installSelectedRule?.()}
disabled={isTableLoading}
+ isLoading={isTableLoading}
data-test-subj="installSelectedRulesButton"
aria-label={i18n.INSTALL_SELECTED_ARIA_LABEL}
>
{i18n.INSTALL_SELECTED_RULES(numberOfSelectedRules)}
- {isTableLoading && }
- ) : null}
+ )}
+ {showRetryFailedRulesButton && (
+
+ reprocessFailedRules?.()}
+ disabled={isTableLoading}
+ isLoading={isTableLoading}
+ data-test-subj="reprocessFailedRulesButton"
+ aria-label={i18n.REPROCESS_FAILED_ARIA_LABEL}
+ >
+ {i18n.REPROCESS_FAILED_RULES(numberOfFailedRules)}
+
+
+ )}
installTranslatedRule?.()}
disabled={disableInstallTranslatedRulesButton}
+ isLoading={isTableLoading}
+ data-test-subj="installTranslatedRulesButton"
aria-label={i18n.INSTALL_TRANSLATED_ARIA_LABEL}
>
{numberOfTranslatedRules > 0
? i18n.INSTALL_TRANSLATED_RULES(numberOfTranslatedRules)
: i18n.INSTALL_TRANSLATED_RULES_EMPTY_STATE}
- {isTableLoading && }
diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table/index.tsx
index b883934a0bdcb..94101d3fbf540 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table/index.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table/index.tsx
@@ -18,6 +18,7 @@ import {
} from '@elastic/eui';
import React, { useCallback, useMemo, useState } from 'react';
+import type { RelatedIntegration, RuleResponse } from '../../../../../common/api/detection_engine';
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
import type { RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen';
import { EmptyMigration } from './empty_migration';
@@ -33,6 +34,7 @@ import { BulkActions } from './bulk_actions';
import { SearchField } from './search_field';
import { RuleTranslationResult } from '../../../../../common/siem_migrations/constants';
import * as i18n from './translations';
+import { useRetryRuleMigration } from '../../service/hooks/use_retry_rules';
const DEFAULT_PAGE_SIZE = 10;
const DEFAULT_SORT_FIELD = 'translation_result';
@@ -43,13 +45,28 @@ export interface MigrationRulesTableProps {
* Selected rule migration id
*/
migrationId: string;
+
+ /**
+ * Re-fetches latest rule migration data
+ */
+ refetchData?: () => void;
+
+ /**
+ * Existing integrations.
+ */
+ integrations?: Record;
+
+ /**
+ * Indicates whether the integrations loading is in progress.
+ */
+ isIntegrationsLoading?: boolean;
}
/**
* Table Component for displaying SIEM rules migrations
*/
export const MigrationRulesTable: React.FC = React.memo(
- ({ migrationId }) => {
+ ({ migrationId, refetchData, integrations, isIntegrationsLoading }) => {
const { addError } = useAppToasts();
const [pageIndex, setPageIndex] = useState(0);
@@ -132,6 +149,7 @@ export const MigrationRulesTable: React.FC = React.mem
const { mutateAsync: installMigrationRules } = useInstallMigrationRules(migrationId);
const { mutateAsync: installTranslatedMigrationRules } =
useInstallTranslatedMigrationRules(migrationId);
+ const { retryRuleMigration, isLoading: isRetryLoading } = useRetryRuleMigration(refetchData);
const [isTableLoading, setTableLoading] = useState(false);
const installSingleRule = useCallback(
@@ -180,7 +198,12 @@ export const MigrationRulesTable: React.FC = React.mem
[addError, installTranslatedMigrationRules]
);
- const isLoading = isStatsLoading || isPrebuiltRulesLoading || isDataLoading || isTableLoading;
+ const reprocessFailedRules = useCallback(async () => {
+ retryRuleMigration(migrationId, { failed: true });
+ }, [migrationId, retryRuleMigration]);
+
+ const isLoading =
+ isStatsLoading || isPrebuiltRulesLoading || isDataLoading || isTableLoading || isRetryLoading;
const ruleActionsFactory = useCallback(
(ruleMigration: RuleMigration, closeRulePreview: () => void) => {
@@ -221,13 +244,35 @@ export const MigrationRulesTable: React.FC = React.mem
[installSingleRule, isLoading]
);
- const getMigrationRule = useCallback(
+ const getMigrationRuleData = useCallback(
(ruleId: string) => {
if (!isLoading && ruleMigrations.length) {
- return ruleMigrations.find((item) => item.id === ruleId);
+ const ruleMigration = ruleMigrations.find((item) => item.id === ruleId);
+ let matchedPrebuiltRule: RuleResponse | undefined;
+ const relatedIntegrations: RelatedIntegration[] = [];
+ if (ruleMigration) {
+ // Find matched prebuilt rule if any and prioritize its installed version
+ const matchedPrebuiltRuleVersion = ruleMigration.elastic_rule?.prebuilt_rule_id
+ ? prebuiltRules[ruleMigration.elastic_rule.prebuilt_rule_id]
+ : undefined;
+ matchedPrebuiltRule =
+ matchedPrebuiltRuleVersion?.current ?? matchedPrebuiltRuleVersion?.target;
+
+ if (integrations) {
+ if (matchedPrebuiltRule?.related_integrations) {
+ relatedIntegrations.push(...matchedPrebuiltRule.related_integrations);
+ } else if (ruleMigration.elastic_rule?.integration_id) {
+ const integration = integrations[ruleMigration.elastic_rule.integration_id];
+ if (integration) {
+ relatedIntegrations.push(integration);
+ }
+ }
+ }
+ }
+ return { ruleMigration, matchedPrebuiltRule, relatedIntegrations, isIntegrationsLoading };
}
},
- [isLoading, ruleMigrations]
+ [integrations, isIntegrationsLoading, isLoading, prebuiltRules, ruleMigrations]
);
const {
@@ -235,8 +280,7 @@ export const MigrationRulesTable: React.FC = React.mem
openMigrationRuleDetails: openRulePreview,
} = useMigrationRuleDetailsFlyout({
isLoading,
- prebuiltRules,
- getMigrationRule,
+ getMigrationRuleData,
ruleActionsFactory,
});
@@ -244,6 +288,7 @@ export const MigrationRulesTable: React.FC = React.mem
disableActions: isTableLoading,
openMigrationRuleDetails: openRulePreview,
installMigrationRule: installSingleRule,
+ getMigrationRuleData,
});
return (
@@ -268,10 +313,12 @@ export const MigrationRulesTable: React.FC = React.mem
diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table/translations.ts
index 79b5a1fe00900..b553b6dfd7358 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table/translations.ts
+++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table/translations.ts
@@ -49,6 +49,13 @@ export const INSTALL_SELECTED_RULES = (numberOfSelectedRules: number) => {
});
};
+export const REPROCESS_FAILED_RULES = (numberOfFailedRules: number) => {
+ return i18n.translate('xpack.securitySolution.siemMigrations.rules.table.reprocessFailedRules', {
+ defaultMessage: 'Reprocess rules ({numberOfFailedRules})',
+ values: { numberOfFailedRules },
+ });
+};
+
export const INSTALL_TRANSLATED_RULES_EMPTY_STATE = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.table.installTranslatedRulesEmptyState',
{
@@ -81,6 +88,13 @@ export const INSTALL_TRANSLATED_ARIA_LABEL = i18n.translate(
}
);
+export const REPROCESS_FAILED_ARIA_LABEL = i18n.translate(
+ 'xpack.securitySolution.siemMigrations.rules.table.reprocessFailedRulesButtonAriaLabel',
+ {
+ defaultMessage: 'Reprocess failed rules',
+ }
+);
+
export const ALREADY_TRANSLATED_RULE_TOOLTIP = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.table.alreadyTranslatedTooltip',
{
diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/actions.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/actions.tsx
index 45de70582d4b1..a8175cbffc4db 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/actions.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/actions.tsx
@@ -7,12 +7,19 @@
import React from 'react';
import { EuiLink } from '@elastic/eui';
+import {
+ RuleTranslationResult,
+ SiemMigrationStatus,
+} from '../../../../../common/siem_migrations/constants';
import { getRuleDetailsUrl } from '../../../../common/components/link_to';
import { useKibana } from '../../../../common/lib/kibana';
import { APP_UI_ID, SecurityPageName } from '../../../../../common';
-import type { RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen';
+import {
+ RuleMigrationStatusEnum,
+ type RuleMigration,
+} from '../../../../../common/siem_migrations/model/rule_migration.gen';
import * as i18n from './translations';
-import type { TableColumn } from './constants';
+import { type TableColumn } from './constants';
interface ActionNameProps {
disableActions?: boolean;
@@ -46,7 +53,7 @@ const ActionName = ({
);
}
- if (migrationRule.status === 'failed') {
+ if (migrationRule.status === SiemMigrationStatus.FAILED) {
return (
{}} data-test-subj="restartRule">
{i18n.ACTIONS_RESTART_LABEL}
@@ -54,7 +61,7 @@ const ActionName = ({
);
}
- if (migrationRule.translation_result === 'full') {
+ if (migrationRule.translation_result === RuleTranslationResult.FULL) {
return (
{
- return (
+ render: (_, rule: RuleMigration) => {
+ return rule.status === RuleMigrationStatusEnum.failed ? null : (
diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/author.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/author.tsx
index 23980f5612f89..6f14cfd550346 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/author.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/author.tsx
@@ -7,9 +7,10 @@
import React from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui';
-import type { RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen';
+import { SiemMigrationStatus } from '../../../../../common/siem_migrations/constants';
+import { type RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen';
import * as i18n from './translations';
-import type { TableColumn } from './constants';
+import { COLUMN_EMPTY_VALUE, type TableColumn } from './constants';
const Author = ({ isPrebuiltRule }: { isPrebuiltRule: boolean }) => {
return (
@@ -31,9 +32,14 @@ export const createAuthorColumn = (): TableColumn => {
field: 'elastic_rule.prebuilt_rule_id',
name: i18n.COLUMN_AUTHOR,
render: (_, rule: RuleMigration) => {
- return ;
+ return rule.status === SiemMigrationStatus.FAILED ? (
+ <>{COLUMN_EMPTY_VALUE}>
+ ) : (
+
+ );
},
sortable: true,
+ truncateText: true,
width: '10%',
};
};
diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/constants.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/constants.tsx
index 724e4dcb101a1..1576b13f76dc6 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/constants.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/constants.tsx
@@ -9,3 +9,5 @@ import type { EuiBasicTableColumn } from '@elastic/eui';
import type { RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen';
export type TableColumn = EuiBasicTableColumn;
+
+export const COLUMN_EMPTY_VALUE = '-';
diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/integrations.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/integrations.tsx
new file mode 100644
index 0000000000000..43b7086c9814c
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/integrations.tsx
@@ -0,0 +1,41 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { EuiLoadingSpinner } from '@elastic/eui';
+import type { RelatedIntegration } from '../../../../../common/api/detection_engine';
+import { IntegrationsPopover } from '../../../../detections/components/rules/related_integrations/integrations_popover';
+import type { RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen';
+import * as i18n from './translations';
+import type { TableColumn } from './constants';
+
+export const createIntegrationsColumn = ({
+ getMigrationRuleData,
+}: {
+ getMigrationRuleData: (
+ ruleId: string
+ ) => { relatedIntegrations?: RelatedIntegration[]; isIntegrationsLoading?: boolean } | undefined;
+}): TableColumn => {
+ return {
+ field: 'elastic_rule.integration_id',
+ name: i18n.COLUMN_INTEGRATIONS,
+ render: (_, rule: RuleMigration) => {
+ const migrationRuleData = getMigrationRuleData(rule.id);
+ if (migrationRuleData?.isIntegrationsLoading) {
+ return ;
+ }
+ const relatedIntegrations = migrationRuleData?.relatedIntegrations;
+ if (relatedIntegrations == null || relatedIntegrations.length === 0) {
+ return null;
+ }
+ return ;
+ },
+ truncateText: true,
+ width: '143px',
+ align: 'center',
+ };
+};
diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/name.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/name.tsx
index ce0e1d3c99d8d..56f4123403c97 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/name.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/name.tsx
@@ -7,7 +7,8 @@
import React from 'react';
import { EuiLink, EuiText } from '@elastic/eui';
-import type { RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen';
+import { SiemMigrationStatus } from '../../../../../common/siem_migrations/constants';
+import { type RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen';
import * as i18n from './translations';
import type { TableColumn } from './constants';
@@ -17,7 +18,7 @@ interface NameProps {
}
const Name = ({ rule, openMigrationRuleDetails }: NameProps) => {
- if (!rule.elastic_rule) {
+ if (rule.status === SiemMigrationStatus.FAILED) {
return (
{rule.original_rule.title}
@@ -31,7 +32,7 @@ const Name = ({ rule, openMigrationRuleDetails }: NameProps) => {
}}
data-test-subj="ruleName"
>
- {rule.elastic_rule.title}
+ {rule.elastic_rule?.title ?? rule.original_rule.title}
);
};
diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/risk_score.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/risk_score.tsx
index 0fb78ae8bf709..d0584cc14e2af 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/risk_score.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/risk_score.tsx
@@ -7,17 +7,23 @@
import React from 'react';
import { EuiText } from '@elastic/eui';
-import { DEFAULT_TRANSLATION_RISK_SCORE } from '../../../../../common/siem_migrations/constants';
+import { type RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen';
+import {
+ DEFAULT_TRANSLATION_RISK_SCORE,
+ SiemMigrationStatus,
+} from '../../../../../common/siem_migrations/constants';
import * as i18n from './translations';
-import type { TableColumn } from './constants';
+import { COLUMN_EMPTY_VALUE, type TableColumn } from './constants';
export const createRiskScoreColumn = (): TableColumn => {
return {
field: 'risk_score',
name: i18n.COLUMN_RISK_SCORE,
- render: () => (
+ render: (_, rule: RuleMigration) => (
- {DEFAULT_TRANSLATION_RISK_SCORE}
+ {rule.status === SiemMigrationStatus.FAILED
+ ? COLUMN_EMPTY_VALUE
+ : DEFAULT_TRANSLATION_RISK_SCORE}
),
sortable: true,
diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/severity.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/severity.tsx
index 9a6c0b98ff317..2a97288eef267 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/severity.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/severity.tsx
@@ -7,16 +7,25 @@
import React from 'react';
import type { Severity } from '@kbn/securitysolution-io-ts-alerting-types';
-import { DEFAULT_TRANSLATION_SEVERITY } from '../../../../../common/siem_migrations/constants';
+import { type RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen';
+import {
+ DEFAULT_TRANSLATION_SEVERITY,
+ SiemMigrationStatus,
+} from '../../../../../common/siem_migrations/constants';
import { SeverityBadge } from '../../../../common/components/severity_badge';
-import type { TableColumn } from './constants';
+import { COLUMN_EMPTY_VALUE, type TableColumn } from './constants';
import * as i18n from './translations';
export const createSeverityColumn = (): TableColumn => {
return {
field: 'elastic_rule.severity',
name: i18n.COLUMN_SEVERITY,
- render: (value?: Severity) => ,
+ render: (value: Severity = DEFAULT_TRANSLATION_SEVERITY, rule: RuleMigration) =>
+ rule.status === SiemMigrationStatus.FAILED ? (
+ <>{COLUMN_EMPTY_VALUE}>
+ ) : (
+
+ ),
sortable: true,
truncateText: true,
width: '12%',
diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/translations.ts
index 64e459a609143..5b4fd6d6a477e 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/translations.ts
+++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/translations.ts
@@ -97,3 +97,10 @@ export const COLUMN_UPDATED = i18n.translate(
defaultMessage: 'Updated',
}
);
+
+export const COLUMN_INTEGRATIONS = i18n.translate(
+ 'xpack.securitySolution.siemMigrations.rules.tableColumn.integrationsLabel',
+ {
+ defaultMessage: 'Integrations',
+ }
+);
diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/updated.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/updated.tsx
index cec9f86eb7bde..aaf4e75ac4917 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/updated.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/updated.tsx
@@ -19,7 +19,7 @@ export const createUpdatedColumn = (): TableColumn => {
),
sortable: true,
- truncateText: false,
+ truncateText: true,
align: 'center',
width: '10%',
};
diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/hooks/use_migration_rule_preview_flyout.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/hooks/use_migration_rule_preview_flyout.tsx
index 4efaa4aba7181..9dad5a30ab073 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/hooks/use_migration_rule_preview_flyout.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/hooks/use_migration_rule_preview_flyout.tsx
@@ -8,16 +8,18 @@
import type { ReactNode } from 'react';
import React, { useCallback, useState, useMemo } from 'react';
import type { EuiTabbedContentTab } from '@elastic/eui';
-import type {
- PrebuiltRuleVersion,
- RuleMigration,
-} from '../../../../common/siem_migrations/model/rule_migration.gen';
+import type { RuleResponse } from '../../../../common/api/detection_engine';
+import type { RuleMigration } from '../../../../common/siem_migrations/model/rule_migration.gen';
import { MigrationRuleDetailsFlyout } from '../components/rule_details_flyout';
interface UseMigrationRuleDetailsFlyoutParams {
isLoading?: boolean;
- prebuiltRules: Record;
- getMigrationRule: (ruleId: string) => RuleMigration | undefined;
+ getMigrationRuleData: (ruleId: string) =>
+ | {
+ ruleMigration?: RuleMigration;
+ matchedPrebuiltRule?: RuleResponse;
+ }
+ | undefined;
ruleActionsFactory: (ruleMigration: RuleMigration, closeRulePreview: () => void) => ReactNode;
extraTabsFactory?: (ruleMigration: RuleMigration) => EuiTabbedContentTab[];
}
@@ -30,27 +32,17 @@ interface UseMigrationRuleDetailsFlyoutResult {
export function useMigrationRuleDetailsFlyout({
isLoading,
- prebuiltRules,
- getMigrationRule,
+ getMigrationRuleData,
extraTabsFactory,
ruleActionsFactory,
}: UseMigrationRuleDetailsFlyoutParams): UseMigrationRuleDetailsFlyoutResult {
const [migrationRuleId, setMigrationRuleId] = useState();
- const ruleMigration = useMemo(() => {
+ const migrationRuleData = useMemo(() => {
if (migrationRuleId) {
- return getMigrationRule(migrationRuleId);
+ return getMigrationRuleData(migrationRuleId);
}
- }, [getMigrationRule, migrationRuleId]);
- const matchedPrebuiltRule = useMemo(() => {
- if (ruleMigration) {
- // Find matched prebuilt rule if any and prioritize its installed version
- const matchedPrebuiltRuleVersion = ruleMigration.elastic_rule?.prebuilt_rule_id
- ? prebuiltRules[ruleMigration.elastic_rule.prebuilt_rule_id]
- : undefined;
- return matchedPrebuiltRuleVersion?.current ?? matchedPrebuiltRuleVersion?.target;
- }
- }, [prebuiltRules, ruleMigration]);
+ }, [getMigrationRuleData, migrationRuleId]);
const openMigrationRuleDetails = useCallback((rule: RuleMigration) => {
setMigrationRuleId(rule.id);
@@ -58,19 +50,24 @@ export function useMigrationRuleDetailsFlyout({
const closeMigrationRuleDetails = useCallback(() => setMigrationRuleId(undefined), []);
const ruleActions = useMemo(
- () => ruleMigration && ruleActionsFactory(ruleMigration, closeMigrationRuleDetails),
- [ruleMigration, ruleActionsFactory, closeMigrationRuleDetails]
+ () =>
+ migrationRuleData?.ruleMigration &&
+ ruleActionsFactory(migrationRuleData.ruleMigration, closeMigrationRuleDetails),
+ [migrationRuleData?.ruleMigration, ruleActionsFactory, closeMigrationRuleDetails]
);
const extraTabs = useMemo(
- () => (ruleMigration && extraTabsFactory ? extraTabsFactory(ruleMigration) : []),
- [ruleMigration, extraTabsFactory]
+ () =>
+ migrationRuleData?.ruleMigration && extraTabsFactory
+ ? extraTabsFactory(migrationRuleData.ruleMigration)
+ : [],
+ [extraTabsFactory, migrationRuleData?.ruleMigration]
);
return {
- migrationRuleDetailsFlyout: ruleMigration && (
+ migrationRuleDetailsFlyout: migrationRuleData?.ruleMigration && (
void;
installMigrationRule: (migrationRule: RuleMigration, enable?: boolean) => void;
+ getMigrationRuleData: (
+ ruleId: string
+ ) => { relatedIntegrations?: RelatedIntegration[]; isIntegrationsLoading?: boolean } | undefined;
}): TableColumn[] => {
return useMemo(
() => [
@@ -35,12 +41,13 @@ export const useMigrationRulesTableColumns = ({
createRiskScoreColumn(),
createSeverityColumn(),
createAuthorColumn(),
+ createIntegrationsColumn({ getMigrationRuleData }),
createActionsColumn({
disableActions,
openMigrationRuleDetails,
installMigrationRule,
}),
],
- [disableActions, installMigrationRule, openMigrationRuleDetails]
+ [disableActions, getMigrationRuleData, installMigrationRule, openMigrationRuleDetails]
);
};
diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/translations.ts
index ef3521fd37301..e83293ec61097 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/translations.ts
+++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/translations.ts
@@ -41,3 +41,10 @@ export const UPDATE_MIGRATION_RULES_FAILURE = i18n.translate(
defaultMessage: 'Failed to update migration rules',
}
);
+
+export const RETRY_FAILED_RULES_FAILURE = i18n.translate(
+ 'xpack.securitySolution.siemMigrations.rules.retryFailedRulesFailDescription',
+ {
+ defaultMessage: 'Failed to reprocess migration rules',
+ }
+);
diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/use_get_migration_rules.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/use_get_migration_rules.ts
index b06f041e2c58e..ffa382e5a3a40 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/use_get_migration_rules.ts
+++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/use_get_migration_rules.ts
@@ -52,20 +52,23 @@ export const useGetMigrationRules = (params: {
*
* @returns A rule migrations cache invalidation callback
*/
-export const useInvalidateGetMigrationRules = (migrationId: string) => {
+export const useInvalidateGetMigrationRules = () => {
const queryClient = useQueryClient();
- const SPECIFIC_MIGRATION_PATH = replaceParams(SIEM_RULE_MIGRATION_PATH, {
- migration_id: migrationId,
- });
+ return useCallback(
+ (migrationId: string) => {
+ const SPECIFIC_MIGRATION_PATH = replaceParams(SIEM_RULE_MIGRATION_PATH, {
+ migration_id: migrationId,
+ });
- return useCallback(() => {
- /**
- * Invalidate all queries that start with SPECIFIC_MIGRATION_PATH. This
- * includes the in-memory query cache and paged query cache.
- */
- queryClient.invalidateQueries(['GET', SPECIFIC_MIGRATION_PATH], {
- refetchType: 'active',
- });
- }, [SPECIFIC_MIGRATION_PATH, queryClient]);
+ /**
+ * Invalidate all queries that start with SPECIFIC_MIGRATION_PATH. This
+ * includes the in-memory query cache and paged query cache.
+ */
+ queryClient.invalidateQueries(['GET', SPECIFIC_MIGRATION_PATH], {
+ refetchType: 'active',
+ });
+ },
+ [queryClient]
+ );
};
diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/use_get_migration_translation_stats.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/use_get_migration_translation_stats.ts
index b19a1133e3061..0111c3382cfe2 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/use_get_migration_translation_stats.ts
+++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/use_get_migration_translation_stats.ts
@@ -44,19 +44,22 @@ export const useGetMigrationTranslationStats = (migrationId: string) => {
*
* @returns A translation stats cache invalidation callback
*/
-export const useInvalidateGetMigrationTranslationStats = (migrationId: string) => {
+export const useInvalidateGetMigrationTranslationStats = () => {
const queryClient = useQueryClient();
- const SPECIFIC_MIGRATION_TRANSLATION_PATH = replaceParams(
- SIEM_RULE_MIGRATION_TRANSLATION_STATS_PATH,
- {
- migration_id: migrationId,
- }
- );
+ return useCallback(
+ (migrationId: string) => {
+ const SPECIFIC_MIGRATION_TRANSLATION_PATH = replaceParams(
+ SIEM_RULE_MIGRATION_TRANSLATION_STATS_PATH,
+ {
+ migration_id: migrationId,
+ }
+ );
- return useCallback(() => {
- queryClient.invalidateQueries(['GET', SPECIFIC_MIGRATION_TRANSLATION_PATH], {
- refetchType: 'active',
- });
- }, [SPECIFIC_MIGRATION_TRANSLATION_PATH, queryClient]);
+ queryClient.invalidateQueries(['GET', SPECIFIC_MIGRATION_TRANSLATION_PATH], {
+ refetchType: 'active',
+ });
+ },
+ [queryClient]
+ );
};
diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/use_install_migration_rules.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/use_install_migration_rules.ts
index 2b28b3b944990..b69be3b86d11c 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/use_install_migration_rules.ts
+++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/use_install_migration_rules.ts
@@ -19,9 +19,8 @@ export const INSTALL_MIGRATION_RULES_MUTATION_KEY = ['POST', SIEM_RULE_MIGRATION
export const useInstallMigrationRules = (migrationId: string) => {
const { addError } = useAppToasts();
- const invalidateGetRuleMigrations = useInvalidateGetMigrationRules(migrationId);
- const invalidateGetMigrationTranslationStats =
- useInvalidateGetMigrationTranslationStats(migrationId);
+ const invalidateGetRuleMigrations = useInvalidateGetMigrationRules();
+ const invalidateGetMigrationTranslationStats = useInvalidateGetMigrationTranslationStats();
return useMutation(
({ ids, enabled = false }) => installMigrationRules({ migrationId, ids, enabled }),
@@ -31,8 +30,8 @@ export const useInstallMigrationRules = (migrationId: string) => {
addError(error, { title: i18n.INSTALL_MIGRATION_RULES_FAILURE });
},
onSettled: () => {
- invalidateGetRuleMigrations();
- invalidateGetMigrationTranslationStats();
+ invalidateGetRuleMigrations(migrationId);
+ invalidateGetMigrationTranslationStats(migrationId);
},
}
);
diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/use_install_translated_migration_rules.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/use_install_translated_migration_rules.ts
index b0d9e11136396..bcce981a4cfb5 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/use_install_translated_migration_rules.ts
+++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/use_install_translated_migration_rules.ts
@@ -22,9 +22,8 @@ export const INSTALL_ALL_MIGRATION_RULES_MUTATION_KEY = [
export const useInstallTranslatedMigrationRules = (migrationId: string) => {
const { addError } = useAppToasts();
- const invalidateGetRuleMigrations = useInvalidateGetMigrationRules(migrationId);
- const invalidateGetMigrationTranslationStats =
- useInvalidateGetMigrationTranslationStats(migrationId);
+ const invalidateGetRuleMigrations = useInvalidateGetMigrationRules();
+ const invalidateGetMigrationTranslationStats = useInvalidateGetMigrationTranslationStats();
return useMutation(
() => installTranslatedMigrationRules({ migrationId }),
@@ -34,8 +33,8 @@ export const useInstallTranslatedMigrationRules = (migrationId: string) => {
addError(error, { title: i18n.INSTALL_MIGRATION_RULES_FAILURE });
},
onSettled: () => {
- invalidateGetRuleMigrations();
- invalidateGetMigrationTranslationStats();
+ invalidateGetRuleMigrations(migrationId);
+ invalidateGetMigrationTranslationStats(migrationId);
},
}
);
diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/use_update_migration_rules.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/use_update_migration_rules.ts
index 1e0fa22c466f0..2c39da63f8374 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/use_update_migration_rules.ts
+++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/use_update_migration_rules.ts
@@ -20,9 +20,8 @@ export const UPDATE_MIGRATION_RULES_MUTATION_KEY = ['PUT', SIEM_RULE_MIGRATIONS_
export const useUpdateMigrationRules = (migrationId: string) => {
const { addError } = useAppToasts();
- const invalidateGetRuleMigrations = useInvalidateGetMigrationRules(migrationId);
- const invalidateGetMigrationTranslationStats =
- useInvalidateGetMigrationTranslationStats(migrationId);
+ const invalidateGetRuleMigrations = useInvalidateGetMigrationRules();
+ const invalidateGetMigrationTranslationStats = useInvalidateGetMigrationTranslationStats();
return useMutation(
(rulesToUpdate) => updateMigrationRules({ rulesToUpdate }),
@@ -32,8 +31,8 @@ export const useUpdateMigrationRules = (migrationId: string) => {
addError(error, { title: i18n.UPDATE_MIGRATION_RULES_FAILURE });
},
onSettled: () => {
- invalidateGetRuleMigrations();
- invalidateGetMigrationTranslationStats();
+ invalidateGetRuleMigrations(migrationId);
+ invalidateGetMigrationTranslationStats(migrationId);
},
}
);
diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/pages/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/pages/index.tsx
index 3877a6f46cbe7..af4e2abf5ebd7 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/pages/index.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/pages/index.tsx
@@ -5,10 +5,11 @@
* 2.0.
*/
-import React, { useEffect, useMemo } from 'react';
+import React, { useCallback, useEffect, useMemo } from 'react';
import { EuiSkeletonLoading, EuiSkeletonText, EuiSkeletonTitle } from '@elastic/eui';
import type { RouteComponentProps } from 'react-router-dom';
+import type { RelatedIntegration } from '../../../../common/api/detection_engine';
import { SiemMigrationTaskStatus } from '../../../../common/siem_migrations/constants';
import { useNavigation } from '../../../common/lib/kibana';
import { HeaderPage } from '../../../common/components/header_page';
@@ -22,6 +23,12 @@ import { MissingPrivilegesCallOut } from '../../../detections/components/callout
import { HeaderButtons } from '../components/header_buttons';
import { UnknownMigration } from '../components/unknown_migration';
import { useLatestStats } from '../service/hooks/use_latest_stats';
+import { RuleMigrationDataInputWrapper } from '../components/data_input_flyout/data_input_wrapper';
+import { MigrationReadyPanel } from '../components/migration_status_panels/migration_ready_panel';
+import { MigrationProgressPanel } from '../components/migration_status_panels/migration_progress_panel';
+import { useInvalidateGetMigrationRules } from '../logic/use_get_migration_rules';
+import { useInvalidateGetMigrationTranslationStats } from '../logic/use_get_migration_translation_stats';
+import { useGetIntegrations } from '../service/hooks/use_get_integrations';
type MigrationRulesPageProps = RouteComponentProps<{ migrationId?: string }>;
@@ -32,25 +39,25 @@ export const MigrationRulesPage: React.FC = React.memo(
},
}) => {
const { navigateTo } = useNavigation();
+ const { data: ruleMigrationsStats, isLoading, refreshStats } = useLatestStats();
- const { data: ruleMigrationsStatsAll, isLoading: isLoadingMigrationsStats } = useLatestStats();
+ const [integrations, setIntegrations] = React.useState<
+ Record | undefined
+ >();
+ const { getIntegrations, isLoading: isIntegrationsLoading } =
+ useGetIntegrations(setIntegrations);
- const finishedRuleMigrationsStats = useMemo(() => {
- if (isLoadingMigrationsStats || !ruleMigrationsStatsAll?.length) {
- return [];
- }
- return ruleMigrationsStatsAll.filter(
- (migration) => migration.status === SiemMigrationTaskStatus.FINISHED
- );
- }, [isLoadingMigrationsStats, ruleMigrationsStatsAll]);
+ useEffect(() => {
+ getIntegrations();
+ }, [getIntegrations]);
useEffect(() => {
- if (isLoadingMigrationsStats) {
+ if (isLoading) {
return;
}
// Navigate to landing page if there are no migrations
- if (!finishedRuleMigrationsStats.length) {
+ if (!ruleMigrationsStats.length) {
navigateTo({ deepLinkId: SecurityPageName.landing, path: 'siem_migrations' });
return;
}
@@ -59,21 +66,59 @@ export const MigrationRulesPage: React.FC = React.memo(
if (!migrationId) {
navigateTo({
deepLinkId: SecurityPageName.siemMigrationsRules,
- path: finishedRuleMigrationsStats[0].id,
+ path: ruleMigrationsStats[0].id,
});
}
- }, [isLoadingMigrationsStats, migrationId, finishedRuleMigrationsStats, navigateTo]);
+ }, [isLoading, migrationId, navigateTo, ruleMigrationsStats]);
const onMigrationIdChange = (selectedId?: string) => {
navigateTo({ deepLinkId: SecurityPageName.siemMigrationsRules, path: selectedId });
};
+ const invalidateGetRuleMigrations = useInvalidateGetMigrationRules();
+ const invalidateGetMigrationTranslationStats = useInvalidateGetMigrationTranslationStats();
+ const refetchData = useCallback(() => {
+ if (!migrationId) {
+ return;
+ }
+ refreshStats();
+ invalidateGetRuleMigrations(migrationId);
+ invalidateGetMigrationTranslationStats(migrationId);
+ }, [
+ invalidateGetMigrationTranslationStats,
+ invalidateGetRuleMigrations,
+ migrationId,
+ refreshStats,
+ ]);
+
const content = useMemo(() => {
- if (!migrationId || !finishedRuleMigrationsStats.some((stats) => stats.id === migrationId)) {
+ const migrationStats = ruleMigrationsStats.find((stats) => stats.id === migrationId);
+ if (!migrationId || !migrationStats) {
return ;
}
- return ;
- }, [migrationId, finishedRuleMigrationsStats]);
+ if (migrationStats.status === SiemMigrationTaskStatus.FINISHED) {
+ return (
+
+ );
+ }
+ return (
+
+ <>
+ {migrationStats.status === SiemMigrationTaskStatus.READY && (
+
+ )}
+ {migrationStats.status === SiemMigrationTaskStatus.RUNNING && (
+
+ )}
+ >
+
+ );
+ }, [migrationId, refetchData, ruleMigrationsStats, integrations, isIntegrationsLoading]);
return (
<>
@@ -83,13 +128,13 @@ export const MigrationRulesPage: React.FC = React.memo(
diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_get_integrations.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_get_integrations.ts
new file mode 100644
index 0000000000000..8ed94e78f31c4
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_get_integrations.ts
@@ -0,0 +1,42 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { useCallback, useReducer } from 'react';
+import { i18n } from '@kbn/i18n';
+import type { RelatedIntegration } from '../../../../../common/api/detection_engine';
+import { useKibana } from '../../../../common/lib/kibana/kibana_react';
+import { reducer, initialState } from './common/api_request_reducer';
+
+export const GET_INTEGRATIONS_ERROR = i18n.translate(
+ 'xpack.securitySolution.siemMigrations.rules.service.getIntegrationsError',
+ { defaultMessage: 'Failed to fetch integrations' }
+);
+
+export type OnSuccess = (integrations: Record) => void;
+
+export const useGetIntegrations = (onSuccess: OnSuccess) => {
+ const { siemMigrations, notifications } = useKibana().services;
+ const [state, dispatch] = useReducer(reducer, initialState);
+
+ const getIntegrations = useCallback(() => {
+ (async () => {
+ try {
+ dispatch({ type: 'start' });
+ const integrations = await siemMigrations.rules.getIntegrations();
+
+ onSuccess(integrations);
+ dispatch({ type: 'success' });
+ } catch (err) {
+ const apiError = err.body ?? err;
+ notifications.toasts.addError(apiError, { title: GET_INTEGRATIONS_ERROR });
+ dispatch({ type: 'error', error: apiError });
+ }
+ })();
+ }, [siemMigrations.rules, notifications.toasts, onSuccess]);
+
+ return { isLoading: state.loading, error: state.error, getIntegrations };
+};
diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_retry_rules.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_retry_rules.ts
new file mode 100644
index 0000000000000..6755d2da738a6
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_retry_rules.ts
@@ -0,0 +1,51 @@
+/*
+ * 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, useReducer } from 'react';
+import { i18n } from '@kbn/i18n';
+import { useKibana } from '../../../../common/lib/kibana/kibana_react';
+import { reducer, initialState } from './common/api_request_reducer';
+import type { RetryRuleMigrationFilter } from '../../types';
+
+export const RETRY_RULE_MIGRATION_SUCCESS = i18n.translate(
+ 'xpack.securitySolution.siemMigrations.rules.service.retryMigrationRulesSuccess',
+ { defaultMessage: 'Retry rule migration started successfully.' }
+);
+export const RETRY_RULE_MIGRATION_ERROR = i18n.translate(
+ 'xpack.securitySolution.siemMigrations.rules.service.retryMigrationRulesError',
+ { defaultMessage: 'Error retrying a rule migration.' }
+);
+
+export type RetryRuleMigration = (migrationId: string, filter?: RetryRuleMigrationFilter) => void;
+export type OnSuccess = () => void;
+
+export const useRetryRuleMigration = (onSuccess?: OnSuccess) => {
+ const { siemMigrations, notifications } = useKibana().services;
+ const [state, dispatch] = useReducer(reducer, initialState);
+
+ const retryRuleMigration = useCallback(
+ (migrationId, filter) => {
+ (async () => {
+ try {
+ dispatch({ type: 'start' });
+ await siemMigrations.rules.retryRuleMigration(migrationId, filter);
+
+ notifications.toasts.addSuccess(RETRY_RULE_MIGRATION_SUCCESS);
+ dispatch({ type: 'success' });
+ onSuccess?.();
+ } catch (err) {
+ const apiError = err.body ?? err;
+ notifications.toasts.addError(apiError, { title: RETRY_RULE_MIGRATION_ERROR });
+ dispatch({ type: 'error', error: apiError });
+ }
+ })();
+ },
+ [siemMigrations.rules, notifications.toasts, onSuccess]
+ );
+
+ return { isLoading: state.loading, error: state.error, retryRuleMigration };
+};
diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts
index 83ead556b09cc..cda500df3e215 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts
+++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts
@@ -12,6 +12,7 @@ import {
DEFAULT_ASSISTANT_NAMESPACE,
TRACE_OPTIONS_SESSION_STORAGE_KEY,
} from '@kbn/elastic-assistant/impl/assistant_context/constants';
+import type { RelatedIntegration } from '../../../../common/api/detection_engine';
import type { LangSmithOptions } from '../../../../common/siem_migrations/model/common.gen';
import type {
RuleMigrationResourceData,
@@ -19,8 +20,9 @@ import type {
} from '../../../../common/siem_migrations/model/rule_migration.gen';
import type {
CreateRuleMigrationRequestBody,
- GetAllStatsRuleMigrationResponse,
GetRuleMigrationStatsResponse,
+ RetryRuleMigrationResponse,
+ StartRuleMigrationResponse,
UpsertRuleMigrationResourcesRequestBody,
} from '../../../../common/siem_migrations/model/api/rules/rule_migration.gen';
import { SiemMigrationTaskStatus } from '../../../../common/siem_migrations/constants';
@@ -35,8 +37,10 @@ import {
type GetRuleMigrationsStatsAllParams,
getMissingResources,
upsertMigrationResources,
+ retryRuleMigration,
+ getIntegrations,
} from '../api';
-import type { RuleMigrationStats } from '../types';
+import type { RetryRuleMigrationFilter, RuleMigrationStats } from '../types';
import { getSuccessToast } from './success_notification';
import { RuleMigrationsStorage } from './storage';
import * as i18n from './translations';
@@ -119,7 +123,7 @@ export class SiemRulesMigrationsService {
}
}
- public async startRuleMigration(migrationId: string): Promise {
+ public async startRuleMigration(migrationId: string): Promise {
const connectorId = this.connectorIdStorage.get();
if (!connectorId) {
throw new Error(i18n.MISSING_CONNECTOR_ERROR);
@@ -139,6 +143,34 @@ export class SiemRulesMigrationsService {
return result;
}
+ public async retryRuleMigration(
+ migrationId: string,
+ filter?: RetryRuleMigrationFilter
+ ): Promise {
+ const connectorId = this.connectorIdStorage.get();
+ if (!connectorId) {
+ throw new Error(i18n.MISSING_CONNECTOR_ERROR);
+ }
+
+ const langSmithSettings = this.traceOptionsStorage.get();
+ let langSmithOptions: LangSmithOptions | undefined;
+ if (langSmithSettings) {
+ langSmithOptions = {
+ project_name: langSmithSettings.langSmithProject,
+ api_key: langSmithSettings.langSmithApiKey,
+ };
+ }
+
+ const result = await retryRuleMigration({
+ migrationId,
+ connectorId,
+ langSmithOptions,
+ ...filter,
+ });
+ this.startPolling();
+ return result;
+ }
+
public async getRuleMigrationStats(migrationId: string): Promise {
return getRuleMigrationStats({ migrationId });
}
@@ -181,6 +213,10 @@ export class SiemRulesMigrationsService {
});
}
+ public async getIntegrations(): Promise> {
+ return getIntegrations({});
+ }
+
private async startTaskStatsPolling(): Promise {
let pendingMigrationIds: string[] = [];
do {
@@ -213,7 +249,12 @@ export class SiemRulesMigrationsService {
}
}
- await new Promise((resolve) => setTimeout(resolve, REQUEST_POLLING_INTERVAL_SECONDS * 1000));
+ // Do not wait if there are no more pending migrations
+ if (pendingMigrationIds.length > 0) {
+ await new Promise((resolve) =>
+ setTimeout(resolve, REQUEST_POLLING_INTERVAL_SECONDS * 1000)
+ );
+ }
} while (pendingMigrationIds.length > 0);
}
}
diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/types.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/types.ts
index bcc11327d1051..b852a62e8c29f 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/types.ts
+++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/types.ts
@@ -13,3 +13,8 @@ export interface RuleMigrationStats extends RuleMigrationTaskStats {
/** The sequential number of the migration */
number: number;
}
+
+export interface RetryRuleMigrationFilter {
+ failed?: boolean;
+ notFullyTranslated?: boolean;
+}
diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/containers/use_timeline_data_filters.ts b/x-pack/solutions/security/plugins/security_solution/public/timelines/containers/use_timeline_data_filters.ts
index cd6ed2c4baa93..cd27ff06020ba 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/timelines/containers/use_timeline_data_filters.ts
+++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/containers/use_timeline_data_filters.ts
@@ -6,27 +6,19 @@
*/
import { useMemo } from 'react';
-import { useSelector } from 'react-redux';
-import { useLocation } from 'react-router-dom';
import { useDeepEqualSelector } from '../../common/hooks/use_selector';
import {
isLoadingSelector,
startSelector,
endSelector,
} from '../../common/components/super_date_picker/selectors';
-import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features';
import { SourcererScopeName } from '../../sourcerer/store/model';
import { useSourcererDataView } from '../../sourcerer/containers';
-import { getScopeFromPath } from '../../sourcerer/containers/sourcerer_paths';
-import { sourcererSelectors } from '../../common/store';
export function useTimelineDataFilters(isActiveTimelines: boolean) {
const getStartSelector = useMemo(() => startSelector(), []);
const getEndSelector = useMemo(() => endSelector(), []);
const getIsLoadingSelector = useMemo(() => isLoadingSelector(), []);
- const isDatePickerAndSourcererDisabled = useIsExperimentalFeatureEnabled(
- 'analyzerDatePickersAndSourcererDisabled'
- );
const shouldUpdate = useDeepEqualSelector((state) => {
if (isActiveTimelines) {
@@ -49,35 +41,15 @@ export function useTimelineDataFilters(isActiveTimelines: boolean) {
return getEndSelector(state.inputs.global);
}
});
- const defaultDataView = useSelector(sourcererSelectors.defaultDataView);
- const { pathname } = useLocation();
- const { selectedPatterns: nonTimelinePatterns } = useSourcererDataView(
- getScopeFromPath(pathname)
- );
-
- const { selectedPatterns: timelinePatterns } = useSourcererDataView(SourcererScopeName.timeline);
-
- const selectedPatterns = useMemo(() => {
- return isActiveTimelines
- ? [...new Set([...timelinePatterns, ...defaultDataView.patternList])]
- : [...new Set([...nonTimelinePatterns, ...defaultDataView.patternList])];
- }, [isActiveTimelines, timelinePatterns, nonTimelinePatterns, defaultDataView.patternList]);
const { selectedPatterns: analyzerPatterns } = useSourcererDataView(SourcererScopeName.analyzer);
return useMemo(() => {
return {
- selectedPatterns: isDatePickerAndSourcererDisabled ? selectedPatterns : analyzerPatterns,
+ selectedPatterns: analyzerPatterns,
from,
to,
shouldUpdate,
};
- }, [
- selectedPatterns,
- from,
- to,
- shouldUpdate,
- isDatePickerAndSourcererDisabled,
- analyzerPatterns,
- ]);
+ }, [from, to, shouldUpdate, analyzerPatterns]);
}
diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/fleet_integrations/api/get_all_integrations/route.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/fleet_integrations/api/get_all_integrations/route.ts
index 4b5642b9d199b..5adc103cfff7c 100644
--- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/fleet_integrations/api/get_all_integrations/route.ts
+++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/fleet_integrations/api/get_all_integrations/route.ts
@@ -6,6 +6,7 @@
*/
import { transformError } from '@kbn/securitysolution-es-utils';
+import { SO_SEARCH_LIMIT } from '@kbn/fleet-plugin/common/constants';
import { PREBUILT_RULES_PACKAGE_NAME } from '../../../../../../common/detection_engine/constants';
import { buildSiemResponse } from '../../../routes/utils';
import type { SecuritySolutionPluginRouter } from '../../../../../types';
@@ -43,7 +44,9 @@ export const getAllIntegrationsRoute = (router: SecuritySolutionPluginRouter) =>
const [packages, packagePolicies] = await Promise.all([
fleet.packages.getPackages(),
- fleet.packagePolicy.list(fleet.savedObjects.createInternalScopedSoClient(), {}),
+ fleet.packagePolicy.list(fleet.savedObjects.createInternalScopedSoClient(), {
+ perPage: SO_SEARCH_LIMIT,
+ }),
]);
// Elastic prebuilt rules is a special package and should be skipped
const packagesWithoutPrebuiltSecurityRules = packages.filter(
diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/get_integrations.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/get_integrations.ts
new file mode 100644
index 0000000000000..afc7c7b9608d3
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/get_integrations.ts
@@ -0,0 +1,54 @@
+/*
+ * 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 { IKibanaResponse, Logger } from '@kbn/core/server';
+import type { RelatedIntegration } from '../../../../../common/api/detection_engine';
+import { type GetRuleMigrationIntegrationsResponse } from '../../../../../common/siem_migrations/model/api/rules/rule_migration.gen';
+import { SIEM_RULE_MIGRATIONS_INTEGRATIONS_PATH } from '../../../../../common/siem_migrations/constants';
+import type { SecuritySolutionPluginRouter } from '../../../../types';
+import { withLicense } from './util/with_license';
+
+export const registerSiemRuleMigrationsIntegrationsRoute = (
+ router: SecuritySolutionPluginRouter,
+ logger: Logger
+) => {
+ router.versioned
+ .get({
+ path: SIEM_RULE_MIGRATIONS_INTEGRATIONS_PATH,
+ access: 'internal',
+ security: { authz: { requiredPrivileges: ['securitySolution'] } },
+ })
+ .addVersion(
+ {
+ version: '1',
+ validate: {},
+ },
+ withLicense(
+ async (
+ context,
+ req,
+ res
+ ): Promise> => {
+ try {
+ const ctx = await context.resolve(['core', 'alerting', 'securitySolution']);
+ const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient();
+
+ const relatedIntegrations: Record = {};
+ const packages = await ruleMigrationsClient.data.integrations.getIntegrationPackages();
+ packages?.forEach(({ id, version, integration }) => {
+ relatedIntegrations[id] = { package: id, version, integration };
+ });
+
+ return res.ok({ body: relatedIntegrations });
+ } catch (err) {
+ logger.error(err);
+ return res.badRequest({ body: err.message });
+ }
+ }
+ )
+ );
+};
diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/get_prebuilt_rules.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/get_prebuilt_rules.ts
index 551e4a51e477e..8165b858e2a31 100644
--- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/get_prebuilt_rules.ts
+++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/get_prebuilt_rules.ts
@@ -12,8 +12,7 @@ import { GetRuleMigrationPrebuiltRulesRequestParams } from '../../../../../commo
import { SIEM_RULE_MIGRATIONS_PREBUILT_RULES_PATH } from '../../../../../common/siem_migrations/constants';
import type { SecuritySolutionPluginRouter } from '../../../../types';
import { withLicense } from './util/with_license';
-import { getPrebuiltRules, getUniquePrebuiltRuleIds } from './util/prebuilt_rules';
-import { MAX_PREBUILT_RULES_TO_FETCH } from './constants';
+import { getPrebuiltRulesForMigration } from './util/prebuilt_rules';
export const registerSiemRuleMigrationsPrebuiltRulesRoute = (
router: SecuritySolutionPluginRouter,
@@ -47,19 +46,11 @@ export const registerSiemRuleMigrationsPrebuiltRulesRoute = (
const savedObjectsClient = ctx.core.savedObjects.client;
const rulesClient = await ctx.alerting.getRulesClient();
- const result = await ruleMigrationsClient.data.rules.get(migrationId, {
- filters: {
- prebuilt: true,
- },
- from: 0,
- size: MAX_PREBUILT_RULES_TO_FETCH,
- });
-
- const prebuiltRulesIds = getUniquePrebuiltRuleIds(result.data);
- const prebuiltRules = await getPrebuiltRules(
+ const prebuiltRules = await getPrebuiltRulesForMigration(
+ migrationId,
+ ruleMigrationsClient,
rulesClient,
- savedObjectsClient,
- prebuiltRulesIds
+ savedObjectsClient
);
return res.ok({ body: prebuiltRules });
diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/index.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/index.ts
index 241e59ac02a27..05d2c8f1a5dc4 100644
--- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/index.ts
+++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/index.ts
@@ -22,6 +22,7 @@ import { registerSiemRuleMigrationsInstallRoute } from './install';
import { registerSiemRuleMigrationsInstallTranslatedRoute } from './install_translated';
import { registerSiemRuleMigrationsResourceGetMissingRoute } from './resources/missing';
import { registerSiemRuleMigrationsPrebuiltRulesRoute } from './get_prebuilt_rules';
+import { registerSiemRuleMigrationsIntegrationsRoute } from './get_integrations';
export const registerSiemRuleMigrationsRoutes = (
router: SecuritySolutionPluginRouter,
@@ -39,6 +40,7 @@ export const registerSiemRuleMigrationsRoutes = (
registerSiemRuleMigrationsStopRoute(router, logger);
registerSiemRuleMigrationsInstallRoute(router, logger);
registerSiemRuleMigrationsInstallTranslatedRoute(router, logger);
+ registerSiemRuleMigrationsIntegrationsRoute(router, logger);
registerSiemRuleMigrationsResourceUpsertRoute(router, logger);
registerSiemRuleMigrationsResourceGetRoute(router, logger);
diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/retry.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/retry.ts
index 0fb96d9aaf72c..0062e21a8124f 100644
--- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/retry.ts
+++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/retry.ts
@@ -10,13 +10,14 @@ import { buildRouteValidationWithZod } from '@kbn/zod-helpers';
import { APMTracer } from '@kbn/langchain/server/tracers/apm';
import { getLangSmithTracer } from '@kbn/langchain/server/tracers/langsmith';
import {
- StartRuleMigrationRequestBody,
- StartRuleMigrationRequestParams,
- type StartRuleMigrationResponse,
+ RetryRuleMigrationRequestBody,
+ RetryRuleMigrationRequestParams,
+ type RetryRuleMigrationResponse,
} from '../../../../../common/siem_migrations/model/api/rules/rule_migration.gen';
import { SIEM_RULE_MIGRATION_RETRY_PATH } from '../../../../../common/siem_migrations/constants';
import type { SecuritySolutionPluginRouter } from '../../../../types';
import { withLicense } from './util/with_license';
+import type { RuleMigrationFilters } from '../data/rule_migrations_data_rules_client';
export const registerSiemRuleMigrationsRetryRoute = (
router: SecuritySolutionPluginRouter,
@@ -33,15 +34,20 @@ export const registerSiemRuleMigrationsRetryRoute = (
version: '1',
validate: {
request: {
- params: buildRouteValidationWithZod(StartRuleMigrationRequestParams),
- body: buildRouteValidationWithZod(StartRuleMigrationRequestBody),
+ params: buildRouteValidationWithZod(RetryRuleMigrationRequestParams),
+ body: buildRouteValidationWithZod(RetryRuleMigrationRequestBody),
},
},
},
withLicense(
- async (context, req, res): Promise> => {
+ async (context, req, res): Promise> => {
const migrationId = req.params.migration_id;
- const { langsmith_options: langsmithOptions, connector_id: connectorId } = req.body;
+ const {
+ langsmith_options: langsmithOptions,
+ connector_id: connectorId,
+ failed,
+ not_fully_translated: notFullyTranslated,
+ } = req.body;
try {
const ctx = await context.resolve(['core', 'actions', 'alerting', 'securitySolution']);
@@ -59,7 +65,8 @@ export const registerSiemRuleMigrationsRetryRoute = (
],
};
- const { updated } = await ruleMigrationsClient.task.updateToRetry(migrationId);
+ const filters: RuleMigrationFilters = { failed, notFullyTranslated };
+ const { updated } = await ruleMigrationsClient.task.updateToRetry(migrationId, filters);
if (!updated) {
return res.ok({ body: { started: false } });
}
diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/util/prebuilt_rules.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/util/prebuilt_rules.ts
index 7760612abc878..cf7317f0bfde0 100644
--- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/util/prebuilt_rules.ts
+++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/util/prebuilt_rules.ts
@@ -13,6 +13,7 @@ import { fetchRuleVersionsTriad } from '../../../../detection_engine/prebuilt_ru
import { createPrebuiltRuleAssetsClient } from '../../../../detection_engine/prebuilt_rules/logic/rule_assets/prebuilt_rule_assets_client';
import { convertPrebuiltRuleAssetToRuleResponse } from '../../../../detection_engine/rule_management/logic/detection_rules_client/converters/convert_prebuilt_rule_asset_to_rule_response';
import type { RuleMigration } from '../../../../../../common/siem_migrations/model/rule_migration.gen';
+import type { SiemRuleMigrationsClient } from '../../siem_rule_migrations_service';
export const getUniquePrebuiltRuleIds = (migrationRules: RuleMigration[]): string[] => {
const rulesIds = new Set();
@@ -82,3 +83,37 @@ export const getPrebuiltRules = async (
return prebuiltRules;
};
+
+/**
+ * Gets Elastic prebuilt rules
+ * @param migrationId The `id` of the migration to get related prebuilt rules for
+ * @param ruleMigrationsClient The rules migration client to migration rules data
+ * @param rulesClient The rules client to fetch prebuilt rules
+ * @param savedObjectsClient The saved objects client
+ * @returns
+ */
+export const getPrebuiltRulesForMigration = async (
+ migrationId: string,
+ ruleMigrationsClient: SiemRuleMigrationsClient,
+ rulesClient: RulesClient,
+ savedObjectsClient: SavedObjectsClientContract
+): Promise> => {
+ const options = { filters: { prebuilt: true } };
+ const batches = ruleMigrationsClient.data.rules.searchBatches(migrationId, options);
+
+ const rulesIds = new Set();
+ let results = await batches.next();
+ while (results.length) {
+ results.forEach((rule) => {
+ if (rule.elastic_rule?.prebuilt_rule_id) {
+ rulesIds.add(rule.elastic_rule.prebuilt_rule_id);
+ }
+ });
+ results = await batches.next();
+ }
+ const prebuiltRulesIds = Array.from(rulesIds);
+
+ const prebuiltRules = await getPrebuiltRules(rulesClient, savedObjectsClient, prebuiltRulesIds);
+
+ return prebuiltRules;
+};
diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_client.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_client.ts
index c06c889482360..e479c42cce273 100644
--- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_client.ts
+++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_client.ts
@@ -6,6 +6,7 @@
*/
import type { ElasticsearchClient, Logger } from '@kbn/core/server';
+import type { PackageService } from '@kbn/fleet-plugin/server';
import { RuleMigrationsDataIntegrationsClient } from './rule_migrations_data_integrations_client';
import { RuleMigrationsDataPrebuiltRulesClient } from './rule_migrations_data_prebuilt_rules_client';
import { RuleMigrationsDataResourcesClient } from './rule_migrations_data_resources_client';
@@ -25,7 +26,8 @@ export class RuleMigrationsDataClient {
indexNameProviders: IndexNameProviders,
username: string,
esClient: ElasticsearchClient,
- logger: Logger
+ logger: Logger,
+ packageService?: PackageService
) {
this.rules = new RuleMigrationsDataRulesClient(
indexNameProviders.rules,
@@ -43,7 +45,8 @@ export class RuleMigrationsDataClient {
indexNameProviders.integrations,
username,
esClient,
- logger
+ logger,
+ packageService
);
this.prebuiltRules = new RuleMigrationsDataPrebuiltRulesClient(
indexNameProviders.prebuiltrules,
diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_integrations_client.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_integrations_client.ts
index fdb063836f9e4..947a206cd0c7a 100644
--- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_integrations_client.ts
+++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_integrations_client.ts
@@ -5,11 +5,15 @@
* 2.0.
*/
+import type { PackageService } from '@kbn/fleet-plugin/server';
+import type { ElasticsearchClient, Logger } from '@kbn/core/server';
+import type { PackageList } from '@kbn/fleet-plugin/common';
import type { Integration } from '../types';
import { RuleMigrationsDataBaseClient } from './rule_migrations_data_base_client';
/* This will be removed once the package registry changes is performed */
import integrationsFile from './integrations_temp.json';
+import type { IndexNameProvider } from './rule_migrations_data_client';
/* The minimum score required for a integration to be considered correct, might need to change this later */
const MIN_SCORE = 40 as const;
@@ -22,6 +26,20 @@ const INTEGRATIONS = integrationsFile as Integration[];
* The 500 number was chosen as a reasonable number to avoid large payloads. It can be adjusted if needed.
*/
export class RuleMigrationsDataIntegrationsClient extends RuleMigrationsDataBaseClient {
+ constructor(
+ getIndexName: IndexNameProvider,
+ username: string,
+ esClient: ElasticsearchClient,
+ logger: Logger,
+ private packageService?: PackageService
+ ) {
+ super(getIndexName, username, esClient, logger);
+ }
+
+ async getIntegrationPackages(): Promise {
+ return this.packageService?.asInternalUser.getPackages();
+ }
+
/** Indexes an array of integrations to be used with ELSER semantic search queries */
async create(): Promise {
const index = await this.getIndexName();
diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_rules_client.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_rules_client.ts
index 47bcd56e6433e..e65921ca2a9ac 100644
--- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_rules_client.ts
+++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_rules_client.ts
@@ -43,6 +43,9 @@ export interface RuleMigrationFilters {
ids?: string[];
installable?: boolean;
prebuilt?: boolean;
+ custom?: boolean;
+ failed?: boolean;
+ notFullyTranslated?: boolean;
searchTerm?: string;
}
export interface RuleMigrationGetOptions {
@@ -239,7 +242,7 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient
async releaseProcessing(migrationId: string): Promise {
return this.updateStatus(
migrationId,
- SiemMigrationStatus.PROCESSING,
+ { status: SiemMigrationStatus.PROCESSING },
SiemMigrationStatus.PENDING
);
}
@@ -247,12 +250,12 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient
/** Updates all the rule migration with the provided id and with status `statusToQuery` to `statusToUpdate` */
async updateStatus(
migrationId: string,
- statusToQuery: SiemMigrationStatus | SiemMigrationStatus[] | undefined,
+ filter: RuleMigrationFilters,
statusToUpdate: SiemMigrationStatus,
{ refresh = false }: { refresh?: boolean } = {}
): Promise {
const index = await this.getIndexName();
- const query = this.getFilterQuery(migrationId, { status: statusToQuery });
+ const query = this.getFilterQuery(migrationId, filter);
const script = { source: `ctx._source['status'] = '${statusToUpdate}'` };
await this.esClient.updateByQuery({ index, query, script, refresh }).catch((error) => {
this.logger.error(`Error updating rule migrations status: ${error.message}`);
@@ -397,7 +400,16 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient
private getFilterQuery(
migrationId: string,
- { status, ids, installable, prebuilt, searchTerm }: RuleMigrationFilters = {}
+ {
+ status,
+ ids,
+ installable,
+ prebuilt,
+ custom,
+ searchTerm,
+ failed,
+ notFullyTranslated,
+ }: RuleMigrationFilters = {}
): QueryDslQueryContainer {
const filter: QueryDslQueryContainer[] = [{ term: { migration_id: migrationId } }];
if (status) {
@@ -416,9 +428,18 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient
if (prebuilt) {
filter.push(searchConditions.isPrebuilt());
}
+ if (custom) {
+ filter.push(searchConditions.isCustom());
+ }
if (searchTerm?.length) {
filter.push(searchConditions.matchTitle(searchTerm));
}
+ if (failed) {
+ filter.push(searchConditions.isFailed());
+ }
+ if (notFullyTranslated) {
+ filter.push(searchConditions.isNotFullyTranslated());
+ }
return { bool: { filter } };
}
}
diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_service.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_service.ts
index 5799e5ab84c07..6681f0c3903b0 100644
--- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_service.ts
+++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_service.ts
@@ -6,6 +6,7 @@
*/
import type { AuthenticatedUser, ElasticsearchClient, Logger } from '@kbn/core/server';
import { IndexPatternAdapter, type FieldMap, type InstallParams } from '@kbn/index-adapter';
+import type { PackageService } from '@kbn/fleet-plugin/server';
import type { IndexNameProvider, IndexNameProviders } from './rule_migrations_data_client';
import { RuleMigrationsDataClient } from './rule_migrations_data_client';
import {
@@ -24,6 +25,7 @@ interface CreateClientParams {
spaceId: string;
currentUser: AuthenticatedUser;
esClient: ElasticsearchClient;
+ packageService?: PackageService;
}
export class RuleMigrationsDataService {
@@ -58,7 +60,7 @@ export class RuleMigrationsDataService {
]);
}
- public createClient({ spaceId, currentUser, esClient }: CreateClientParams) {
+ public createClient({ spaceId, currentUser, esClient, packageService }: CreateClientParams) {
const indexNameProviders: IndexNameProviders = {
rules: this.createIndexNameProvider('rules', spaceId),
resources: this.createIndexNameProvider('resources', spaceId),
@@ -70,7 +72,8 @@ export class RuleMigrationsDataService {
indexNameProviders,
currentUser.username,
esClient,
- this.logger
+ this.logger,
+ packageService
);
}
diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/search.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/search.ts
index 3bd8da066a45f..dbc217bf6e0b7 100644
--- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/search.ts
+++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/search.ts
@@ -6,12 +6,18 @@
*/
import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
-import { RuleTranslationResult } from '../../../../../common/siem_migrations/constants';
+import {
+ RuleTranslationResult,
+ SiemMigrationStatus,
+} from '../../../../../common/siem_migrations/constants';
export const conditions = {
isFullyTranslated(): QueryDslQueryContainer {
return { term: { translation_result: RuleTranslationResult.FULL } };
},
+ isNotFullyTranslated(): QueryDslQueryContainer {
+ return { bool: { must_not: conditions.isFullyTranslated() } };
+ },
isNotInstalled(): QueryDslQueryContainer {
return {
nested: {
@@ -28,6 +34,14 @@ export const conditions = {
},
};
},
+ isCustom(): QueryDslQueryContainer {
+ return {
+ nested: {
+ path: 'elastic_rule',
+ query: { bool: { must_not: { exists: { field: 'elastic_rule.prebuilt_rule_id' } } } },
+ },
+ };
+ },
matchTitle(title: string): QueryDslQueryContainer {
return {
nested: {
@@ -39,4 +53,7 @@ export const conditions = {
isInstallable(): QueryDslQueryContainer[] {
return [this.isFullyTranslated(), this.isNotInstalled()];
},
+ isFailed(): QueryDslQueryContainer {
+ return { term: { status: SiemMigrationStatus.FAILED } };
+ },
};
diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/sort.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/sort.ts
index 2d0ef644b8e56..6f3fcd46612d4 100644
--- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/sort.ts
+++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/sort.ts
@@ -113,9 +113,21 @@ const sortingOptionsMap: {
[key: string]: (direction?: estypes.SortOrder) => estypes.SortCombinations[];
} = {
'elastic_rule.title': sortingOptions.name,
- 'elastic_rule.severity': sortingOptions.severity,
- 'elastic_rule.prebuilt_rule_id': sortingOptions.matchedPrebuiltRule,
- translation_result: sortingOptions.status,
+ 'elastic_rule.severity': (direction?: estypes.SortOrder) => [
+ ...sortingOptions.severity(direction),
+ ...sortingOptions.status('desc'),
+ ...sortingOptions.matchedPrebuiltRule('desc'),
+ ],
+ 'elastic_rule.prebuilt_rule_id': (direction?: estypes.SortOrder) => [
+ ...sortingOptions.matchedPrebuiltRule(direction),
+ ...sortingOptions.status('desc'),
+ ...sortingOptions.severity('desc'),
+ ],
+ translation_result: (direction?: estypes.SortOrder) => [
+ ...sortingOptions.status(direction),
+ ...sortingOptions.matchedPrebuiltRule('desc'),
+ ...sortingOptions.severity('desc'),
+ ],
updated_at: sortingOptions.updated,
};
diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/siem_rule_migrations_service.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/siem_rule_migrations_service.ts
index d9f4a1c5249cb..3be54a7e3d896 100644
--- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/siem_rule_migrations_service.ts
+++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/siem_rule_migrations_service.ts
@@ -14,6 +14,7 @@ import type {
KibanaRequest,
Logger,
} from '@kbn/core/server';
+import type { PackageService } from '@kbn/fleet-plugin/server';
import { RuleMigrationsDataService } from './data/rule_migrations_data_service';
import type { RuleMigrationsDataClient } from './data/rule_migrations_data_client';
import type { RuleMigrationsTaskClient } from './task/rule_migrations_task_client';
@@ -29,6 +30,7 @@ export interface SiemRuleMigrationsCreateClientParams {
request: KibanaRequest;
currentUser: AuthenticatedUser | null;
spaceId: string;
+ packageService?: PackageService;
}
export interface SiemRuleMigrationsClient {
@@ -60,13 +62,19 @@ export class SiemRuleMigrationsService {
createClient({
spaceId,
currentUser,
+ packageService,
request,
}: SiemRuleMigrationsCreateClientParams): SiemRuleMigrationsClient {
assert(currentUser, 'Current user must be authenticated');
assert(this.esClusterClient, 'ES client not available, please call setup first');
const esClient = this.esClusterClient.asInternalUser;
- const dataClient = this.dataService.createClient({ spaceId, currentUser, esClient });
+ const dataClient = this.dataService.createClient({
+ spaceId,
+ currentUser,
+ esClient,
+ packageService,
+ });
const taskClient = this.taskService.createClient({ currentUser, dataClient });
return { data: dataClient, task: taskClient };
diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/rule_migrations_task_client.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/rule_migrations_task_client.ts
index 1edd1b449070c..663a8f5218f33 100644
--- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/rule_migrations_task_client.ts
+++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/rule_migrations_task_client.ts
@@ -14,7 +14,10 @@ import {
} from '../../../../../common/siem_migrations/constants';
import type { RuleMigrationTaskStats } from '../../../../../common/siem_migrations/model/rule_migration.gen';
import type { RuleMigrationsDataClient } from '../data/rule_migrations_data_client';
-import type { RuleMigrationDataStats } from '../data/rule_migrations_data_rules_client';
+import type {
+ RuleMigrationDataStats,
+ RuleMigrationFilters,
+} from '../data/rule_migrations_data_rules_client';
import { getRuleMigrationAgent } from './agent';
import type { MigrateRuleState } from './agent/types';
import { RuleMigrationsRetriever } from './retrievers';
@@ -49,7 +52,7 @@ export class RuleMigrationsTaskClient {
// Just in case some previous execution was interrupted without cleaning up
await this.data.rules.updateStatus(
migrationId,
- SiemMigrationStatus.PROCESSING,
+ { status: SiemMigrationStatus.PROCESSING },
SiemMigrationStatus.PENDING,
{ refresh: true }
);
@@ -203,12 +206,15 @@ export class RuleMigrationsTaskClient {
}
/** Updates all the rules in a migration to be re-executed */
- public async updateToRetry(migrationId: string): Promise<{ updated: boolean }> {
+ public async updateToRetry(
+ migrationId: string,
+ filter: RuleMigrationFilters = {}
+ ): Promise<{ updated: boolean }> {
if (this.migrationsRunning.has(migrationId)) {
return { updated: false };
}
// Update all the rules in the migration to pending
- await this.data.rules.updateStatus(migrationId, undefined, SiemMigrationStatus.PENDING, {
+ await this.data.rules.updateStatus(migrationId, filter, SiemMigrationStatus.PENDING, {
refresh: true,
});
return { updated: true };
diff --git a/x-pack/solutions/security/plugins/security_solution/server/request_context_factory.ts b/x-pack/solutions/security/plugins/security_solution/server/request_context_factory.ts
index a6b5e1b3e650a..1c5e954dfeb79 100644
--- a/x-pack/solutions/security/plugins/security_solution/server/request_context_factory.ts
+++ b/x-pack/solutions/security/plugins/security_solution/server/request_context_factory.ts
@@ -176,6 +176,7 @@ export class RequestContextFactory implements IRequestContextFactory {
request,
currentUser: coreContext.security.authc.getCurrentUser(),
spaceId: getSpaceId(),
+ packageService: startPlugins.fleet?.packageService,
})
),
diff --git a/x-pack/test/accessibility/apps/group3/reporting.ts b/x-pack/test/accessibility/apps/group3/reporting.ts
index edd5bbeb0a0a6..b5ec8a7d7446b 100644
--- a/x-pack/test/accessibility/apps/group3/reporting.ts
+++ b/x-pack/test/accessibility/apps/group3/reporting.ts
@@ -20,7 +20,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const createReportingUser = async () => {
await security.user.create(reporting.REPORTING_USER_USERNAME, {
password: reporting.REPORTING_USER_PASSWORD,
- roles: ['data_analyst', 'kibana_user'],
+ roles: ['data_analyst', 'kibana_admin'],
full_name: 'a reporting user',
});
};
diff --git a/x-pack/test/api_integration/apis/streams/enrichment.ts b/x-pack/test/api_integration/apis/streams/enrichment.ts
index 01c67ac14808e..43e8d51962dfb 100644
--- a/x-pack/test/api_integration/apis/streams/enrichment.ts
+++ b/x-pack/test/api_integration/apis/streams/enrichment.ts
@@ -8,7 +8,14 @@
import expect from '@kbn/expect';
import { SearchTotalHits } from '@elastic/elasticsearch/lib/api/types';
import { WiredStreamConfigDefinition } from '@kbn/streams-schema';
-import { enableStreams, fetchDocument, indexDocument, putStream } from './helpers/requests';
+import {
+ deleteStream,
+ enableStreams,
+ fetchDocument,
+ forkStream,
+ indexDocument,
+ putStream,
+} from './helpers/requests';
import { FtrProviderContext } from '../../ftr_provider_context';
import { waitForDocumentInIndex } from '../../../alerting_api_integration/observability/helpers/alerting_wait_for_helpers';
import { cleanUpRootStream } from './helpers/cleanup';
@@ -22,9 +29,22 @@ export default function ({ getService }: FtrProviderContext) {
describe('Enrichment', () => {
before(async () => {
await enableStreams(supertest);
+ const body = {
+ stream: {
+ name: 'logs.nginx',
+ },
+ condition: {
+ field: 'host.name',
+ operator: 'eq',
+ value: 'routeme',
+ },
+ };
+ // We use a forked stream as processing changes cannot be made to the root stream
+ await forkStream(supertest, 'logs', body);
});
after(async () => {
+ await deleteStream(supertest, 'logs.nginx');
await cleanUpRootStream(esClient);
await esClient.indices.deleteDataStream({
name: ['logs*'],
@@ -81,7 +101,7 @@ export default function ({ getService }: FtrProviderContext) {
},
},
};
- const response = await putStream(supertest, 'logs', body);
+ const response = await putStream(supertest, 'logs.nginx', body);
expect(response).to.have.property('acknowledged', true);
});
@@ -89,15 +109,28 @@ export default function ({ getService }: FtrProviderContext) {
const doc = {
'@timestamp': '2024-01-01T00:00:10.000Z',
message: '2023-01-01T00:00:10.000Z error test',
+ ['host.name']: 'routeme',
};
const response = await indexDocument(esClient, 'logs', doc);
expect(response.result).to.eql('created');
- await waitForDocumentInIndex({ esClient, indexName: 'logs', retryService, logger });
+ const reroutedDocResponse = await waitForDocumentInIndex({
+ esClient,
+ indexName: 'logs.nginx',
+ retryService,
+ logger,
+ });
- const result = await fetchDocument(esClient, 'logs', response._id);
+ const result = await fetchDocument(
+ esClient,
+ 'logs.nginx',
+ reroutedDocResponse.hits?.hits[0]?._id!
+ );
expect(result._source).to.eql({
'@timestamp': '2024-01-01T00:00:10.000Z',
message: '2023-01-01T00:00:10.000Z error test',
+ host: {
+ name: 'routeme',
+ },
inner_timestamp: '2023-01-01T00:00:10.000Z',
message2: 'test',
log: {
@@ -110,22 +143,30 @@ export default function ({ getService }: FtrProviderContext) {
const doc = {
'@timestamp': '2024-01-01T00:00:11.000Z',
message: '2023-01-01T00:00:10.000Z info mylogger this is the message',
+ ['host.name']: 'routeme',
};
const response = await indexDocument(esClient, 'logs', doc);
expect(response.result).to.eql('created');
- await waitForDocumentInIndex({
+ const reroutedDocResponse = await waitForDocumentInIndex({
esClient,
- indexName: 'logs',
+ indexName: 'logs.nginx',
retryService,
logger,
docCountTarget: 2,
});
- const result = await fetchDocument(esClient, 'logs', response._id);
+ const result = await fetchDocument(
+ esClient,
+ 'logs.nginx',
+ reroutedDocResponse.hits?.hits[0]?._id!
+ );
expect(result._source).to.eql({
'@timestamp': '2024-01-01T00:00:11.000Z',
message: '2023-01-01T00:00:10.000Z info mylogger this is the message',
inner_timestamp: '2023-01-01T00:00:10.000Z',
+ host: {
+ name: 'routeme',
+ },
log: {
level: 'info',
logger: 'mylogger',
@@ -137,7 +178,7 @@ export default function ({ getService }: FtrProviderContext) {
it('Doc is searchable', async () => {
const response = await esClient.search({
- index: 'logs',
+ index: 'logs.nginx',
body: {
query: {
match: {
@@ -151,7 +192,7 @@ export default function ({ getService }: FtrProviderContext) {
it('Non-indexed field is not searchable', async () => {
const response = await esClient.search({
- index: 'logs',
+ index: 'logs.nginx',
body: {
query: {
match: {
diff --git a/x-pack/test/api_integration/apis/streams/helpers/requests.ts b/x-pack/test/api_integration/apis/streams/helpers/requests.ts
index 799ec480c4a02..748e2f627c5e5 100644
--- a/x-pack/test/api_integration/apis/streams/helpers/requests.ts
+++ b/x-pack/test/api_integration/apis/streams/helpers/requests.ts
@@ -37,9 +37,14 @@ export async function forkStream(supertest: Agent, root: string, body: JsonObjec
return response.body;
}
-export async function putStream(supertest: Agent, name: string, body: StreamConfigDefinition) {
+export async function putStream(
+ supertest: Agent,
+ name: string,
+ body: StreamConfigDefinition,
+ expectStatusCode?: number
+) {
const req = supertest.put(`/api/streams/${encodeURIComponent(name)}`).set('kbn-xsrf', 'xxx');
- const response = await req.send(body).expect(200);
+ const response = await req.send(body).expect(expectStatusCode ?? 200);
return response.body;
}
diff --git a/x-pack/test/api_integration/apis/streams/index.ts b/x-pack/test/api_integration/apis/streams/index.ts
index 299f349172ffd..f481bfcf03639 100644
--- a/x-pack/test/api_integration/apis/streams/index.ts
+++ b/x-pack/test/api_integration/apis/streams/index.ts
@@ -15,5 +15,6 @@ export default function ({ loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./flush_config'));
loadTestFile(require.resolve('./assets/dashboard'));
loadTestFile(require.resolve('./schema'));
+ loadTestFile(require.resolve('./root_stream'));
});
}
diff --git a/x-pack/test/api_integration/apis/streams/root_stream.ts b/x-pack/test/api_integration/apis/streams/root_stream.ts
new file mode 100644
index 0000000000000..42215c834eb50
--- /dev/null
+++ b/x-pack/test/api_integration/apis/streams/root_stream.ts
@@ -0,0 +1,116 @@
+/*
+ * 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 { WiredStreamConfigDefinition, WiredStreamDefinition } from '@kbn/streams-schema';
+import { FtrProviderContext } from '../../ftr_provider_context';
+import { cleanUpRootStream } from './helpers/cleanup';
+import { enableStreams, putStream } from './helpers/requests';
+
+const rootStreamDefinition: WiredStreamDefinition = {
+ name: 'logs',
+ stream: {
+ ingest: {
+ processing: [],
+ routing: [],
+ wired: {
+ fields: {
+ '@timestamp': {
+ type: 'date',
+ },
+ message: {
+ type: 'match_only_text',
+ },
+ 'host.name': {
+ type: 'keyword',
+ },
+ 'log.level': {
+ type: 'keyword',
+ },
+ },
+ },
+ },
+ },
+};
+
+export default function ({ getService }: FtrProviderContext) {
+ const supertest = getService('supertest');
+ const esClient = getService('es');
+
+ describe('Root stream', () => {
+ before(async () => {
+ await enableStreams(supertest);
+ });
+
+ after(async () => {
+ await cleanUpRootStream(esClient);
+ });
+
+ it('Should not allow processing changes', async () => {
+ const body: WiredStreamConfigDefinition = {
+ ingest: {
+ ...rootStreamDefinition.stream.ingest,
+ processing: [
+ {
+ config: {
+ grok: {
+ field: 'message',
+ patterns: [
+ '%{TIMESTAMP_ISO8601:inner_timestamp} %{LOGLEVEL:log.level} %{GREEDYDATA:message2}',
+ ],
+ },
+ },
+ },
+ ],
+ },
+ };
+ const response = await putStream(supertest, 'logs', body, 400);
+ expect(response).to.have.property(
+ 'message',
+ 'Root stream processing rules cannot be changed'
+ );
+ });
+
+ it('Should not allow fields changes', async () => {
+ const body: WiredStreamConfigDefinition = {
+ ingest: {
+ ...rootStreamDefinition.stream.ingest,
+ wired: {
+ fields: {
+ ...rootStreamDefinition.stream.ingest.wired.fields,
+ 'log.level': {
+ type: 'boolean',
+ },
+ },
+ },
+ },
+ };
+ const response = await putStream(supertest, 'logs', body, 400);
+ expect(response).to.have.property('message', 'Root stream fields cannot be changed');
+ });
+
+ it('Should allow routing changes', async () => {
+ const body: WiredStreamConfigDefinition = {
+ ingest: {
+ ...rootStreamDefinition.stream.ingest,
+ routing: [
+ {
+ name: 'logs.gcpcloud',
+ condition: {
+ field: 'cloud.provider',
+ operator: 'eq',
+ value: 'gcp',
+ },
+ },
+ ],
+ },
+ };
+ const response = await putStream(supertest, 'logs', body);
+ expect(response).to.have.property('acknowledged', true);
+ });
+ });
+}
diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/find_slo.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/find_slo.ts
index 6c49cb9df751e..98b0bd1d237f2 100644
--- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/find_slo.ts
+++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/find_slo.ts
@@ -65,24 +65,34 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) {
await retry.tryForTime(180 * 1000, async () => {
let response = await supertestWithoutAuth
.get(`/api/observability/slos`)
+ .query({ page: 1, perPage: 333 })
.set(adminRoleAuthc.apiKeyHeader)
.set(internalHeaders)
.send();
expect(response.body.results).length(2);
+ expect(response.body.page).eql(1);
+ expect(response.body.perPage).eql(333);
+ expect(response.body.total).eql(2);
response = await supertestWithoutAuth
- .get(`/api/observability/slos?kqlQuery=slo.name%3Airrelevant`)
+ .get(`/api/observability/slos`)
+ .query({ size: 222, kqlQuery: 'slo.name:irrelevant' })
.set(adminRoleAuthc.apiKeyHeader)
.set(internalHeaders)
.send()
.expect(200);
+ expect(response.body.page).eql(1); // always return page with default value
+ expect(response.body.perPage).eql(25); // always return perPage with default value
+ expect(response.body.size).eql(222);
+ expect(response.body.searchAfter).ok();
expect(response.body.results).length(1);
expect(response.body.results[0].id).eql(sloId2);
response = await supertestWithoutAuth
- .get(`/api/observability/slos?kqlQuery=slo.name%3Aintegration`)
+ .get(`/api/observability/slos`)
+ .query({ kqlQuery: 'slo.name:integration' })
.set(adminRoleAuthc.apiKeyHeader)
.set(internalHeaders)
.send()
diff --git a/x-pack/test/api_integration/services/security_solution_api.gen.ts b/x-pack/test/api_integration/services/security_solution_api.gen.ts
index 9ffdc1c43a2a1..5021895c1cb68 100644
--- a/x-pack/test/api_integration/services/security_solution_api.gen.ts
+++ b/x-pack/test/api_integration/services/security_solution_api.gen.ts
@@ -138,6 +138,10 @@ import { PreviewRiskScoreRequestBodyInput } from '@kbn/security-solution-plugin/
import { ReadAlertsMigrationStatusRequestQueryInput } from '@kbn/security-solution-plugin/common/api/detection_engine/signals_migration/read_signals_migration_status/read_signals_migration_status.gen';
import { ReadRuleRequestQueryInput } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_management/crud/read_rule/read_rule_route.gen';
import { ResolveTimelineRequestQueryInput } from '@kbn/security-solution-plugin/common/api/timeline/resolve_timeline/resolve_timeline_route.gen';
+import {
+ RetryRuleMigrationRequestParamsInput,
+ RetryRuleMigrationRequestBodyInput,
+} from '@kbn/security-solution-plugin/common/siem_migrations/model/api/rules/rule_migration.gen';
import {
RulePreviewRequestQueryInput,
RulePreviewRequestBodyInput,
@@ -974,6 +978,16 @@ finalize it.
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.query(props.query);
},
+ /**
+ * Retrieves all related integrations
+ */
+ getRuleMigrationIntegrations(kibanaSpace: string = 'default') {
+ return supertest
+ .get(routeWithNamespace('/internal/siem_migrations/rules/integrations', kibanaSpace))
+ .set('kbn-xsrf', 'true')
+ .set(ELASTIC_HTTP_VERSION_HEADER, '1')
+ .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana');
+ },
/**
* Retrieves all available prebuilt rules (installed and installable)
*/
@@ -1402,6 +1416,22 @@ detection engine rules.
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.query(props.query);
},
+ /**
+ * Retries a SIEM rules migration using the migration id provided
+ */
+ retryRuleMigration(props: RetryRuleMigrationProps, kibanaSpace: string = 'default') {
+ return supertest
+ .put(
+ routeWithNamespace(
+ replaceParams('/internal/siem_migrations/rules/{migration_id}/retry', props.params),
+ kibanaSpace
+ )
+ )
+ .set('kbn-xsrf', 'true')
+ .set(ELASTIC_HTTP_VERSION_HEADER, '1')
+ .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
+ .send(props.body as object);
+ },
riskEngineGetPrivileges(kibanaSpace: string = 'default') {
return supertest
.get(routeWithNamespace('/internal/risk_engine/privileges', kibanaSpace))
@@ -1875,6 +1905,10 @@ export interface ReadRuleProps {
export interface ResolveTimelineProps {
query: ResolveTimelineRequestQueryInput;
}
+export interface RetryRuleMigrationProps {
+ params: RetryRuleMigrationRequestParamsInput;
+ body: RetryRuleMigrationRequestBodyInput;
+}
export interface RulePreviewProps {
query: RulePreviewRequestQueryInput;
body: RulePreviewRequestBodyInput;
diff --git a/x-pack/test/reporting_api_integration/reporting_and_security/default_reporting_user_role.ts b/x-pack/test/reporting_api_integration/reporting_and_security/default_reporting_user_role.ts
new file mode 100644
index 0000000000000..b559463eee6e5
--- /dev/null
+++ b/x-pack/test/reporting_api_integration/reporting_and_security/default_reporting_user_role.ts
@@ -0,0 +1,48 @@
+/*
+ * 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';
+
+// eslint-disable-next-line import/no-default-export
+export default function ({ getService }: FtrProviderContext) {
+ const reportingAPI = getService('reportingAPI');
+ const security = getService('security');
+ const log = getService('log');
+
+ const testUserUsername = 'test_reporting_user';
+ const testUserPassword = 'changeme';
+
+ describe('Default reporting_user role', () => {
+ before(async () => {
+ await reportingAPI.initEcommerce();
+
+ log.info('creating test user with reporting_user role');
+ await security.user.create(testUserUsername, {
+ password: testUserPassword,
+ roles: ['data_analyst', 'reporting_user'], // no custom privileges to reporting, uses the built-in role that grants access to all features in all applications and all spaces
+ full_name:
+ 'a reporting user which uses the built-in reporting_user role to access reporting features',
+ });
+ });
+
+ after(async () => {
+ await reportingAPI.teardownEcommerce();
+ });
+
+ it('able to generate CSV report', async () => {
+ log.info('posting test report job with test user account');
+ const reportPath = await reportingAPI.postJob(
+ '/api/reporting/generate/csv_searchsource?jobParams=%28browserTimezone%3AAmerica%2FPhoenix%2Ccolumns%3A%21%28order_date%2Ccategory%2Ccurrency%2Ccustomer_id%2Corder_id%2Cday_of_week_i%2Cproducts.created_on%2Csku%29%2CobjectType%3Asearch%2CsearchSource%3A%28fields%3A%21%28%28field%3Aorder_date%2Cinclude_unmapped%3A%21t%29%2C%28field%3Acategory%2Cinclude_unmapped%3A%21t%29%2C%28field%3Acurrency%2Cinclude_unmapped%3A%21t%29%2C%28field%3Acustomer_id%2Cinclude_unmapped%3A%21t%29%2C%28field%3Aorder_id%2Cinclude_unmapped%3A%21t%29%2C%28field%3Aday_of_week_i%2Cinclude_unmapped%3A%21t%29%2C%28field%3Aproducts.created_on%2Cinclude_unmapped%3A%21t%29%2C%28field%3Asku%2Cinclude_unmapped%3A%21t%29%29%2Cfilter%3A%21%28%28meta%3A%28field%3Aorder_date%2Cindex%3A%275193f870-d861-11e9-a311-0fa548c5f953%27%2Cparams%3A%28%29%29%2Cquery%3A%28range%3A%28order_date%3A%28format%3Astrict_date_optional_time%2Cgte%3A%272019-07-01T20%3A56%3A00.833Z%27%2Clte%3A%272019-07-02T15%3A09%3A46.563Z%27%29%29%29%29%29%2Cindex%3A%275193f870-d861-11e9-a311-0fa548c5f953%27%2Cquery%3A%28language%3Akuery%2Cquery%3A%27%27%29%2Csort%3A%21%28%28order_date%3A%28format%3Astrict_date_optional_time%2Corder%3Adesc%29%29%2C%28order_id%3Adesc%29%29%2Cversion%3A%21t%29%2Ctitle%3A%27Ecommerce%20Data%27%2Cversion%3A%279.0.0%27%29',
+ testUserUsername,
+ testUserPassword
+ );
+ log.info('test report job download path: ', reportPath);
+
+ await reportingAPI.waitForJobToFinish(reportPath, false, testUserUsername, testUserPassword);
+ });
+ });
+}
diff --git a/x-pack/test/reporting_api_integration/reporting_and_security/index.ts b/x-pack/test/reporting_api_integration/reporting_and_security/index.ts
index 8bd69780b44b7..6caffc5d562e1 100644
--- a/x-pack/test/reporting_api_integration/reporting_and_security/index.ts
+++ b/x-pack/test/reporting_api_integration/reporting_and_security/index.ts
@@ -21,6 +21,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./bwc_existing_indexes'));
loadTestFile(require.resolve('./datastream'));
+ loadTestFile(require.resolve('./default_reporting_user_role'));
loadTestFile(require.resolve('./ilm_migration_apis'));
loadTestFile(require.resolve('./security_roles_privileges'));
loadTestFile(require.resolve('./spaces'));
diff --git a/x-pack/test/reporting_api_integration/services/scenarios.ts b/x-pack/test/reporting_api_integration/services/scenarios.ts
index ed542cc74e44e..0800647d2abef 100644
--- a/x-pack/test/reporting_api_integration/services/scenarios.ts
+++ b/x-pack/test/reporting_api_integration/services/scenarios.ts
@@ -25,9 +25,9 @@ export function createScenarios({ getService }: Pick => {
+ const postJob = async (
+ apiPath: string,
+ username = 'elastic',
+ password = process.env.TEST_KIBANA_PASS || 'changeme'
+ ): Promise => {
log.debug(`ReportingAPI.postJob(${apiPath})`);
- const { body } = await supertest
+ const { body } = await supertestWithoutAuth
.post(removeWhitespace(apiPath))
+ .auth(username, password)
.set('kbn-xsrf', 'xxx')
.expect(200);
return body.path;
diff --git a/x-pack/test/reporting_api_integration/services/usage.ts b/x-pack/test/reporting_api_integration/services/usage.ts
index 3e9167e44e522..52ec94e7e7f53 100644
--- a/x-pack/test/reporting_api_integration/services/usage.ts
+++ b/x-pack/test/reporting_api_integration/services/usage.ts
@@ -13,17 +13,25 @@ import { FtrProviderContext } from '../ftr_provider_context';
export function createUsageServices({ getService }: FtrProviderContext) {
const log = getService('log');
const esSupertest = getService('esSupertest');
- const supertest = getService('supertest');
+ const supertestWithoutAuth = getService('supertestWithoutAuth');
return {
- async waitForJobToFinish(downloadReportPath: string, ignoreFailure = false) {
+ async waitForJobToFinish(
+ downloadReportPath: string,
+ ignoreFailure = false,
+ username = 'elastic',
+ password = process.env.TEST_KIBANA_PASS || 'changeme'
+ ) {
log.debug(`Waiting for job to finish: ${downloadReportPath}`);
const JOB_IS_PENDING_CODE = 503;
let response: Response & { statusCode?: number };
const statusCode = await new Promise((resolve) => {
const intervalId = setInterval(async () => {
- response = await supertest.get(downloadReportPath).responseType('blob');
+ response = await supertestWithoutAuth
+ .get(downloadReportPath)
+ .auth(username, password)
+ .responseType('blob');
if (response.statusCode === 503) {
log.debug(`Report at path ${downloadReportPath} is pending`);
} else if (response.statusCode === 200) {
@@ -38,12 +46,14 @@ export function createUsageServices({ getService }: FtrProviderContext) {
}, 1500);
});
if (!ignoreFailure) {
- const jobInfo = await supertest.get(
- downloadReportPath.replace(
- PUBLIC_ROUTES.JOBS.DOWNLOAD_PREFIX,
- INTERNAL_ROUTES.JOBS.INFO_PREFIX
+ const jobInfo = await supertestWithoutAuth
+ .get(
+ downloadReportPath.replace(
+ PUBLIC_ROUTES.JOBS.DOWNLOAD_PREFIX,
+ INTERNAL_ROUTES.JOBS.INFO_PREFIX
+ )
)
- );
+ .auth(username, password);
expect(jobInfo.body.output.warnings).to.be(undefined); // expect no failure message to be present in job info
expect(statusCode).to.be(200);
}
diff --git a/yarn.lock b/yarn.lock
index 4b2ec974f4314..e3be6bba02f8a 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4217,11 +4217,11 @@
version "0.0.0"
uid ""
-"@kbn/code-editor-mock@link:packages/shared-ux/code_editor/mocks":
+"@kbn/code-editor-mock@link:src/platform/packages/shared/shared-ux/code_editor/mocks":
version "0.0.0"
uid ""
-"@kbn/code-editor@link:packages/shared-ux/code_editor/impl":
+"@kbn/code-editor@link:src/platform/packages/shared/shared-ux/code_editor/impl":
version "0.0.0"
uid ""
@@ -6445,7 +6445,7 @@
version "0.0.0"
uid ""
-"@kbn/monaco@link:packages/kbn-monaco":
+"@kbn/monaco@link:src/platform/packages/shared/kbn-monaco":
version "0.0.0"
uid ""