diff --git a/packages/kbn-apm-synthtrace-client/src/lib/apm/apm_fields.ts b/packages/kbn-apm-synthtrace-client/src/lib/apm/apm_fields.ts index 7c3ab2c2b1e3a..3ee43dc63f04f 100644 --- a/packages/kbn-apm-synthtrace-client/src/lib/apm/apm_fields.ts +++ b/packages/kbn-apm-synthtrace-client/src/lib/apm/apm_fields.ts @@ -50,6 +50,35 @@ export interface GeoLocation { type: string; } +export interface APMStacktrace { + abs_path?: string; + classname?: string; + context?: { + post?: string[]; + pre?: string[]; + }; + exclude_from_grouping?: boolean; + filename?: string; + function?: string; + module?: string; + library_frame?: boolean; + line?: + | { + column?: number; + number: number; + } + | { + context?: string; + }; + sourcemap?: { + error?: string; + updated?: boolean; + }; + vars?: { + [key: string]: unknown; + }; +} + type ExperimentalFields = Partial<{ 'metricset.interval': string; 'transaction.duration.summary': string; @@ -80,6 +109,8 @@ export type ApmFields = Fields<{ 'cloud.provider': string; 'cloud.region': string; 'cloud.service.name': string; + // otel + 'code.stacktrace': string; 'container.id': string; 'destination.address': string; 'destination.port': number; @@ -169,6 +200,7 @@ export type ApmFields = Fields<{ 'span.duration.us': number; 'span.id': string; 'span.name': string; + 'span.stacktrace': APMStacktrace[]; 'span.self_time.count': number; 'span.self_time.sum.us': number; 'span.subtype': string; diff --git a/packages/kbn-check-mappings-update-cli/current_fields.json b/packages/kbn-check-mappings-update-cli/current_fields.json index 7f7c317f7f63a..c745e4886cde9 100644 --- a/packages/kbn-check-mappings-update-cli/current_fields.json +++ b/packages/kbn-check-mappings-update-cli/current_fields.json @@ -968,5 +968,8 @@ "kuery", "serviceEnvironmentFilterEnabled", "serviceNameFilterEnabled" + ], + "cloud-security-posture-settings": [ + "rules" ] } diff --git a/packages/kbn-check-mappings-update-cli/current_mappings.json b/packages/kbn-check-mappings-update-cli/current_mappings.json index fe4b3dba0940d..7f5c94647b941 100644 --- a/packages/kbn-check-mappings-update-cli/current_mappings.json +++ b/packages/kbn-check-mappings-update-cli/current_mappings.json @@ -2349,6 +2349,10 @@ } } }, + "cloud-security-posture-settings": { + "dynamic": false, + "properties": {} + }, "slo": { "dynamic": false, "properties": { diff --git a/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts b/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts index a2235b0f77812..f02dd54f791b4 100644 --- a/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts +++ b/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts @@ -76,6 +76,7 @@ describe('checking migration metadata changes on all registered SO types', () => "cases-connector-mappings": "f9d1ac57e484e69506c36a8051e4d61f4a8cfd25", "cases-telemetry": "f219eb7e26772884342487fc9602cfea07b3cedc", "cases-user-actions": "483f10db9b3bd1617948d7032a98b7791bf87414", + "cloud-security-posture-settings": "675e47dd958fbce6c70a20baac12af3145e7c0ef", "config": "179b3e2bc672626aafce3cf92093a113f456af38", "config-global": "8e8a134a2952df700d7d4ec51abb794bbd4cf6da", "connector_token": "5a9ac29fe9c740eb114e9c40517245c71706b005", diff --git a/src/core/server/integration_tests/saved_objects/migrations/group3/type_registrations.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group3/type_registrations.test.ts index 2cef3801868bd..99e2692523f6d 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/group3/type_registrations.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/group3/type_registrations.test.ts @@ -29,6 +29,7 @@ const previouslyRegisteredTypes = [ 'canvas-element', 'canvas-workpad', 'canvas-workpad-template', + 'cloud-security-posture-settings', 'cases', 'cases-comments', 'cases-configure', diff --git a/src/core/server/integration_tests/saved_objects/migrations/group5/dot_kibana_split.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group5/dot_kibana_split.test.ts index c39ceaf30da69..9f9c3a3c7bd58 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/group5/dot_kibana_split.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/group5/dot_kibana_split.test.ts @@ -197,6 +197,7 @@ describe('split .kibana index into multiple system indices', () => { "cases-connector-mappings", "cases-telemetry", "cases-user-actions", + "cloud-security-posture-settings", "config", "config-global", "connector_token", diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/transaction_details/generate_span_stacktrace_data.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/transaction_details/generate_span_stacktrace_data.ts new file mode 100644 index 0000000000000..85eeb2cd456ef --- /dev/null +++ b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/transaction_details/generate_span_stacktrace_data.ts @@ -0,0 +1,106 @@ +/* + * 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 { apm, timerange } from '@kbn/apm-synthtrace-client'; +import { synthtrace } from '../../../synthtrace'; + +function getAPMGeneratedStacktrace() { + const apmGeneratedStacktrace = apm + .service({ + name: 'apm-generated', + environment: 'production', + agentName: 'java', + }) + .instance('instance a'); + + return Array.from( + timerange( + new Date('2022-01-01T00:00:00.000Z'), + new Date('2022-01-01T00:01:00.000Z') + ) + .interval('1m') + .rate(1) + .generator((timestamp) => { + return apmGeneratedStacktrace + .transaction({ transactionName: `Transaction A` }) + .defaults({ + 'service.language.name': 'java', + }) + .timestamp(timestamp) + .duration(1000) + .success() + .children( + apmGeneratedStacktrace + .span({ + spanName: `Span A`, + spanType: 'internal', + 'span.stacktrace': [ + { + library_frame: false, + exclude_from_grouping: false, + filename: 'OutputBuffer.java', + classname: 'org.apache.catalina.connector.OutputBuffer', + line: { number: 825 }, + module: 'org.apache.catalina.connector', + function: 'flushByteBuffer', + }, + ], + }) + .timestamp(timestamp + 50) + .duration(100) + .failure() + ); + }) + ); +} + +function getOtelGeneratedStacktrace() { + const apmGeneratedStacktrace = apm + .service({ + name: 'otel-generated', + environment: 'production', + agentName: 'java', + }) + .instance('instance a'); + + return Array.from( + timerange( + new Date('2022-01-01T00:00:00.000Z'), + new Date('2022-01-01T00:01:00.000Z') + ) + .interval('1m') + .rate(1) + .generator((timestamp) => { + return apmGeneratedStacktrace + .transaction({ transactionName: `Transaction A` }) + .timestamp(timestamp) + .duration(1000) + .defaults({ + 'service.language.name': 'java', + }) + .success() + .children( + apmGeneratedStacktrace + .span({ + spanName: `Span A`, + spanType: 'internal', + 'code.stacktrace': + 'java.lang.Throwable\n\tat co.elastic.otel.ElasticSpanProcessor.captureStackTrace(ElasticSpanProcessor.java:81)', + }) + .timestamp(timestamp + 50) + .duration(100) + .failure() + ); + }) + ); +} + +export function generateSpanStacktraceData() { + const apmGeneratedStacktrace = getAPMGeneratedStacktrace(); + const otelGeneratedStacktrace = getOtelGeneratedStacktrace(); + + synthtrace.index([...apmGeneratedStacktrace, ...otelGeneratedStacktrace]); +} diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/transaction_details/span_stacktrace.cy.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/transaction_details/span_stacktrace.cy.ts new file mode 100644 index 0000000000000..dbc4a1072ffa0 --- /dev/null +++ b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/transaction_details/span_stacktrace.cy.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import url from 'url'; +import { synthtrace } from '../../../synthtrace'; +import { generateSpanStacktraceData } from './generate_span_stacktrace_data'; + +const start = '2022-01-01T00:00:00.000Z'; +const end = '2022-01-01T00:15:00.000Z'; + +function getServiceInventoryUrl({ serviceName }: { serviceName: string }) { + return url.format({ + pathname: `/app/apm/services/${serviceName}`, + query: { + rangeFrom: start, + rangeTo: end, + environment: 'ENVIRONMENT_ALL', + kuery: '', + serviceGroup: '', + transactionType: 'request', + comparisonEnabled: true, + offset: '1d', + }, + }); +} + +describe('Span stacktrace', () => { + beforeEach(() => { + cy.loginAsViewerUser(); + }); + describe('span flyout', () => { + before(() => { + generateSpanStacktraceData(); + }); + + after(() => { + synthtrace.clean(); + }); + it('Shows APM agent generated stacktrace', () => { + cy.visitKibana(getServiceInventoryUrl({ serviceName: 'apm-generated' })); + cy.contains('Transaction A').click(); + cy.contains('Span A').click(); + cy.getByTestSubj('spanStacktraceTab').click(); + cy.contains( + 'at org.apache.catalina.connector.OutputBuffer.flushByteBuffer(OutputBuffer.java:825)' + ); + }); + + it('Shows Otel generated stacktrace', () => { + cy.visitKibana(getServiceInventoryUrl({ serviceName: 'otel-generated' })); + cy.contains('Transaction A').click(); + cy.contains('Span A').click(); + cy.getByTestSubj('spanStacktraceTab').click(); + cy.contains( + `java.lang.Throwable at co.elastic.otel.ElasticSpanProcessor.captureStackTrace(ElasticSpanProcessor.java:81)` + ); + }); + }); +}); diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/span_flyout/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/span_flyout/index.tsx index 59b06577b2757..f32bd7be896d2 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/span_flyout/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/span_flyout/index.tsx @@ -25,6 +25,7 @@ import { euiStyled } from '@kbn/kibana-react-plugin/common'; import { ProcessorEvent } from '@kbn/observability-plugin/common'; import { isEmpty } from 'lodash'; import React, { Fragment } from 'react'; +import { PlaintextStacktrace } from '../../../../../error_group_details/error_sampler/plaintext_stacktrace'; import { Span } from '../../../../../../../../typings/es_schemas/ui/span'; import { Transaction } from '../../../../../../../../typings/es_schemas/ui/transaction'; import { useFetcher, isPending } from '../../../../../../../hooks/use_fetcher'; @@ -204,6 +205,7 @@ function SpanFlyoutBody({ flyoutDetailTab?: string; }) { const stackframes = span.span.stacktrace; + const plaintextStacktrace = span.code?.stacktrace; const codeLanguage = parentTransaction?.service.language?.name; const spanDb = span.span.db; const spanTypes = getSpanTypes(span); @@ -232,10 +234,11 @@ function SpanFlyoutBody({ ), }, - ...(!isEmpty(stackframes) + ...(!isEmpty(stackframes) || !isEmpty(plaintextStacktrace) ? [ { id: 'stack-trace', + 'data-test-subj': 'spanStacktraceTab', name: i18n.translate( 'xpack.apm.transactionDetails.spanFlyout.stackTraceTabLabel', { @@ -245,10 +248,17 @@ function SpanFlyoutBody({ content: ( - + {stackframes ? ( + + ) : ( + + )} ), }, diff --git a/x-pack/plugins/apm/public/components/shared/stacktrace/index.tsx b/x-pack/plugins/apm/public/components/shared/stacktrace/index.tsx index 4a3646dbb78c1..379dd5700406e 100644 --- a/x-pack/plugins/apm/public/components/shared/stacktrace/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/stacktrace/index.tsx @@ -17,6 +17,7 @@ import { Stackframe as StackframeComponent } from './stackframe'; interface Props { stackframes?: Stackframe[]; codeLanguage?: string; + stackTrace?: string; } export function Stacktrace({ stackframes = [], codeLanguage }: Props) { diff --git a/x-pack/plugins/apm/typings/es_schemas/raw/span_raw.ts b/x-pack/plugins/apm/typings/es_schemas/raw/span_raw.ts index 1cac68f74b8b7..301a4c96dfa35 100644 --- a/x-pack/plugins/apm/typings/es_schemas/raw/span_raw.ts +++ b/x-pack/plugins/apm/typings/es_schemas/raw/span_raw.ts @@ -71,6 +71,9 @@ export interface SpanRaw extends APMBaseDoc { id: string; }; child?: { id: string[] }; + code?: { + stacktrace?: string; + }; http?: Http; url?: Url; } diff --git a/x-pack/plugins/cloud_security_posture/common/constants.ts b/x-pack/plugins/cloud_security_posture/common/constants.ts index 5e56b1131f10f..7f4f8c796f4c1 100644 --- a/x-pack/plugins/cloud_security_posture/common/constants.ts +++ b/x-pack/plugins/cloud_security_posture/common/constants.ts @@ -26,11 +26,14 @@ export const BENCHMARKS_API_CURRENT_VERSION = '1'; export const FIND_CSP_BENCHMARK_RULE_ROUTE_PATH = '/internal/cloud_security_posture/rules/_find'; export const FIND_CSP_BENCHMARK_RULE_API_CURRENT_VERSION = '1'; -export const DETECTION_RULE_ALERTS_STATUS_API_CURRENT_VERSION = '1'; -export const DETECTION_RULE_RULES_API_CURRENT_VERSION = '2023-10-31'; +export const CSP_BENCHMARK_RULES_BULK_ACTION_ROUTE_PATH = + '/internal/cloud_security_posture/rules/_bulk_action'; +export const CSP_BENCHMARK_RULES_BULK_ACTION_API_CURRENT_VERSION = '1'; export const GET_DETECTION_RULE_ALERTS_STATUS_PATH = '/internal/cloud_security_posture/detection_engine_rules/alerts/_status'; +export const DETECTION_RULE_ALERTS_STATUS_API_CURRENT_VERSION = '1'; +export const DETECTION_RULE_RULES_API_CURRENT_VERSION = '2023-10-31'; export const CLOUD_SECURITY_POSTURE_PACKAGE_NAME = 'cloud_security_posture'; // TODO: REMOVE CSP_LATEST_FINDINGS_DATA_VIEW and replace it with LATEST_FINDINGS_INDEX_PATTERN @@ -88,6 +91,8 @@ export const INTERNAL_FEATURE_FLAGS = { } as const; export const CSP_BENCHMARK_RULE_SAVED_OBJECT_TYPE = 'csp-rule-template'; +export const INTERNAL_CSP_SETTINGS_SAVED_OBJECT_TYPE = 'cloud-security-posture-settings'; +export const INTERNAL_CSP_SETTINGS_SAVED_OBJECT_ID = 'csp-internal-settings'; export const CLOUDBEAT_VANILLA = 'cloudbeat/cis_k8s'; export const CLOUDBEAT_EKS = 'cloudbeat/cis_eks'; diff --git a/x-pack/plugins/cloud_security_posture/common/types/rules/v3.ts b/x-pack/plugins/cloud_security_posture/common/types/rules/v3.ts index 0b7b90339dff1..cef3e445b91a8 100644 --- a/x-pack/plugins/cloud_security_posture/common/types/rules/v3.ts +++ b/x-pack/plugins/cloud_security_posture/common/types/rules/v3.ts @@ -129,3 +129,36 @@ export interface FindCspBenchmarkRuleResponse { page: number; perPage: number; } + +export const cspBenchmarkRules = schema.arrayOf( + schema.object({ + benchmark_id: schema.string(), + benchmark_version: schema.string(), + rule_number: schema.string(), + }) +); + +export const cspBenchmarkRulesBulkActionRequestSchema = schema.object({ + action: schema.oneOf([schema.literal('mute'), schema.literal('unmute')]), + rules: cspBenchmarkRules, +}); + +export type CspBenchmarkRules = TypeOf; + +export type CspBenchmarkRulesBulkActionRequestSchema = TypeOf< + typeof cspBenchmarkRulesBulkActionRequestSchema +>; + +const rulesStates = schema.recordOf( + schema.string(), + schema.object({ + muted: schema.boolean(), + }) +); + +export const cspSettingsSchema = schema.object({ + rules: rulesStates, +}); + +export type CspBenchmarkRulesStates = TypeOf; +export type CspSettings = TypeOf; diff --git a/x-pack/plugins/cloud_security_posture/server/plugin.ts b/x-pack/plugins/cloud_security_posture/server/plugin.ts index 8b77581efd39d..58a143b220857 100755 --- a/x-pack/plugins/cloud_security_posture/server/plugin.ts +++ b/x-pack/plugins/cloud_security_posture/server/plugin.ts @@ -35,7 +35,7 @@ import type { CspServerPluginStartServices, } from './types'; import { setupRoutes } from './routes/setup_routes'; -import { setupSavedObjects } from './saved_objects'; +import { cspBenchmarkRule, cspSettings } from './saved_objects'; import { initializeCspIndices } from './create_indices/create_indices'; import { initializeCspTransforms } from './create_transforms/create_transforms'; import { @@ -50,6 +50,7 @@ import { } from './tasks/findings_stats_task'; import { registerCspmUsageCollector } from './lib/telemetry/collectors/register'; import { CloudSecurityPostureConfig } from './config'; +import { CspBenchmarkRule, CspSettings } from '../common/types/latest'; export class CspPlugin implements @@ -80,7 +81,8 @@ export class CspPlugin core: CoreSetup, plugins: CspServerPluginSetupDeps ): CspServerPluginSetup { - setupSavedObjects(core.savedObjects); + core.savedObjects.registerType(cspBenchmarkRule); + core.savedObjects.registerType(cspSettings); setupRoutes({ core, diff --git a/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/bulk_action/bulk_action.test.ts b/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/bulk_action/bulk_action.test.ts new file mode 100644 index 0000000000000..1bb2232d2834d --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/bulk_action/bulk_action.test.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 expect from 'expect'; +import { setRulesStates, buildRuleKey } from './utils'; + +describe('CSP Rule State Management', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should set rules states correctly', () => { + const ruleIds = ['rule1', 'rule3']; + const newState = true; + + const updatedRulesStates = setRulesStates(ruleIds, newState); + + expect(updatedRulesStates).toEqual({ + rule1: { muted: true }, + rule3: { muted: true }, + }); + }); + + it('should build a rule key with the provided benchmarkId, benchmarkVersion, and ruleNumber', () => { + const benchmarkId = 'randomId'; + const benchmarkVersion = 'v1'; + const ruleNumber = '001'; + + const result = buildRuleKey(benchmarkId, benchmarkVersion, ruleNumber); + + expect(result).toBe(`${benchmarkId};${benchmarkVersion};${ruleNumber}`); + }); +}); diff --git a/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/bulk_action/bulk_action.ts b/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/bulk_action/bulk_action.ts new file mode 100644 index 0000000000000..a87df39341741 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/bulk_action/bulk_action.ts @@ -0,0 +1,68 @@ +/* + * 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 { transformError } from '@kbn/securitysolution-es-utils'; +import { + CspBenchmarkRulesBulkActionRequestSchema, + CspBenchmarkRulesStates, + cspBenchmarkRulesBulkActionRequestSchema, +} from '../../../../common/types/rules/v3'; +import { CspRouter } from '../../../types'; + +import { CSP_BENCHMARK_RULES_BULK_ACTION_ROUTE_PATH } from '../../../../common/constants'; +import { bulkActionBenchmarkRulesHandler } from './v1'; + +export const defineBulkActionCspBenchmarkRulesRoute = (router: CspRouter) => + router.versioned + .post({ + access: 'internal', + path: CSP_BENCHMARK_RULES_BULK_ACTION_ROUTE_PATH, + }) + .addVersion( + { + version: '1', + validate: { + request: { + body: cspBenchmarkRulesBulkActionRequestSchema, + }, + }, + }, + async (context, request, response) => { + if (!(await context.fleet).authz.fleet.all) { + return response.forbidden(); + } + const cspContext = await context.csp; + + try { + const requestBody: CspBenchmarkRulesBulkActionRequestSchema = request.body; + + const benchmarkRulesToUpdate = requestBody.rules; + + const handlerResponse = await bulkActionBenchmarkRulesHandler( + cspContext.encryptedSavedObjects, + benchmarkRulesToUpdate, + requestBody.action + ); + + const updatedBenchmarkRules: CspBenchmarkRulesStates = handlerResponse; + return response.ok({ + body: { + updated_benchmark_rules: updatedBenchmarkRules, + message: 'The bulk operation has been executed successfully.', + }, + }); + } catch (err) { + const error = transformError(err); + + cspContext.logger.error(`Bulk action failed: ${error.message}`); + return response.customError({ + body: { message: error.message }, + statusCode: error.statusCode || 500, // Default to 500 if no specific status code is provided + }); + } + } + ); diff --git a/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/bulk_action/utils.ts b/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/bulk_action/utils.ts new file mode 100644 index 0000000000000..3442b3f050b90 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/bulk_action/utils.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 type { + SavedObjectsClientContract, + SavedObjectsUpdateResponse, +} from '@kbn/core-saved-objects-api-server'; +import { CspBenchmarkRulesStates, CspSettings } from '../../../../common/types/rules/v3'; + +import { + INTERNAL_CSP_SETTINGS_SAVED_OBJECT_ID, + INTERNAL_CSP_SETTINGS_SAVED_OBJECT_TYPE, +} from '../../../../common/constants'; + +export const updateRulesStates = async ( + encryptedSoClient: SavedObjectsClientContract, + newRulesStates: CspBenchmarkRulesStates +): Promise> => { + return await encryptedSoClient.update( + INTERNAL_CSP_SETTINGS_SAVED_OBJECT_TYPE, + INTERNAL_CSP_SETTINGS_SAVED_OBJECT_ID, + { rules: newRulesStates }, + // if there is no saved object yet, insert a new SO + { upsert: { rules: newRulesStates } } + ); +}; + +export const setRulesStates = (ruleIds: string[], state: boolean): CspBenchmarkRulesStates => { + const rulesStates: CspBenchmarkRulesStates = {}; + ruleIds.forEach((ruleId) => { + rulesStates[ruleId] = { muted: state }; + }); + return rulesStates; +}; + +export const buildRuleKey = (benchmarkId: string, benchmarkVersion: string, ruleNumber: string) => { + return `${benchmarkId};${benchmarkVersion};${ruleNumber}`; +}; diff --git a/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/bulk_action/v1.ts b/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/bulk_action/v1.ts new file mode 100644 index 0000000000000..42895b5eb694d --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/bulk_action/v1.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server'; +import { CspBenchmarkRules, CspBenchmarkRulesStates } from '../../../../common/types/rules/v3'; +import { buildRuleKey, setRulesStates, updateRulesStates } from './utils'; + +const muteStatesMap = { + mute: true, + unmute: false, +}; + +export const bulkActionBenchmarkRulesHandler = async ( + encryptedSoClient: SavedObjectsClientContract, + rulesToUpdate: CspBenchmarkRules, + action: 'mute' | 'unmute' +): Promise => { + const ruleKeys = rulesToUpdate.map((rule) => + buildRuleKey(rule.benchmark_id, rule.benchmark_version, rule.rule_number) + ); + + const newRulesStates = setRulesStates(ruleKeys, muteStatesMap[action]); + + const newCspSettings = await updateRulesStates(encryptedSoClient, newRulesStates); + + return newCspSettings.attributes.rules!; +}; diff --git a/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/find/find.test.ts b/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/find/find.test.ts index ffec7d5b9b261..2a0130c0f0fd7 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/find/find.test.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/find/find.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { getSortedCspBenchmarkRulesTemplates } from './v1'; +import { getSortedCspBenchmarkRulesTemplates } from './utils'; import { CspBenchmarkRule } from '../../../../common/types/latest'; describe('getSortedCspBenchmarkRules', () => { diff --git a/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/find/find.ts b/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/find/find.ts index aa4856fd9bada..728fca1c0ae7c 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/find/find.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/find/find.ts @@ -9,11 +9,10 @@ import { FindCspBenchmarkRuleRequest, FindCspBenchmarkRuleResponse, findCspBenchmarkRuleRequestSchema, -} from '../../../../common/types/latest'; - +} from '../../../../common/types/rules/v3'; import { FIND_CSP_BENCHMARK_RULE_ROUTE_PATH } from '../../../../common/constants'; import { CspRouter } from '../../../types'; -import { findRuleHandler as findRuleHandlerV1 } from './v1'; +import { findBenchmarkRuleHandler as findRuleHandlerV1 } from './v1'; export const defineFindCspBenchmarkRuleRoute = (router: CspRouter) => router.versioned diff --git a/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/find/utils.ts b/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/find/utils.ts new file mode 100644 index 0000000000000..0601a4eb25577 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/find/utils.ts @@ -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 semverValid from 'semver/functions/valid'; +import semverCompare from 'semver/functions/compare'; +import { NewPackagePolicy } from '@kbn/fleet-plugin/common'; +import { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server'; +import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../benchmarks/benchmarks'; +import { getBenchmarkFromPackagePolicy } from '../../../../common/utils/helpers'; + +import type { CspBenchmarkRule } from '../../../../common/types/latest'; + +export const getSortedCspBenchmarkRulesTemplates = (cspBenchmarkRules: CspBenchmarkRule[]) => { + return cspBenchmarkRules.slice().sort((a, b) => { + const ruleNumberA = a?.metadata?.benchmark?.rule_number; + const ruleNumberB = b?.metadata?.benchmark?.rule_number; + + const versionA = semverValid(ruleNumberA); + const versionB = semverValid(ruleNumberB); + + if (versionA !== null && versionB !== null) { + return semverCompare(versionA, versionB); + } else { + return String(ruleNumberA).localeCompare(String(ruleNumberB)); + } + }); +}; + +export const getBenchmarkIdFromPackagePolicyId = async ( + soClient: SavedObjectsClientContract, + packagePolicyId: string +): Promise => { + const res = await soClient.get( + PACKAGE_POLICY_SAVED_OBJECT_TYPE, + packagePolicyId + ); + return getBenchmarkFromPackagePolicy(res.attributes.inputs); +}; diff --git a/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/find/v1.ts b/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/find/v1.ts index 6bd0d14a2f540..4971098cb8067 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/find/v1.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/find/v1.ts @@ -4,49 +4,17 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import semverValid from 'semver/functions/valid'; -import semverCompare from 'semver/functions/compare'; -import { NewPackagePolicy } from '@kbn/fleet-plugin/common'; import { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server'; import { getBenchmarkFilter } from '../../../../common/utils/helpers'; import { CSP_BENCHMARK_RULE_SAVED_OBJECT_TYPE } from '../../../../common/constants'; -import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../benchmarks/benchmarks'; -import { getBenchmarkFromPackagePolicy } from '../../../../common/utils/helpers'; - +import { getBenchmarkIdFromPackagePolicyId, getSortedCspBenchmarkRulesTemplates } from './utils'; +import type { CspBenchmarkRule } from '../../../../common/types/latest'; import type { - CspBenchmarkRule, FindCspBenchmarkRuleRequest, FindCspBenchmarkRuleResponse, -} from '../../../../common/types/latest'; - -export const getSortedCspBenchmarkRulesTemplates = (cspBenchmarkRules: CspBenchmarkRule[]) => { - return cspBenchmarkRules.slice().sort((a, b) => { - const ruleNumberA = a?.metadata?.benchmark?.rule_number; - const ruleNumberB = b?.metadata?.benchmark?.rule_number; - - const versionA = semverValid(ruleNumberA); - const versionB = semverValid(ruleNumberB); - - if (versionA !== null && versionB !== null) { - return semverCompare(versionA, versionB); - } else { - return String(ruleNumberA).localeCompare(String(ruleNumberB)); - } - }); -}; - -export const getBenchmarkIdFromPackagePolicyId = async ( - soClient: SavedObjectsClientContract, - packagePolicyId: string -): Promise => { - const res = await soClient.get( - PACKAGE_POLICY_SAVED_OBJECT_TYPE, - packagePolicyId - ); - return getBenchmarkFromPackagePolicy(res.attributes.inputs); -}; +} from '../../../../common/types/rules/v3'; -export const findRuleHandler = async ( +export const findBenchmarkRuleHandler = async ( soClient: SavedObjectsClientContract, options: FindCspBenchmarkRuleRequest ): Promise => { diff --git a/x-pack/plugins/cloud_security_posture/server/routes/setup_routes.ts b/x-pack/plugins/cloud_security_posture/server/routes/setup_routes.ts index 49022dd1f5fe6..88570781ed066 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/setup_routes.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/setup_routes.ts @@ -7,6 +7,7 @@ import type { CoreSetup, Logger } from '@kbn/core/server'; import type { AuthenticatedUser } from '@kbn/security-plugin/common'; +import { INTERNAL_CSP_SETTINGS_SAVED_OBJECT_TYPE } from '../../common/constants'; import type { CspRequestHandlerContext, CspServerPluginStart, @@ -19,6 +20,7 @@ import { defineGetBenchmarksRoute } from './benchmarks/benchmarks'; import { defineGetCspStatusRoute } from './status/status'; import { defineFindCspBenchmarkRuleRoute } from './benchmark_rules/find/find'; import { defineGetDetectionEngineAlertsStatus } from './detection_engine/get_detection_engine_alerts_count_by_rule_tags'; +import { defineBulkActionCspBenchmarkRulesRoute } from './benchmark_rules/bulk_action/bulk_action'; /** * 1. Registers routes @@ -40,6 +42,7 @@ export async function setupRoutes({ defineGetCspStatusRoute(router); defineFindCspBenchmarkRuleRoute(router); defineGetDetectionEngineAlertsStatus(router); + defineBulkActionCspBenchmarkRulesRoute(router); core.http.registerRouteHandlerContext( PLUGIN_ID, @@ -61,6 +64,9 @@ export async function setupRoutes({ logger, esClient: coreContext.elasticsearch.client, soClient: coreContext.savedObjects.client, + encryptedSavedObjects: coreContext.savedObjects.getClient({ + includedHiddenTypes: [INTERNAL_CSP_SETTINGS_SAVED_OBJECT_TYPE], + }), agentPolicyService: fleet.agentPolicyService, agentService: fleet.agentService, packagePolicyService: fleet.packagePolicyService, diff --git a/x-pack/plugins/cloud_security_posture/server/saved_objects/csp_settings.ts b/x-pack/plugins/cloud_security_posture/server/saved_objects/csp_settings.ts new file mode 100644 index 0000000000000..d163ca6e26f05 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/server/saved_objects/csp_settings.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SavedObjectsType } from '@kbn/core/server'; +import { SECURITY_SOLUTION_SAVED_OBJECT_INDEX } from '@kbn/core-saved-objects-server'; +import { cspSettingsSchema } from '../../common/types/rules/v3'; +import { cspSettingsSavedObjectMapping } from './mappings'; +import { INTERNAL_CSP_SETTINGS_SAVED_OBJECT_TYPE } from '../../common/constants'; + +export const cspSettings: SavedObjectsType = { + name: INTERNAL_CSP_SETTINGS_SAVED_OBJECT_TYPE, + indexPattern: SECURITY_SOLUTION_SAVED_OBJECT_INDEX, + hidden: true, + namespaceType: 'agnostic', + schemas: { + '8.12.0': cspSettingsSchema, + }, + mappings: cspSettingsSavedObjectMapping, +}; diff --git a/x-pack/plugins/cloud_security_posture/server/saved_objects/index.ts b/x-pack/plugins/cloud_security_posture/server/saved_objects/index.ts index 7217fd8606ed9..98b08c2fad402 100644 --- a/x-pack/plugins/cloud_security_posture/server/saved_objects/index.ts +++ b/x-pack/plugins/cloud_security_posture/server/saved_objects/index.ts @@ -5,4 +5,5 @@ * 2.0. */ -export { setupSavedObjects } from './saved_objects'; +export { cspBenchmarkRule } from './csp_benchmark_rule'; +export { cspSettings } from './csp_settings'; diff --git a/x-pack/plugins/cloud_security_posture/server/saved_objects/mappings.ts b/x-pack/plugins/cloud_security_posture/server/saved_objects/mappings.ts index f672d271c8b5f..e2e880f72a7c9 100644 --- a/x-pack/plugins/cloud_security_posture/server/saved_objects/mappings.ts +++ b/x-pack/plugins/cloud_security_posture/server/saved_objects/mappings.ts @@ -60,3 +60,8 @@ export const cspBenchmarkRuleSavedObjectMapping: SavedObjectsTypeMappingDefiniti }, }, }; + +export const cspSettingsSavedObjectMapping: SavedObjectsTypeMappingDefinition = { + dynamic: false, + properties: {}, +}; diff --git a/x-pack/plugins/cloud_security_posture/server/saved_objects/saved_objects.ts b/x-pack/plugins/cloud_security_posture/server/saved_objects/saved_objects.ts deleted file mode 100644 index eed21b5b91916..0000000000000 --- a/x-pack/plugins/cloud_security_posture/server/saved_objects/saved_objects.ts +++ /dev/null @@ -1,36 +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 { SavedObjectsServiceSetup } from '@kbn/core/server'; -import { SECURITY_SOLUTION_SAVED_OBJECT_INDEX } from '@kbn/core-saved-objects-server'; -import { rulesV1, rulesV2, rulesV3 } from '../../common/types'; -import { cspBenchmarkRuleSavedObjectMapping } from './mappings'; -import { cspBenchmarkRuleMigrations } from './migrations'; - -import { CspBenchmarkRule } from '../../common/types/latest'; - -import { CSP_BENCHMARK_RULE_SAVED_OBJECT_TYPE } from '../../common/constants'; - -export function setupSavedObjects(savedObjects: SavedObjectsServiceSetup) { - savedObjects.registerType({ - name: CSP_BENCHMARK_RULE_SAVED_OBJECT_TYPE, - indexPattern: SECURITY_SOLUTION_SAVED_OBJECT_INDEX, - hidden: false, - namespaceType: 'agnostic', - management: { - importableAndExportable: true, - visibleInManagement: true, - }, - schemas: { - '8.3.0': rulesV1.cspBenchmarkRuleSchema, - '8.4.0': rulesV2.cspBenchmarkRuleSchema, - '8.7.0': rulesV3.cspBenchmarkRuleSchema, - }, - migrations: cspBenchmarkRuleMigrations, - mappings: cspBenchmarkRuleSavedObjectMapping, - }); -} diff --git a/x-pack/plugins/cloud_security_posture/server/types.ts b/x-pack/plugins/cloud_security_posture/server/types.ts index 3c84de12decf6..b6e83939b6ca7 100644 --- a/x-pack/plugins/cloud_security_posture/server/types.ts +++ b/x-pack/plugins/cloud_security_posture/server/types.ts @@ -69,6 +69,7 @@ export interface CspApiRequestHandlerContext { logger: Logger; esClient: IScopedClusterClient; soClient: SavedObjectsClientContract; + encryptedSavedObjects: SavedObjectsClientContract; agentPolicyService: AgentPolicyServiceInterface; agentService: AgentService; packagePolicyService: PackagePolicyClient; diff --git a/x-pack/plugins/dataset_quality/common/api_types.ts b/x-pack/plugins/dataset_quality/common/api_types.ts index 4f9936d65e014..70a5c3e597148 100644 --- a/x-pack/plugins/dataset_quality/common/api_types.ts +++ b/x-pack/plugins/dataset_quality/common/api_types.ts @@ -7,7 +7,7 @@ import * as rt from 'io-ts'; -export const datasetStatRt = rt.intersection([ +export const dataStreamStatRt = rt.intersection([ rt.type({ name: rt.string, }), @@ -19,6 +19,8 @@ export const datasetStatRt = rt.intersection([ }), ]); +export type DataStreamStat = rt.TypeOf; + export const integrationIconRt = rt.intersection([ rt.type({ path: rt.string, @@ -39,9 +41,12 @@ export const integrationRt = rt.intersection([ title: rt.string, version: rt.string, icons: rt.array(integrationIconRt), + datasets: rt.record(rt.string, rt.string), }), ]); +export type Integration = rt.TypeOf; + export const degradedDocsRt = rt.type({ dataset: rt.string, percentage: rt.number, @@ -52,7 +57,7 @@ export type DegradedDocs = rt.TypeOf; export const getDataStreamsStatsResponseRt = rt.exact( rt.intersection([ rt.type({ - dataStreamsStats: rt.array(datasetStatRt), + dataStreamsStats: rt.array(dataStreamStatRt), }), rt.type({ integrations: rt.array(integrationRt), diff --git a/x-pack/plugins/dataset_quality/common/data_streams_stats/data_stream_stat.ts b/x-pack/plugins/dataset_quality/common/data_streams_stats/data_stream_stat.ts index 2456284eb3cdc..88b6ed8d5c68b 100644 --- a/x-pack/plugins/dataset_quality/common/data_streams_stats/data_stream_stat.ts +++ b/x-pack/plugins/dataset_quality/common/data_streams_stats/data_stream_stat.ts @@ -10,16 +10,18 @@ import { DataStreamStatType, IntegrationType } from './types'; export class DataStreamStat { name: DataStreamStatType['name']; + namespace: string; title: string; size?: DataStreamStatType['size']; - sizeBytes?: DataStreamStatType['size_bytes']; - lastActivity?: DataStreamStatType['last_activity']; + sizeBytes?: DataStreamStatType['sizeBytes']; + lastActivity?: DataStreamStatType['lastActivity']; integration?: IntegrationType; degradedDocs?: number; private constructor(dataStreamStat: DataStreamStat) { this.name = dataStreamStat.name; this.title = dataStreamStat.title ?? dataStreamStat.name; + this.namespace = dataStreamStat.namespace; this.size = dataStreamStat.size; this.sizeBytes = dataStreamStat.sizeBytes; this.lastActivity = dataStreamStat.lastActivity; @@ -32,10 +34,11 @@ export class DataStreamStat { const dataStreamStatProps = { name: dataStreamStat.name, - title: `${dataset}-${namespace}`, + title: dataStreamStat.integration?.datasets?.[dataset] ?? dataset, + namespace, size: dataStreamStat.size, - sizeBytes: dataStreamStat.size_bytes, - lastActivity: dataStreamStat.last_activity, + sizeBytes: dataStreamStat.sizeBytes, + lastActivity: dataStreamStat.lastActivity, integration: dataStreamStat.integration ? Integration.create(dataStreamStat.integration) : undefined, diff --git a/x-pack/plugins/dataset_quality/public/components/dataset_quality/columns.tsx b/x-pack/plugins/dataset_quality/public/components/dataset_quality/columns.tsx index 94223e10f316f..0bdfcf0d28770 100644 --- a/x-pack/plugins/dataset_quality/public/components/dataset_quality/columns.tsx +++ b/x-pack/plugins/dataset_quality/public/components/dataset_quality/columns.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { + EuiBadge, EuiBasicTableColumn, EuiCode, EuiFlexGroup, @@ -33,6 +34,10 @@ const nameColumnName = i18n.translate('xpack.datasetQuality.nameColumnName', { defaultMessage: 'Dataset Name', }); +const namespaceColumnName = i18n.translate('xpack.datasetQuality.namespaceColumnName', { + defaultMessage: 'Namespace', +}); + const sizeColumnName = i18n.translate('xpack.datasetQuality.sizeColumnName', { defaultMessage: 'Size', }); @@ -122,6 +127,14 @@ export const getDatasetQualitTableColumns = ({ ); }, }, + { + name: namespaceColumnName, + field: 'namespace', + sortable: true, + render: (_, dataStreamStat: DataStreamStat) => ( + {dataStreamStat.namespace} + ), + }, { name: sizeColumnName, field: 'size', diff --git a/x-pack/plugins/dataset_quality/server/routes/data_streams/get_data_streams/index.ts b/x-pack/plugins/dataset_quality/server/routes/data_streams/get_data_streams/index.ts index 6154e3bc11a24..b79b4eeec0116 100644 --- a/x-pack/plugins/dataset_quality/server/routes/data_streams/get_data_streams/index.ts +++ b/x-pack/plugins/dataset_quality/server/routes/data_streams/get_data_streams/index.ts @@ -6,8 +6,8 @@ */ import type { ElasticsearchClient } from '@kbn/core/server'; +import { DataStreamTypes } from '../../../types/default_api_types'; import { dataStreamService } from '../../../services'; -import { DataStreamTypes } from '../../../types/data_stream'; export async function getDataStreams(options: { esClient: ElasticsearchClient; diff --git a/x-pack/plugins/dataset_quality/server/routes/data_streams/get_data_streams_stats/get_data_streams_stats.test.ts b/x-pack/plugins/dataset_quality/server/routes/data_streams/get_data_streams_stats/get_data_streams_stats.test.ts index 830c3b162573f..a0104afc1d5fe 100644 --- a/x-pack/plugins/dataset_quality/server/routes/data_streams/get_data_streams_stats/get_data_streams_stats.test.ts +++ b/x-pack/plugins/dataset_quality/server/routes/data_streams/get_data_streams_stats/get_data_streams_stats.test.ts @@ -79,32 +79,32 @@ describe('getDataStreams', () => { { name: 'logs-elastic_agent-default', size: '1gb', - size_bytes: 1170805528, - last_activity: 1698916071000, + sizeBytes: 1170805528, + lastActivity: 1698916071000, }, { name: 'logs-elastic_agent.filebeat-default', size: '1.3mb', - size_bytes: 1459100, - last_activity: 1698902209996, + sizeBytes: 1459100, + lastActivity: 1698902209996, }, { name: 'logs-elastic_agent.fleet_server-default', size: '2.9mb', - size_bytes: 3052148, - last_activity: 1698914110010, + sizeBytes: 3052148, + lastActivity: 1698914110010, }, { name: 'logs-elastic_agent.metricbeat-default', size: '1.6mb', - size_bytes: 1704807, - last_activity: 1698672046707, + sizeBytes: 1704807, + lastActivity: 1698672046707, }, { name: 'logs-test.test-default', size: '6.2mb', - size_bytes: 6570447, - last_activity: 1698913802000, + sizeBytes: 6570447, + lastActivity: 1698913802000, }, ]); }); diff --git a/x-pack/plugins/dataset_quality/server/routes/data_streams/get_data_streams_stats/index.ts b/x-pack/plugins/dataset_quality/server/routes/data_streams/get_data_streams_stats/index.ts index 9ec252d096357..a78f2fec53e29 100644 --- a/x-pack/plugins/dataset_quality/server/routes/data_streams/get_data_streams_stats/index.ts +++ b/x-pack/plugins/dataset_quality/server/routes/data_streams/get_data_streams_stats/index.ts @@ -6,8 +6,8 @@ */ import type { ElasticsearchClient } from '@kbn/core/server'; +import { DataStreamTypes } from '../../../types/default_api_types'; import { dataStreamService } from '../../../services'; -import { DataStreamTypes } from '../../../types/data_stream'; export async function getDataStreamsStats(options: { esClient: ElasticsearchClient; @@ -24,9 +24,9 @@ export async function getDataStreamsStats(options: { const mappedDataStreams = matchingDataStreamsStats.map((dataStream) => { return { name: dataStream.data_stream, - size: dataStream.store_size, - size_bytes: dataStream.store_size_bytes, - last_activity: dataStream.maximum_timestamp, + size: dataStream.store_size?.toString(), + sizeBytes: dataStream.store_size_bytes, + lastActivity: dataStream.maximum_timestamp, }; }); diff --git a/x-pack/plugins/dataset_quality/server/routes/data_streams/get_degraded_docs.ts b/x-pack/plugins/dataset_quality/server/routes/data_streams/get_degraded_docs.ts index 4bbe01d219322..1af5a603f3638 100644 --- a/x-pack/plugins/dataset_quality/server/routes/data_streams/get_degraded_docs.ts +++ b/x-pack/plugins/dataset_quality/server/routes/data_streams/get_degraded_docs.ts @@ -14,7 +14,7 @@ import { DATA_STREAM_TYPE, _IGNORED, } from '../../../common/es_fields'; -import { DataStreamTypes } from '../../types/data_stream'; +import { DataStreamTypes } from '../../types/default_api_types'; import { createDatasetQualityESClient, wildcardQuery } from '../../utils'; export async function getDegradedDocsPaginated(options: { diff --git a/x-pack/plugins/dataset_quality/server/routes/data_streams/get_integrations.ts b/x-pack/plugins/dataset_quality/server/routes/data_streams/get_integrations.ts new file mode 100644 index 0000000000000..7871571b607b6 --- /dev/null +++ b/x-pack/plugins/dataset_quality/server/routes/data_streams/get_integrations.ts @@ -0,0 +1,53 @@ +/* + * 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 { PackageClient } from '@kbn/fleet-plugin/server'; +import { DataStreamStat, Integration } from '../../../common/api_types'; + +export async function getIntegrations(options: { + packageClient: PackageClient; + dataStreams: DataStreamStat[]; +}): Promise { + const { packageClient, dataStreams } = options; + + const packages = await packageClient.getPackages(); + const installedPackages = dataStreams.map((item) => item.integration); + + return Promise.all( + packages + .filter((pkg) => installedPackages.includes(pkg.name)) + .map(async (p) => ({ + name: p.name, + title: p.title, + version: p.version, + icons: p.icons, + datasets: await getDatasets({ + packageClient, + name: p.name, + version: p.version, + }), + })) + ); +} + +const getDatasets = async (options: { + packageClient: PackageClient; + name: string; + version: string; +}) => { + const { packageClient, name, version } = options; + + const pkg = await packageClient.getPackage(name, version); + + return pkg.packageInfo.data_streams?.reduce( + (acc, curr) => ({ + ...acc, + [curr.dataset]: curr.title, + }), + {} + ); +}; diff --git a/x-pack/plugins/dataset_quality/server/routes/data_streams/routes.ts b/x-pack/plugins/dataset_quality/server/routes/data_streams/routes.ts index 7be8c102060d6..4f95331d97651 100644 --- a/x-pack/plugins/dataset_quality/server/routes/data_streams/routes.ts +++ b/x-pack/plugins/dataset_quality/server/routes/data_streams/routes.ts @@ -7,20 +7,19 @@ import * as t from 'io-ts'; import { keyBy, merge, values } from 'lodash'; -import { DataStreamStat } from '../../types/data_stream'; -import { dataStreamTypesRt, rangeRt } from '../../types/default_api_types'; -import { Integration } from '../../types/integration'; +import { typeRt, rangeRt } from '../../types/default_api_types'; import { createDatasetQualityServerRoute } from '../create_datasets_quality_server_route'; import { getDataStreams } from './get_data_streams'; import { getDataStreamsStats } from './get_data_streams_stats'; import { getDegradedDocsPaginated } from './get_degraded_docs'; -import { DegradedDocs } from '../../../common/api_types'; +import { DegradedDocs, DataStreamStat, Integration } from '../../../common/api_types'; +import { getIntegrations } from './get_integrations'; const statsRoute = createDatasetQualityServerRoute({ endpoint: 'GET /internal/dataset_quality/data_streams/stats', params: t.type({ query: t.intersection([ - dataStreamTypesRt, + typeRt, t.partial({ datasetQuery: t.string, }), @@ -41,7 +40,6 @@ const statsRoute = createDatasetQualityServerRoute({ const fleetPluginStart = await plugins.fleet.start(); const packageClient = fleetPluginStart.packageService.asInternalUser; - const packages = await packageClient.getPackages(); const [dataStreams, dataStreamsStats] = await Promise.all([ getDataStreams({ @@ -52,22 +50,11 @@ const statsRoute = createDatasetQualityServerRoute({ getDataStreamsStats({ esClient, ...params.query }), ]); - const installedPackages = dataStreams.items.map((item) => item.integration); - - const integrations = packages - .filter((pkg) => installedPackages.includes(pkg.name)) - .map((p) => ({ - name: p.name, - title: p.title, - version: p.version, - icons: p.icons, - })); - return { dataStreamsStats: values( merge(keyBy(dataStreams.items, 'name'), keyBy(dataStreamsStats.items, 'name')) ), - integrations, + integrations: await getIntegrations({ packageClient, dataStreams: dataStreams.items }), }; }, }); @@ -77,7 +64,7 @@ const degradedDocsRoute = createDatasetQualityServerRoute({ params: t.type({ query: t.intersection([ rangeRt, - dataStreamTypesRt, + typeRt, t.partial({ datasetQuery: t.string, }), diff --git a/x-pack/plugins/dataset_quality/server/types/data_stream.ts b/x-pack/plugins/dataset_quality/server/types/data_stream.ts deleted file mode 100644 index 1ccac8199a8b6..0000000000000 --- a/x-pack/plugins/dataset_quality/server/types/data_stream.ts +++ /dev/null @@ -1,18 +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 { ByteSize } from '@elastic/elasticsearch/lib/api/types'; -import { Integration } from './integration'; -export interface DataStreamStat { - name: string; - size?: ByteSize; - size_bytes?: number; - last_activity?: number; - integration?: Integration; -} - -export type DataStreamTypes = 'logs' | 'metrics' | 'traces' | 'synthetics' | 'profiling'; diff --git a/x-pack/plugins/dataset_quality/server/types/default_api_types.ts b/x-pack/plugins/dataset_quality/server/types/default_api_types.ts index e36bb1e58f65a..6148832dad140 100644 --- a/x-pack/plugins/dataset_quality/server/types/default_api_types.ts +++ b/x-pack/plugins/dataset_quality/server/types/default_api_types.ts @@ -8,16 +8,21 @@ import * as t from 'io-ts'; import { isoToEpochRt } from '@kbn/io-ts-utils'; -export const dataStreamTypesRt = t.partial({ - type: t.union([ - t.literal('logs'), - t.literal('metrics'), - t.literal('traces'), - t.literal('synthetics'), - t.literal('profiling'), - ]), +// https://github.com/gcanti/io-ts/blob/master/index.md#union-of-string-literals +export const dataStreamTypesRt = t.keyof({ + logs: null, + metrics: null, + traces: null, + synthetics: null, + profiling: null, }); +export const typeRt = t.partial({ + type: dataStreamTypesRt, +}); + +export type DataStreamTypes = t.TypeOf; + export const rangeRt = t.type({ start: isoToEpochRt, end: isoToEpochRt, diff --git a/x-pack/plugins/dataset_quality/server/types/integration.ts b/x-pack/plugins/dataset_quality/server/types/integration.ts deleted file mode 100644 index 2595a120c8b70..0000000000000 --- a/x-pack/plugins/dataset_quality/server/types/integration.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. - */ - -export interface Integration { - name: string; - title?: string; - version?: string; - icons?: IntegrationIcon[]; -} - -export interface IntegrationIcon { - path: string; - src: string; - title?: string; - size?: string; - type?: string; -} diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/connectors.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/connectors.tsx index 6fb1e2b873eb9..b5b87aa7e2bc3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/connectors.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/connectors.tsx @@ -116,7 +116,7 @@ export const Connectors: React.FC = ({ isCrawler }) => { > {i18n.translate( 'xpack.enterpriseSearch.connectors.newConnectorsClientButtonLabel', - { defaultMessage: 'New Connectors Client' } + { defaultMessage: 'New Connector Client' } )} , ] diff --git a/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/embeddable/embeddable.test.tsx b/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/embeddable/embeddable.test.tsx index 5f451e6e7c86b..e8f6ee48dc206 100644 --- a/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/embeddable/embeddable.test.tsx +++ b/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/embeddable/embeddable.test.tsx @@ -11,12 +11,14 @@ import { DataViewState } from '../hooks/use_app_data_view'; import { render } from '../rtl_helpers'; import { AddToCaseAction } from '../header/add_to_case_action'; import { ActionTypes } from './use_actions'; +import * as lensHook from './use_embeddable_attributes'; jest.mock('../header/add_to_case_action', () => ({ AddToCaseAction: jest.fn(() =>
mockAddToCaseAction
), })); const mockLensAttrs = { + title: '', hidePanelTitles: true, description: '', visualizationType: 'lnsMetric', @@ -102,17 +104,19 @@ describe('Embeddable', () => { jest.clearAllMocks(); }); + jest.spyOn(lensHook, 'useEmbeddableAttributes').mockReturnValue(mockLensAttrs as any); + it('renders title', async () => { const { container, getByText } = render( ); expect(container.querySelector(`[data-test-subj="exploratoryView-title"]`)).toBeInTheDocument(); @@ -123,12 +127,12 @@ describe('Embeddable', () => { const { container } = render( ); expect( @@ -140,12 +144,12 @@ describe('Embeddable', () => { const { container } = render( ); @@ -174,13 +178,13 @@ describe('Embeddable', () => { render( ); diff --git a/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/embeddable/embeddable.tsx b/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/embeddable/embeddable.tsx index c3405225b2e01..ed06d786124f3 100644 --- a/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/embeddable/embeddable.tsx +++ b/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/embeddable/embeddable.tsx @@ -4,6 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { i18n } from '@kbn/i18n'; import { Position } from '@elastic/charts'; import React, { useState } from 'react'; @@ -17,28 +18,22 @@ import { import { ViewMode } from '@kbn/embeddable-plugin/common'; import { observabilityFeatureId } from '@kbn/observability-shared-plugin/public'; import styled from 'styled-components'; -import { useTheme, useKibanaSpace } from '@kbn/observability-shared-plugin/public'; -import { HeatMapLensAttributes } from '../configurations/lens_attributes/heatmap_attributes'; -import { SingleMetricLensAttributes } from '../configurations/lens_attributes/single_metric_attributes'; -import { AllSeries, ReportTypes } from '../../../..'; -import { LayerConfig, LensAttributes } from '../configurations/lens_attributes'; +import { AllSeries } from '../../../..'; import { AppDataType, ReportViewType } from '../types'; -import { getLayerConfigs } from '../hooks/use_lens_attributes'; import { OperationTypeComponent } from '../series_editor/columns/operation_type_select'; import { DataViewState } from '../hooks/use_app_data_view'; import { ReportConfigMap } from '../contexts/exploratory_view_config'; -import { obsvReportConfigMap } from '../obsv_exploratory_view'; import { ActionTypes, useActions } from './use_actions'; import { AddToCaseAction } from '../header/add_to_case_action'; +import { useEmbeddableAttributes } from './use_embeddable_attributes'; export interface ExploratoryEmbeddableProps { id?: string; appendTitle?: JSX.Element; - attributes?: AllSeries; + attributes: AllSeries; axisTitlesVisibility?: XYState['axisTitlesVisibilitySettings']; gridlinesVisibilitySettings?: XYState['gridlinesVisibilitySettings']; customHeight?: string; - customLensAttrs?: any; // Takes LensAttributes customTimeRange?: { from: string; to: string }; // required if rendered with LensAttributes dataTypesIndexPatterns?: Partial>; isSingleMetric?: boolean; @@ -69,77 +64,42 @@ export interface ExploratoryEmbeddableComponentProps extends ExploratoryEmbeddab } // eslint-disable-next-line import/no-default-export -export default function Embeddable({ - appendTitle, - attributes = [], - axisTitlesVisibility, - gridlinesVisibilitySettings, - customHeight, - customLensAttrs, - customTimeRange, - dataViewState, - legendIsVisible, - legendPosition, - lens, - onBrushEnd, - caseOwner = observabilityFeatureId, - reportConfigMap = {}, - reportType, - showCalculationMethod = false, - title, - withActions = true, - lensFormulaHelper, - hideTicks, - align, - noLabel, - fontSize = 27, - lineHeight = 32, - searchSessionId, - onLoad, -}: ExploratoryEmbeddableComponentProps) { +export default function Embeddable(props: ExploratoryEmbeddableComponentProps) { + const { + appendTitle, + attributes = [], + axisTitlesVisibility, + gridlinesVisibilitySettings, + customHeight, + customTimeRange, + legendIsVisible, + legendPosition, + lens, + onBrushEnd, + caseOwner = observabilityFeatureId, + reportType, + showCalculationMethod = false, + title, + withActions = true, + hideTicks, + align, + noLabel, + fontSize = 27, + lineHeight = 32, + searchSessionId, + onLoad, + } = props; const LensComponent = lens?.EmbeddableComponent; const LensSaveModalComponent = lens?.SaveModalComponent; const [isSaveOpen, setIsSaveOpen] = useState(false); const [isAddToCaseOpen, setAddToCaseOpen] = useState(false); - const spaceId = useKibanaSpace(); - const series = Object.entries(attributes)[0]?.[1]; const [operationType, setOperationType] = useState(series?.operationType); - const theme = useTheme(); - - const layerConfigs: LayerConfig[] = getLayerConfigs( - attributes, - reportType, - theme, - dataViewState, - { ...reportConfigMap, ...obsvReportConfigMap }, - spaceId.space?.id - ); - let lensAttributes; - let attributesJSON = customLensAttrs; - if (!customLensAttrs) { - try { - if (reportType === ReportTypes.SINGLE_METRIC) { - lensAttributes = new SingleMetricLensAttributes( - layerConfigs, - reportType, - lensFormulaHelper! - ); - attributesJSON = lensAttributes?.getJSON('lnsLegacyMetric'); - } else if (reportType === ReportTypes.HEATMAP) { - lensAttributes = new HeatMapLensAttributes(layerConfigs, reportType, lensFormulaHelper!); - attributesJSON = lensAttributes?.getJSON('lnsHeatmap'); - } else { - lensAttributes = new LensAttributes(layerConfigs, reportType, lensFormulaHelper); - attributesJSON = lensAttributes?.getJSON(); - } - // eslint-disable-next-line no-empty - } catch (error) {} - } + const attributesJSON = useEmbeddableAttributes(props); const timeRange = customTimeRange ?? series?.time; @@ -182,17 +142,20 @@ export default function Embeddable({ }; } - if (!attributesJSON && layerConfigs.length < 1) { + if (!attributesJSON) { return null; } if (!LensComponent) { - return No lens component; + return ( + + {i18n.translate('xpack.exploratoryView.embeddable.noLensComponentTextLabel', { + defaultMessage: 'No lens component', + })} + + ); } - attributesJSON.state.searchSessionId = searchSessionId; - attributesJSON.searchSessionId = searchSessionId; - return ( { + const spaceId = useKibanaSpace(); + const theme = useTheme(); + + return useMemo(() => { + try { + const layerConfigs: LayerConfig[] = getLayerConfigs( + attributes, + reportType, + theme, + dataViewState, + { ...reportConfigMap, ...obsvReportConfigMap }, + spaceId.space?.id + ); + + if (reportType === ReportTypes.SINGLE_METRIC) { + const lensAttributes = new SingleMetricLensAttributes( + layerConfigs, + reportType, + lensFormulaHelper! + ); + return lensAttributes?.getJSON('lnsLegacyMetric'); + } else if (reportType === ReportTypes.HEATMAP) { + const lensAttributes = new HeatMapLensAttributes( + layerConfigs, + reportType, + lensFormulaHelper! + ); + return lensAttributes?.getJSON('lnsHeatmap'); + } else { + const lensAttributes = new LensAttributes(layerConfigs, reportType, lensFormulaHelper); + return lensAttributes?.getJSON(); + } + } catch (error) { + console.error(error); + } + }, [ + attributes, + dataViewState, + lensFormulaHelper, + reportConfigMap, + reportType, + spaceId.space?.id, + theme, + ]); +}; diff --git a/x-pack/plugins/fleet/common/services/output_helpers.ts b/x-pack/plugins/fleet/common/services/output_helpers.ts index 26d97f42c39b4..988be3bc277f7 100644 --- a/x-pack/plugins/fleet/common/services/output_helpers.ts +++ b/x-pack/plugins/fleet/common/services/output_helpers.ts @@ -28,9 +28,9 @@ export function getAllowedOutputTypeForPolicy(agentPolicy: AgentPolicy) { agentPolicy.package_policies && agentPolicy.package_policies.some( (p) => - p.package?.name === FLEET_APM_PACKAGE || p.package?.name === FLEET_SERVER_PACKAGE || - p.package?.name === FLEET_SYNTHETICS_PACKAGE + p.package?.name === FLEET_SYNTHETICS_PACKAGE || + p.package?.name === FLEET_APM_PACKAGE ); if (isRestrictedToSameClusterES) { diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/hooks.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/hooks.test.tsx index e027317bf621a..8c2983ee9fd83 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/hooks.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/hooks.test.tsx @@ -533,7 +533,7 @@ describe('useOutputOptions', () => { `); }); - it('should only enable remote es output for monitoring output', async () => { + it('should enable remote es output for data and monitoring output', async () => { const testRenderer = createFleetTestRendererMock(); mockedUseLicence.mockReturnValue({ hasAtLeast: () => true, @@ -545,7 +545,8 @@ describe('useOutputOptions', () => { expect(result.current.isLoading).toBeTruthy(); await waitForNextUpdate(); - expect(result.current.dataOutputOptions.length).toEqual(1); + expect(result.current.dataOutputOptions.length).toEqual(2); + expect(result.current.dataOutputOptions[1].value).toEqual('remote1'); expect(result.current.monitoringOutputOptions.length).toEqual(2); expect(result.current.monitoringOutputOptions[1].value).toEqual('remote1'); }); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/hooks.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/hooks.tsx index 9ee8b1f225735..15681ea1dfbb6 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/hooks.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/hooks.tsx @@ -16,7 +16,7 @@ import { useGetDownloadSources, useGetFleetServerHosts, } from '../../../../hooks'; -import { LICENCE_FOR_PER_POLICY_OUTPUT, outputType } from '../../../../../../../common/constants'; +import { LICENCE_FOR_PER_POLICY_OUTPUT } from '../../../../../../../common/constants'; import { getAllowedOutputTypeForPolicy, policyHasFleetServer, @@ -99,28 +99,26 @@ export function useOutputOptions(agentPolicy: Partial item.type !== outputType.RemoteElasticsearch) - .map((item) => { - const isOutputTypeUnsupported = !allowedOutputTypes.includes(item.type); + ...outputsRequest.data.items.map((item) => { + const isOutputTypeUnsupported = !allowedOutputTypes.includes(item.type); - return { - value: item.id, - inputDisplay: getOutputLabel( - item.name, - isOutputTypeUnsupported ? ( - - ) : undefined - ), - disabled: !isPolicyPerOutputAllowed || isOutputTypeUnsupported, - }; - }), + return { + value: item.id, + inputDisplay: getOutputLabel( + item.name, + isOutputTypeUnsupported ? ( + + ) : undefined + ), + disabled: !isPolicyPerOutputAllowed || isOutputTypeUnsupported, + }; + }), ]; }, [outputsRequest, isPolicyPerOutputAllowed, allowedOutputTypes]); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/index.tsx index 511af850cab46..daebac615d6eb 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/index.tsx @@ -542,7 +542,6 @@ export const EditOutputFlyout: React.FunctionComponent = }} /> } - disabled={isRemoteESOutput} /> diff --git a/x-pack/plugins/fleet/server/collectors/agent_collectors.ts b/x-pack/plugins/fleet/server/collectors/agent_collectors.ts index 09a4986e0e6f0..391114aa992b3 100644 --- a/x-pack/plugins/fleet/server/collectors/agent_collectors.ts +++ b/x-pack/plugins/fleet/server/collectors/agent_collectors.ts @@ -75,6 +75,12 @@ export interface AgentData { version: string; count: number; }>; + upgrade_details: Array<{ + target_version: string; + state: string; + error_msg: string; + agent_count: number; + }>; } const DEFAULT_AGENT_DATA = { @@ -82,6 +88,7 @@ const DEFAULT_AGENT_DATA = { agents_per_policy: [], agents_per_version: [], agents_per_os: [], + upgrade_details: [], }; export const getAgentData = async ( @@ -135,6 +142,23 @@ export const getAgentData = async ( ], }, }, + upgrade_details: { + multi_terms: { + size: 1000, + terms: [ + { + field: 'upgrade_details.target_version.keyword', + }, + { + field: 'upgrade_details.state', + }, + { + field: 'upgrade_details.metadata.error_msg.keyword', + missing: '', + }, + ], + }, + }, }, }, { signal: abortController.signal } @@ -190,11 +214,21 @@ export const getAgentData = async ( count: bucket.doc_count, })); + const upgradeDetails = ((response?.aggregations?.upgrade_details as any).buckets ?? []).map( + (bucket: any) => ({ + target_version: bucket.key[0], + state: bucket.key[1], + error_msg: bucket.key[2], + agent_count: bucket.doc_count, + }) + ); + return { agent_checkin_status: statuses, agents_per_policy: agentsPerPolicy, agents_per_version: agentsPerVersion, agents_per_os: agentsPerOS, + upgrade_details: upgradeDetails, }; } catch (error) { if (error.statusCode === 404) { diff --git a/x-pack/plugins/fleet/server/collectors/agents_per_output.ts b/x-pack/plugins/fleet/server/collectors/agents_per_output.ts index b7ed480fc61fc..5090b1530bc02 100644 --- a/x-pack/plugins/fleet/server/collectors/agents_per_output.ts +++ b/x-pack/plugins/fleet/server/collectors/agents_per_output.ts @@ -83,7 +83,9 @@ export async function getAgentsPerOutput( if (!outputTypeSupportPresets(output.type)) { return; } - + if (!outputTypes[output.type]) { + return; + } const outputTelemetryRecord = outputTypes[output.type]; if (!outputTelemetryRecord.preset_counts) { diff --git a/x-pack/plugins/fleet/server/integration_tests/fleet_usage_telemetry.test.ts b/x-pack/plugins/fleet/server/integration_tests/fleet_usage_telemetry.test.ts index 19eb7aa750658..430dc6f745ad9 100644 --- a/x-pack/plugins/fleet/server/integration_tests/fleet_usage_telemetry.test.ts +++ b/x-pack/plugins/fleet/server/integration_tests/fleet_usage_telemetry.test.ts @@ -146,6 +146,13 @@ describe('fleet usage telemetry', () => { status: 'HEALTHY', }, ], + upgrade_details: { + target_version: '8.12.0', + state: 'UPG_FAILED', + metadata: { + error_msg: 'Download failed', + }, + }, }, { create: { @@ -176,6 +183,13 @@ describe('fleet usage telemetry', () => { status: 'HEALTHY', }, ], + upgrade_details: { + target_version: '8.12.0', + state: 'UPG_FAILED', + metadata: { + error_msg: 'Agent crash detected', + }, + }, }, { create: { @@ -220,6 +234,11 @@ describe('fleet usage telemetry', () => { last_checkin: new Date(Date.now() - 1000 * 60 * 6).toISOString(), active: true, policy_id: 'policy2', + upgrade_details: { + target_version: '8.11.0', + state: 'UPG_ROLLBACK', + metadata: {}, + }, }, { create: { @@ -557,5 +576,24 @@ describe('fleet usage telemetry', () => { fleet_server_logs_top_errors: ['failed to unenroll offline agents'], }) ); + expect(usage?.upgrade_details.length).toBe(3); + expect(usage?.upgrade_details).toContainEqual({ + target_version: '8.12.0', + state: 'UPG_FAILED', + error_msg: 'Download failed', + agent_count: 1, + }); + expect(usage?.upgrade_details).toContainEqual({ + target_version: '8.12.0', + state: 'UPG_FAILED', + error_msg: 'Agent crash detected', + agent_count: 1, + }); + expect(usage?.upgrade_details).toContainEqual({ + target_version: '8.11.0', + state: 'UPG_ROLLBACK', + error_msg: '', + agent_count: 1, + }); }); }); diff --git a/x-pack/plugins/fleet/server/services/output.test.ts b/x-pack/plugins/fleet/server/services/output.test.ts index c17cf9d3af210..8c2fee196d8d8 100644 --- a/x-pack/plugins/fleet/server/services/output.test.ts +++ b/x-pack/plugins/fleet/server/services/output.test.ts @@ -705,12 +705,12 @@ describe('Output Service', () => { ); }); - it('should throw when a remote es output is attempted to be created as default data output', async () => { + it('should not throw when a remote es output is attempted to be created as default data output', async () => { const soClient = getMockedSoClient({ defaultOutputId: 'output-test', }); - await expect( + expect( outputService.create( soClient, esClientMock, @@ -722,9 +722,7 @@ describe('Output Service', () => { }, { id: 'output-1' } ) - ).rejects.toThrow( - `Remote elasticsearch output cannot be set as default output for integration data. Please set "is_default" to false.` - ); + ).resolves.not.toThrow(); }); it('should set preset: balanced by default when creating a new ES output', async () => { @@ -1644,21 +1642,19 @@ describe('Output Service', () => { ); }); - it('should throw when a remote es output is attempted to be updated as default data output', async () => { + it('should not throw when a remote es output is attempted to be updated as default data output', async () => { const soClient = getMockedSoClient({ defaultOutputId: 'output-test', }); - await expect( + expect( outputService.update(soClient, esClientMock, 'output-test', { is_default: true, is_default_monitoring: false, name: 'Test', type: 'remote_elasticsearch', }) - ).rejects.toThrow( - `Remote elasticsearch output cannot be set as default output for integration data. Please set "is_default" to false.` - ); + ).resolves.not.toThrow(); }); }); diff --git a/x-pack/plugins/fleet/server/services/output.ts b/x-pack/plugins/fleet/server/services/output.ts index 7b838d9b9b0a5..e73a070a54965 100644 --- a/x-pack/plugins/fleet/server/services/output.ts +++ b/x-pack/plugins/fleet/server/services/output.ts @@ -441,13 +441,6 @@ class OutputService { logger.debug(`Creating new output`); const data: OutputSOAttributes = { ...omit(output, ['ssl', 'secrets']) }; - if (output.type === outputType.RemoteElasticsearch) { - if (data.is_default) { - throw new OutputInvalidError( - 'Remote elasticsearch output cannot be set as default output for integration data. Please set "is_default" to false.' - ); - } - } if (outputTypeSupportPresets(data.type)) { if ( @@ -767,14 +760,6 @@ class OutputService { const logger = appContextService.getLogger(); logger.debug(`Updating output ${id}`); - if (data.type === outputType.RemoteElasticsearch) { - if (data.is_default) { - throw new OutputInvalidError( - 'Remote elasticsearch output cannot be set as default output for integration data. Please set "is_default" to false.' - ); - } - } - let secretsToDelete: PolicySecretReference[] = []; const originalOutput = await this.get(soClient, id); diff --git a/x-pack/plugins/fleet/server/services/telemetry/fleet_usage_sender.ts b/x-pack/plugins/fleet/server/services/telemetry/fleet_usage_sender.ts index 4ee0a7dac4f89..1b90fe6d01e0b 100644 --- a/x-pack/plugins/fleet/server/services/telemetry/fleet_usage_sender.ts +++ b/x-pack/plugins/fleet/server/services/telemetry/fleet_usage_sender.ts @@ -24,7 +24,7 @@ const FLEET_AGENTS_EVENT_TYPE = 'fleet_agents'; export class FleetUsageSender { private taskManager?: TaskManagerStartContract; - private taskVersion = '1.1.3'; + private taskVersion = '1.1.4'; private taskType = 'Fleet-Usage-Sender'; private wasStarted: boolean = false; private interval = '1h'; @@ -83,6 +83,7 @@ export class FleetUsageSender { const { agents_per_version: agentsPerVersion, agents_per_output_type: agentsPerOutputType, + upgrade_details: upgradeDetails, ...fleetUsageData } = usageData; appContextService @@ -106,6 +107,13 @@ export class FleetUsageSender { agents_per_output_type: byOutputType, }); }); + + appContextService + .getLogger() + .debug('Agents upgrade details telemetry: ' + JSON.stringify(upgradeDetails)); + upgradeDetails.forEach((upgradeDetailsObj) => { + core.analytics.reportEvent(FLEET_AGENTS_EVENT_TYPE, { upgrade_details: upgradeDetailsObj }); + }); } catch (error) { appContextService .getLogger() diff --git a/x-pack/plugins/fleet/server/services/telemetry/fleet_usages_schema.ts b/x-pack/plugins/fleet/server/services/telemetry/fleet_usages_schema.ts index fa7d2d769b900..6bfbf1c794b52 100644 --- a/x-pack/plugins/fleet/server/services/telemetry/fleet_usages_schema.ts +++ b/x-pack/plugins/fleet/server/services/telemetry/fleet_usages_schema.ts @@ -76,9 +76,10 @@ export const fleetAgentsSchema: RootSchema = { description: 'Output type used by agent', }, }, - presets_counts: { + preset_counts: { _meta: { description: 'Count of agents per preset', + optional: true, }, properties: { balanced: { @@ -117,6 +118,7 @@ export const fleetAgentsSchema: RootSchema = { type: 'keyword', _meta: { description: 'Output preset used by agent, if applicable', + optional: true, }, }, count_as_data: { @@ -133,6 +135,38 @@ export const fleetAgentsSchema: RootSchema = { }, }, }, + upgrade_details: { + _meta: { + description: 'Agent upgrade details telemetry', + optional: true, + }, + properties: { + target_version: { + type: 'keyword', + _meta: { + description: 'Target version of the agent upgrade', + }, + }, + state: { + type: 'keyword', + _meta: { + description: 'State of the agent upgrade', + }, + }, + error_msg: { + type: 'keyword', + _meta: { + description: 'Error message of the agent upgrade if failed', + }, + }, + agent_count: { + type: 'long', + _meta: { + description: 'How many agents have this upgrade details', + }, + }, + }, + }, }; export const fleetUsagesSchema: RootSchema = { diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_host_count.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_host_count.ts index d261b4ec2e45d..134e651ed7a64 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_host_count.ts +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_host_count.ts @@ -7,10 +7,11 @@ import * as rt from 'io-ts'; import { ES_SEARCH_STRATEGY, IKibanaSearchResponse } from '@kbn/data-plugin/common'; -import { useCallback, useEffect } from 'react'; -import { catchError, map, Observable, of, startWith } from 'rxjs'; +import { useCallback, useEffect, useMemo } from 'react'; +import { catchError, map, Observable, of, startWith, tap } from 'rxjs'; import createContainer from 'constate'; import type { QueryDslQueryContainer, SearchResponse } from '@elastic/elasticsearch/lib/api/types'; +import type { ITelemetryClient } from '../../../../services/telemetry'; import { useKibanaContextForPlugin } from '../../../../hooks/use_kibana'; import { decodeOrThrow } from '../../../../../common/runtime_types'; import { useDataSearch, useLatestPartialDataSearchResponse } from '../../../../utils/data_search'; @@ -79,7 +80,23 @@ export const useHostCount = () => { searchCriteria.dateRange.from, searchCriteria.dateRange.to, ]), - parseResponses: normalizeDataSearchResponse, + parseResponses: useMemo( + () => + normalizeDataSearchResponse({ + telemetry, + telemetryData: { + withQuery: !!searchCriteria.query.query, + withFilters: + searchCriteria.filters.length > 0 || searchCriteria.panelFilters.length > 0, + }, + }), + [ + searchCriteria.filters.length, + searchCriteria.panelFilters.length, + searchCriteria.query.query, + telemetry, + ] + ), }); const { isRequestRunning, isResponsePartial, latestResponseData, latestResponseErrors } = @@ -89,14 +106,6 @@ export const useHostCount = () => { fetchHostCount(); }, [fetchHostCount]); - useEffect(() => { - if (latestResponseData) { - telemetry.reportHostsViewTotalHostCountRetrieved({ - total: latestResponseData.count.value, - }); - } - }, [latestResponseData, telemetry]); - return { errors: latestResponseErrors, isRequestRunning, @@ -116,27 +125,42 @@ const INITIAL_STATE = { loaded: 0, total: undefined, }; -const normalizeDataSearchResponse = ( - response$: Observable>>> -) => - response$.pipe( - map((response) => ({ - data: decodeOrThrow(HostCountResponseRT)(response.rawResponse.aggregations), - errors: [], - isPartial: response.isPartial ?? false, - isRunning: response.isRunning ?? false, - loaded: response.loaded, - total: response.total, - })), - startWith(INITIAL_STATE), - catchError((error) => - of({ - ...INITIAL_STATE, - errors: [error.message ?? error], - isRunning: false, - }) - ) - ); + +const normalizeDataSearchResponse = + ({ + telemetry, + telemetryData, + }: { + telemetry: ITelemetryClient; + telemetryData: { withQuery: boolean; withFilters: boolean }; + }) => + (response$: Observable>>>) => { + return response$.pipe( + map((response) => ({ + data: decodeOrThrow(HostCountResponseRT)(response.rawResponse.aggregations), + errors: [], + isPartial: response.isPartial ?? false, + isRunning: response.isRunning ?? false, + loaded: response.loaded, + total: response.total, + })), + tap(({ data }) => { + telemetry.reportHostsViewTotalHostCountRetrieved({ + total: data.count.value, + with_query: telemetryData.withQuery, + with_filters: telemetryData.withFilters, + }); + }), + startWith(INITIAL_STATE), + catchError((error) => + of({ + ...INITIAL_STATE, + errors: [error.message ?? error], + isRunning: false, + }) + ) + ); + }; const HostCountResponseRT = rt.type({ count: rt.type({ diff --git a/x-pack/plugins/infra/public/services/telemetry/telemetry_events.ts b/x-pack/plugins/infra/public/services/telemetry/telemetry_events.ts index 6ce2d2b827623..b83cbfe262e63 100644 --- a/x-pack/plugins/infra/public/services/telemetry/telemetry_events.ts +++ b/x-pack/plugins/infra/public/services/telemetry/telemetry_events.ts @@ -109,6 +109,20 @@ const hostViewTotalHostCountRetrieved: InfraTelemetryEvent = { optional: false, }, }, + with_query: { + type: 'boolean', + _meta: { + description: 'Has KQL query', + optional: false, + }, + }, + with_filters: { + type: 'boolean', + _meta: { + description: 'Has filters', + optional: false, + }, + }, }, }; diff --git a/x-pack/plugins/infra/public/services/telemetry/telemetry_service.test.ts b/x-pack/plugins/infra/public/services/telemetry/telemetry_service.test.ts index ac450df7dd162..3fa8a9b447111 100644 --- a/x-pack/plugins/infra/public/services/telemetry/telemetry_service.test.ts +++ b/x-pack/plugins/infra/public/services/telemetry/telemetry_service.test.ts @@ -172,6 +172,8 @@ describe('TelemetryService', () => { telemetry.reportHostsViewTotalHostCountRetrieved({ total: 300, + with_filters: true, + with_query: false, }); expect(setupParams.analytics.reportEvent).toHaveBeenCalledTimes(1); @@ -179,6 +181,8 @@ describe('TelemetryService', () => { InfraTelemetryEventTypes.HOST_VIEW_TOTAL_HOST_COUNT_RETRIEVED, { total: 300, + with_filters: true, + with_query: false, } ); }); diff --git a/x-pack/plugins/infra/public/services/telemetry/types.ts b/x-pack/plugins/infra/public/services/telemetry/types.ts index 3b1665078ee3a..769cc303def50 100644 --- a/x-pack/plugins/infra/public/services/telemetry/types.ts +++ b/x-pack/plugins/infra/public/services/telemetry/types.ts @@ -41,6 +41,8 @@ export interface HostFlyoutFilterActionParams { export interface HostsViewQueryHostsCountRetrievedParams { total: number; + with_query: boolean; + with_filters: boolean; } export interface AssetDetailsFlyoutViewedParams { diff --git a/x-pack/plugins/observability/public/embeddable/slo/overview/slo_overview.tsx b/x-pack/plugins/observability/public/embeddable/slo/overview/slo_overview.tsx index 0bd1b7048dcc0..cb0a6492ed310 100644 --- a/x-pack/plugins/observability/public/embeddable/slo/overview/slo_overview.tsx +++ b/x-pack/plugins/observability/public/embeddable/slo/overview/slo_overview.tsx @@ -112,7 +112,7 @@ export function SloOverview({ return (
- + ; export function useCloneSlo() { const { - http, - notifications: { toasts }, + http: { basePath }, + application: { navigateToUrl }, } = useKibana().services; - const queryClient = useQueryClient(); - return useMutation< - CreateSLOResponse, - ServerError, - { slo: CreateSLOInput; originalSloId?: string }, - { previousData?: FindSLOResponse; queryKey?: QueryKey } - >( - ['cloneSlo'], - ({ slo }: { slo: CreateSLOInput; originalSloId?: string }) => { - const body = JSON.stringify(slo); - return http.post(`/api/observability/slos`, { body }); + return useCallback( + (slo: SLOWithSummaryResponse) => { + navigateToUrl( + basePath.prepend( + paths.observability.sloCreateWithEncodedForm( + encode({ ...slo, name: `[Copy] ${slo.name}`, id: undefined }) + ) + ) + ); }, - { - onMutate: async ({ slo, originalSloId }) => { - await queryClient.cancelQueries({ queryKey: sloKeys.lists(), exact: false }); - - const queriesData = queryClient.getQueriesData({ - queryKey: sloKeys.lists(), - exact: false, - }); - const [queryKey, previousData] = queriesData?.at(0) ?? []; - - const originalSlo = previousData?.results?.find((el) => el.id === originalSloId); - const optimisticUpdate = { - page: previousData?.page ?? 1, - perPage: previousData?.perPage ?? 25, - total: previousData?.total ? previousData.total + 1 : 1, - results: [ - ...(previousData?.results ?? []), - { ...originalSlo, name: slo.name, id: uuidv4(), summary: undefined }, - ], - }; - - if (queryKey) { - queryClient.setQueryData(queryKey, optimisticUpdate); - } - - return { queryKey, previousData }; - }, - // If the mutation fails, use the context returned from onMutate to roll back - onError: (error, { slo }, context) => { - if (context?.previousData && context?.queryKey) { - queryClient.setQueryData(context.queryKey, context.previousData); - } - - toasts.addError(new Error(error.body?.message ?? error.message), { - title: i18n.translate('xpack.observability.slo.clone.errorNotification', { - defaultMessage: 'Failed to clone {name}', - values: { name: slo.name }, - }), - }); - }, - onSuccess: (_data, { slo }) => { - toasts.addSuccess( - i18n.translate('xpack.observability.slo.clone.successNotification', { - defaultMessage: 'Successfully created {name}', - values: { name: slo.name }, - }) - ); - queryClient.invalidateQueries({ queryKey: sloKeys.lists(), exact: false }); - }, - } + [navigateToUrl, basePath] ); } diff --git a/x-pack/plugins/observability/public/pages/slo_details/components/header_control.tsx b/x-pack/plugins/observability/public/pages/slo_details/components/header_control.tsx index 615edf3ef65f3..34eb1f278e7a3 100644 --- a/x-pack/plugins/observability/public/pages/slo_details/components/header_control.tsx +++ b/x-pack/plugins/observability/public/pages/slo_details/components/header_control.tsx @@ -10,20 +10,16 @@ import { i18n } from '@kbn/i18n'; import { EuiButton, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } from '@elastic/eui'; import { SLOWithSummaryResponse } from '@kbn/slo-schema'; +import { useCloneSlo } from '../../../hooks/slo/use_clone_slo'; import { SloDeleteConfirmationModal } from '../../../components/slo/delete_confirmation_modal/slo_delete_confirmation_modal'; import { useCapabilities } from '../../../hooks/slo/use_capabilities'; import { useKibana } from '../../../utils/kibana_react'; -import { useCloneSlo } from '../../../hooks/slo/use_clone_slo'; import { useDeleteSlo } from '../../../hooks/slo/use_delete_slo'; import { isApmIndicatorType } from '../../../utils/slo/indicator'; import { convertSliApmParamsToApmAppDeeplinkUrl } from '../../../utils/slo/convert_sli_apm_params_to_apm_app_deeplink_url'; import { SLO_BURN_RATE_RULE_TYPE_ID } from '../../../../common/constants'; import { rulesLocatorID, sloFeatureId } from '../../../../common'; import { paths } from '../../../../common/locators/paths'; -import { - transformSloResponseToCreateSloForm, - transformCreateSLOFormToCreateSLOInput, -} from '../../slo_edit/helpers/process_slo_form_values'; import type { RulesParams } from '../../../locators/rules'; export interface Props { @@ -47,7 +43,6 @@ export function HeaderControl({ isLoading, slo }: Props) { const [isRuleFlyoutVisible, setRuleFlyoutVisibility] = useState(false); const [isDeleteConfirmationModalOpen, setDeleteConfirmationModalOpen] = useState(false); - const { mutate: cloneSlo } = useCloneSlo(); const { mutate: deleteSlo } = useDeleteSlo(); const handleActionsClick = () => setIsPopoverOpen((value) => !value); @@ -101,17 +96,12 @@ export function HeaderControl({ isLoading, slo }: Props) { } }; + const navigateToClone = useCloneSlo(); + const handleClone = async () => { if (slo) { setIsPopoverOpen(false); - - const newSlo = transformCreateSLOFormToCreateSLOInput( - transformSloResponseToCreateSloForm({ ...slo, name: `[Copy] ${slo.name}` })! - ); - - cloneSlo({ slo: newSlo, originalSloId: slo.id }); - - navigate(basePath.prepend(paths.observability.slos)); + navigateToClone(slo); } }; diff --git a/x-pack/plugins/observability/public/pages/slo_details/slo_details.test.tsx b/x-pack/plugins/observability/public/pages/slo_details/slo_details.test.tsx index c9b04fee85eb2..5a06bbd9bf2db 100644 --- a/x-pack/plugins/observability/public/pages/slo_details/slo_details.test.tsx +++ b/x-pack/plugins/observability/public/pages/slo_details/slo_details.test.tsx @@ -17,7 +17,6 @@ import { useFetchSloDetails } from '../../hooks/slo/use_fetch_slo_details'; import { useFetchHistoricalSummary } from '../../hooks/slo/use_fetch_historical_summary'; import { useFetchActiveAlerts } from '../../hooks/slo/use_fetch_active_alerts'; import { ActiveAlerts } from '../../hooks/slo/active_alerts'; -import { useCloneSlo } from '../../hooks/slo/use_clone_slo'; import { useDeleteSlo } from '../../hooks/slo/use_delete_slo'; import { render } from '../../utils/test_helper'; import { SloDetailsPage } from './slo_details'; @@ -30,6 +29,7 @@ import { import { chartPluginMock } from '@kbn/charts-plugin/public/mocks'; import { buildApmAvailabilityIndicator } from '../../data/slo/indicator'; import { ALL_VALUE } from '@kbn/slo-schema'; +import { encode } from '@kbn/rison'; jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), @@ -44,7 +44,6 @@ jest.mock('../../hooks/slo/use_capabilities'); jest.mock('../../hooks/slo/use_fetch_active_alerts'); jest.mock('../../hooks/slo/use_fetch_slo_details'); jest.mock('../../hooks/slo/use_fetch_historical_summary'); -jest.mock('../../hooks/slo/use_clone_slo'); jest.mock('../../hooks/slo/use_delete_slo'); const useKibanaMock = useKibana as jest.Mock; @@ -55,12 +54,10 @@ const useCapabilitiesMock = useCapabilities as jest.Mock; const useFetchActiveAlertsMock = useFetchActiveAlerts as jest.Mock; const useFetchSloDetailsMock = useFetchSloDetails as jest.Mock; const useFetchHistoricalSummaryMock = useFetchHistoricalSummary as jest.Mock; -const useCloneSloMock = useCloneSlo as jest.Mock; const useDeleteSloMock = useDeleteSlo as jest.Mock; const mockNavigate = jest.fn(); const mockLocator = jest.fn(); -const mockClone = jest.fn(); const mockDelete = jest.fn(); const mockCapabilities = { apm: { show: true }, @@ -120,7 +117,6 @@ describe('SLO Details Page', () => { data: historicalSummaryData, }); useFetchActiveAlertsMock.mockReturnValue({ isLoading: false, data: new ActiveAlerts() }); - useCloneSloMock.mockReturnValue({ mutate: mockClone }); useDeleteSloMock.mockReturnValue({ mutate: mockDelete }); useLocationMock.mockReturnValue({ search: '' }); }); @@ -248,29 +244,12 @@ describe('SLO Details Page', () => { fireEvent.click(button!); - const { - id, - createdAt, - enabled, - revision, - summary, - settings, - updatedAt, - instanceId, - version, - ...newSlo - } = slo; - - expect(mockClone).toBeCalledWith({ - originalSloId: slo.id, - slo: { - ...newSlo, - name: `[Copy] ${newSlo.name}`, - }, - }); - await waitFor(() => { - expect(mockNavigate).toBeCalledWith(paths.observability.slos); + expect(mockNavigate).toBeCalledWith( + paths.observability.sloCreateWithEncodedForm( + encode({ ...slo, name: `[Copy] ${slo.name}`, id: undefined }) + ) + ); }); }); diff --git a/x-pack/plugins/observability/public/pages/slo_edit/components/slo_edit_form.tsx b/x-pack/plugins/observability/public/pages/slo_edit/components/slo_edit_form.tsx index 6627f910a7c27..e5f8e9b02ce2a 100644 --- a/x-pack/plugins/observability/public/pages/slo_edit/components/slo_edit_form.tsx +++ b/x-pack/plugins/observability/public/pages/slo_edit/components/slo_edit_form.tsx @@ -18,8 +18,7 @@ import { i18n } from '@kbn/i18n'; import type { GetSLOResponse } from '@kbn/slo-schema'; import React, { useCallback, useEffect, useState } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; -import { sloFeatureId } from '../../../../common'; -import { SLO_BURN_RATE_RULE_TYPE_ID } from '../../../../common/constants'; +import { BurnRateRuleFlyout } from '../../slos/components/common/burn_rate_rule_flyout'; import { paths } from '../../../../common/locators/paths'; import { useCreateSlo } from '../../../hooks/slo/use_create_slo'; import { useFetchRulesForSlo } from '../../../hooks/slo/use_fetch_rules_for_slo'; @@ -54,7 +53,6 @@ export function SloEditForm({ slo }: Props) { const { application: { navigateToUrl }, http: { basePath }, - triggersActionsUi: { getAddRuleFlyout: AddRuleFlyout }, } = useKibana().services; const isEditMode = slo !== undefined; @@ -146,10 +144,6 @@ export function SloEditForm({ slo }: Props) { setIsCreateRuleCheckboxChecked(!isCreateRuleCheckboxChecked); }; - const handleCloseRuleFlyout = async () => { - navigateToUrl(basePath.prepend(paths.observability.slos)); - }; - return ( <> @@ -256,17 +250,11 @@ export function SloEditForm({ slo }: Props) { - {isAddRuleFlyoutOpen && slo ? ( - - ) : null} + ); } diff --git a/x-pack/plugins/observability/public/pages/slo_edit/slo_edit.tsx b/x-pack/plugins/observability/public/pages/slo_edit/slo_edit.tsx index 27fda5cfc61fa..485ee068fc6f7 100644 --- a/x-pack/plugins/observability/public/pages/slo_edit/slo_edit.tsx +++ b/x-pack/plugins/observability/public/pages/slo_edit/slo_edit.tsx @@ -33,7 +33,7 @@ export function SloEditPage() { const { sloId } = useParams<{ sloId: string | undefined }>(); const { hasAtLeast } = useLicense(); const hasRightLicense = hasAtLeast('platinum'); - const { data: slo, isInitialLoading } = useFetchSloDetails({ sloId }); + const { data: slo } = useFetchSloDetails({ sloId }); useBreadcrumbs([ { @@ -66,10 +66,6 @@ export function SloEditPage() { navigateToUrl(basePath.prepend(paths.observability.slos)); } - if (sloId && isInitialLoading) { - return null; - } - return ( { +const getSubTitle = (slo: SLOWithSummaryResponse) => { return slo.groupBy && slo.groupBy !== ALL_VALUE ? `${slo.groupBy}: ${slo.instanceId}` : ''; }; @@ -88,14 +89,14 @@ export function SloCardItem({ slo, rules, activeAlerts, historicalSummary, cards } }} paddingSize="none" - style={{ - height: '182px', - overflow: 'hidden', - position: 'relative', - }} + css={css` + height: 182px; + overflow: hidden; + position: relative; + `} title={slo.summary.status} > - + {(isMouseOver || isActionsPopoverOpen) && ( ; }) { const { @@ -147,7 +146,7 @@ export function SloCardChart({ } = useKibana().services; const cardColor = useSloCardColor(slo.summary.status); - const subTitle = getSubTitle(slo, cardsPerRow); + const subTitle = getSubTitle(slo); const { sliValue, sloTarget, sloDetailsUrl } = useSloFormattedSummary(slo); return ( diff --git a/x-pack/plugins/observability/public/pages/slos/components/card_view/slos_card_view.tsx b/x-pack/plugins/observability/public/pages/slos/components/card_view/slos_card_view.tsx index a8d43c35217d1..3e1a80f147f2e 100644 --- a/x-pack/plugins/observability/public/pages/slos/components/card_view/slos_card_view.tsx +++ b/x-pack/plugins/observability/public/pages/slos/components/card_view/slos_card_view.tsx @@ -68,27 +68,29 @@ export function SloListCardView({ } return ( - - {sloList.map((slo) => ( - - - historicalSummary.sloId === slo.id && - historicalSummary.instanceId === (slo.instanceId ?? ALL_VALUE) - )?.data - } - historicalSummaryLoading={historicalSummaryLoading} - cardsPerRow={Number(cardsPerRow)} - /> - - ))} + + {sloList + .filter((slo) => slo.summary) + .map((slo) => ( + + + historicalSummary.sloId === slo.id && + historicalSummary.instanceId === (slo.instanceId ?? ALL_VALUE) + )?.data + } + historicalSummaryLoading={historicalSummaryLoading} + cardsPerRow={Number(cardsPerRow)} + /> + + ))} ); } diff --git a/x-pack/plugins/observability/public/pages/slos/components/common/burn_rate_rule_flyout.tsx b/x-pack/plugins/observability/public/pages/slos/components/common/burn_rate_rule_flyout.tsx index a02730231ae5f..871c11d39bdde 100644 --- a/x-pack/plugins/observability/public/pages/slos/components/common/burn_rate_rule_flyout.tsx +++ b/x-pack/plugins/observability/public/pages/slos/components/common/burn_rate_rule_flyout.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { SLOWithSummaryResponse } from '@kbn/slo-schema'; import { useQueryClient } from '@tanstack/react-query'; +import { paths } from '../../../../../common/locators/paths'; import { useGetFilteredRuleTypes } from '../../../../hooks/use_get_filtered_rule_types'; import { sloKeys } from '../../../../hooks/slo/query_key_factory'; import { useKibana } from '../../../../utils/kibana_react'; @@ -17,13 +18,17 @@ import { sloFeatureId } from '../../../../../common'; export function BurnRateRuleFlyout({ slo, isAddRuleFlyoutOpen, + canChangeTrigger, setIsAddRuleFlyoutOpen, }: { - slo: SLOWithSummaryResponse; + slo?: SLOWithSummaryResponse; isAddRuleFlyoutOpen: boolean; - setIsAddRuleFlyoutOpen: (value: boolean) => void; + canChangeTrigger?: boolean; + setIsAddRuleFlyoutOpen?: (value: boolean) => void; }) { const { + application: { navigateToUrl }, + http: { basePath }, triggersActionsUi: { getAddRuleFlyout: AddRuleFlyout }, } = useKibana().services; @@ -32,19 +37,30 @@ export function BurnRateRuleFlyout({ const queryClient = useQueryClient(); const handleSavedRule = async () => { - queryClient.invalidateQueries({ queryKey: sloKeys.rules(), exact: false }); + if (setIsAddRuleFlyoutOpen) { + queryClient.invalidateQueries({ queryKey: sloKeys.rules(), exact: false }); + } else { + navigateToUrl(basePath.prepend(paths.observability.slos)); + } }; - return isAddRuleFlyoutOpen ? ( + const handleCloseRuleFlyout = async () => { + if (setIsAddRuleFlyoutOpen) { + setIsAddRuleFlyoutOpen(false); + } else { + navigateToUrl(basePath.prepend(paths.observability.slos)); + } + }; + + return isAddRuleFlyoutOpen && slo ? ( { - setIsAddRuleFlyoutOpen(false); - }} + onClose={handleCloseRuleFlyout} useRuleProducer /> ) : null; diff --git a/x-pack/plugins/observability/public/pages/slos/components/compact_view/slo_list_compact_view.tsx b/x-pack/plugins/observability/public/pages/slos/components/compact_view/slo_list_compact_view.tsx index d7e1c78d117e6..d884de55aed54 100644 --- a/x-pack/plugins/observability/public/pages/slos/components/compact_view/slo_list_compact_view.tsx +++ b/x-pack/plugins/observability/public/pages/slos/components/compact_view/slo_list_compact_view.tsx @@ -19,6 +19,7 @@ import { i18n } from '@kbn/i18n'; import { ALL_VALUE, SLOWithSummaryResponse } from '@kbn/slo-schema'; import { useQueryClient } from '@tanstack/react-query'; import React, { useState } from 'react'; +import { useCloneSlo } from '../../../../hooks/slo/use_clone_slo'; import { rulesLocatorID, sloFeatureId } from '../../../../../common'; import { SLO_BURN_RATE_RULE_TYPE_ID } from '../../../../../common/constants'; import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; @@ -28,7 +29,6 @@ import { SloStatusBadge } from '../../../../components/slo/slo_status_badge'; import { SloActiveAlertsBadge } from '../../../../components/slo/slo_status_badge/slo_active_alerts_badge'; import { sloKeys } from '../../../../hooks/slo/query_key_factory'; import { useCapabilities } from '../../../../hooks/slo/use_capabilities'; -import { useCloneSlo } from '../../../../hooks/slo/use_clone_slo'; import { useDeleteSlo } from '../../../../hooks/slo/use_delete_slo'; import { useFetchActiveAlerts } from '../../../../hooks/slo/use_fetch_active_alerts'; import { useFetchHistoricalSummary } from '../../../../hooks/slo/use_fetch_historical_summary'; @@ -37,10 +37,6 @@ import { useGetFilteredRuleTypes } from '../../../../hooks/use_get_filtered_rule import { RulesParams } from '../../../../locators/rules'; import { useKibana } from '../../../../utils/kibana_react'; import { formatHistoricalData } from '../../../../utils/slo/chart_data_formatter'; -import { - transformCreateSLOFormToCreateSLOInput, - transformSloResponseToCreateSloForm, -} from '../../../slo_edit/helpers/process_slo_form_values'; import { SloRulesBadge } from '../badges/slo_rules_badge'; import { SloListEmpty } from '../slo_list_empty'; import { SloListError } from '../slo_list_error'; @@ -72,7 +68,6 @@ export function SloListCompactView({ sloList, loading, error }: Props) { const filteredRuleTypes = useGetFilteredRuleTypes(); const queryClient = useQueryClient(); - const { mutate: cloneSlo } = useCloneSlo(); const { mutate: deleteSlo } = useDeleteSlo(); const [sloToAddRule, setSloToAddRule] = useState(undefined); @@ -102,6 +97,8 @@ export function SloListCompactView({ sloList, loading, error }: Props) { list: sloList.map((slo) => ({ sloId: slo.id, instanceId: slo.instanceId ?? ALL_VALUE })), }); + const navigateToClone = useCloneSlo(); + const actions: Array> = [ { type: 'icon', @@ -180,11 +177,7 @@ export function SloListCompactView({ sloList, loading, error }: Props) { 'data-test-subj': 'sloActionsClone', enabled: (_) => hasWriteCapabilities, onClick: (slo: SLOWithSummaryResponse) => { - const newSlo = transformCreateSLOFormToCreateSLOInput( - transformSloResponseToCreateSloForm({ ...slo, name: `[Copy] ${slo.name}` })! - ); - - cloneSlo({ slo: newSlo, originalSloId: slo.id }); + navigateToClone(slo); }, }, { diff --git a/x-pack/plugins/observability/public/pages/slos/components/slo_item_actions.tsx b/x-pack/plugins/observability/public/pages/slos/components/slo_item_actions.tsx index 4fb03968d40a0..51652abe13542 100644 --- a/x-pack/plugins/observability/public/pages/slos/components/slo_item_actions.tsx +++ b/x-pack/plugins/observability/public/pages/slos/components/slo_item_actions.tsx @@ -17,16 +17,12 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { ALL_VALUE, SLOWithSummaryResponse } from '@kbn/slo-schema'; import styled from 'styled-components'; -import { useCapabilities } from '../../../hooks/slo/use_capabilities'; import { useCloneSlo } from '../../../hooks/slo/use_clone_slo'; +import { useCapabilities } from '../../../hooks/slo/use_capabilities'; import { useKibana } from '../../../utils/kibana_react'; import { paths } from '../../../../common/locators/paths'; import { RulesParams } from '../../../locators/rules'; import { rulesLocatorID } from '../../../../common'; -import { - transformCreateSLOFormToCreateSLOInput, - transformSloResponseToCreateSloForm, -} from '../../slo_edit/helpers/process_slo_form_values'; interface Props { slo: SLOWithSummaryResponse; @@ -73,7 +69,6 @@ export function SloItemActions({ }, } = useKibana().services; const { hasWriteCapabilities } = useCapabilities(); - const { mutate: cloneSlo } = useCloneSlo(); const sloDetailsUrl = basePath.prepend( paths.observability.sloDetails( @@ -94,18 +89,15 @@ export function SloItemActions({ navigateToUrl(basePath.prepend(paths.observability.sloEdit(slo.id))); }; - const handleNavigateToRules = async () => { - const locator = locators.get(rulesLocatorID); - locator?.navigate({ params: { sloId: slo.id } }, { replace: false }); - }; + const navigateToClone = useCloneSlo(); const handleClone = () => { - const newSlo = transformCreateSLOFormToCreateSLOInput( - transformSloResponseToCreateSloForm({ ...slo, name: `[Copy] ${slo.name}` })! - ); + navigateToClone(slo); + }; - cloneSlo({ slo: newSlo, originalSloId: slo.id }); - setIsActionsPopoverOpen(false); + const handleNavigateToRules = async () => { + const locator = locators.get(rulesLocatorID); + locator?.navigate({ params: { sloId: slo.id } }, { replace: false }); }; const handleDelete = () => { diff --git a/x-pack/plugins/observability/public/pages/slos/slos.test.tsx b/x-pack/plugins/observability/public/pages/slos/slos.test.tsx index 40fd12d4b003e..1f28e822b96f9 100644 --- a/x-pack/plugins/observability/public/pages/slos/slos.test.tsx +++ b/x-pack/plugins/observability/public/pages/slos/slos.test.tsx @@ -15,7 +15,6 @@ import { paths } from '../../../common/locators/paths'; import { historicalSummaryData } from '../../data/slo/historical_summary_data'; import { emptySloList, sloList } from '../../data/slo/slo'; import { useCapabilities } from '../../hooks/slo/use_capabilities'; -import { useCloneSlo } from '../../hooks/slo/use_clone_slo'; import { useCreateSlo } from '../../hooks/slo/use_create_slo'; import { useDeleteSlo } from '../../hooks/slo/use_delete_slo'; import { useFetchHistoricalSummary } from '../../hooks/slo/use_fetch_historical_summary'; @@ -24,6 +23,7 @@ import { useLicense } from '../../hooks/use_license'; import { useKibana } from '../../utils/kibana_react'; import { render } from '../../utils/test_helper'; import { SlosPage } from './slos'; +import { encode } from '@kbn/rison'; jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), @@ -35,7 +35,6 @@ jest.mock('../../utils/kibana_react'); jest.mock('../../hooks/use_license'); jest.mock('../../hooks/slo/use_fetch_slo_list'); jest.mock('../../hooks/slo/use_create_slo'); -jest.mock('../../hooks/slo/use_clone_slo'); jest.mock('../../hooks/slo/use_delete_slo'); jest.mock('../../hooks/slo/use_fetch_historical_summary'); jest.mock('../../hooks/slo/use_capabilities'); @@ -44,17 +43,14 @@ const useKibanaMock = useKibana as jest.Mock; const useLicenseMock = useLicense as jest.Mock; const useFetchSloListMock = useFetchSloList as jest.Mock; const useCreateSloMock = useCreateSlo as jest.Mock; -const useCloneSloMock = useCloneSlo as jest.Mock; const useDeleteSloMock = useDeleteSlo as jest.Mock; const useFetchHistoricalSummaryMock = useFetchHistoricalSummary as jest.Mock; const useCapabilitiesMock = useCapabilities as jest.Mock; const mockCreateSlo = jest.fn(); -const mockCloneSlo = jest.fn(); const mockDeleteSlo = jest.fn(); useCreateSloMock.mockReturnValue({ mutate: mockCreateSlo }); -useCloneSloMock.mockReturnValue({ mutate: mockCloneSlo }); useDeleteSloMock.mockReturnValue({ mutate: mockDeleteSlo }); const mockNavigate = jest.fn(); @@ -358,7 +354,14 @@ describe('SLOs Page', () => { button.click(); - expect(mockCloneSlo).toBeCalled(); + await waitFor(() => { + const slo = sloList.results.at(0); + expect(mockNavigate).toBeCalledWith( + paths.observability.sloCreateWithEncodedForm( + encode({ ...slo, name: `[Copy] ${slo!.name}`, id: undefined }) + ) + ); + }); }); }); }); diff --git a/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/executor.ts b/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/executor.ts index ddcd3b244dbb6..e1891e68af58b 100644 --- a/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/executor.ts +++ b/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/executor.ts @@ -98,7 +98,6 @@ export const getRuleExecutor = ({ const alertLimit = alertFactory.alertLimit.getValue(); let hasReachedLimit = false; let scheduledActionsCount = 0; - for (const result of results) { const { instanceId, @@ -172,40 +171,40 @@ export const getRuleExecutor = ({ scheduledActionsCount++; } } + alertFactory.alertLimit.setLimitReached(hasReachedLimit); + } - const { getRecoveredAlerts } = alertFactory.done(); - const recoveredAlerts = getRecoveredAlerts(); - for (const recoveredAlert of recoveredAlerts) { - const alertId = recoveredAlert.getId(); - const indexedStartedAt = getAlertStartedDate(alertId) ?? startedAt.toISOString(); - const alertUuid = recoveredAlert.getUuid(); - const alertDetailsUrl = await getAlertUrl( - alertUuid, - spaceId, - indexedStartedAt, - alertsLocator, - basePath.publicBaseUrl - ); + const { getRecoveredAlerts } = alertFactory.done(); + const recoveredAlerts = getRecoveredAlerts(); + for (const recoveredAlert of recoveredAlerts) { + const alertId = recoveredAlert.getId(); + const indexedStartedAt = getAlertStartedDate(alertId) ?? startedAt.toISOString(); + const alertUuid = recoveredAlert.getUuid(); + const alertDetailsUrl = await getAlertUrl( + alertUuid, + spaceId, + indexedStartedAt, + alertsLocator, + basePath.publicBaseUrl + ); - const urlQuery = alertId === ALL_VALUE ? '' : `?instanceId=${alertId}`; - const viewInAppUrl = addSpaceIdToPath( - basePath.publicBaseUrl, - spaceId, - `/app/observability/slos/${slo.id}${urlQuery}` - ); + const urlQuery = alertId === ALL_VALUE ? '' : `?instanceId=${alertId}`; + const viewInAppUrl = addSpaceIdToPath( + basePath.publicBaseUrl, + spaceId, + `/app/observability/slos/${slo.id}${urlQuery}` + ); - const context = { - timestamp: startedAt.toISOString(), - viewInAppUrl, - alertDetailsUrl, - sloId: slo.id, - sloName: slo.name, - sloInstanceId: alertId, - }; + const context = { + timestamp: startedAt.toISOString(), + viewInAppUrl, + alertDetailsUrl, + sloId: slo.id, + sloName: slo.name, + sloInstanceId: alertId, + }; - recoveredAlert.setContext(context); - } - alertFactory.alertLimit.setLimitReached(hasReachedLimit); + recoveredAlert.setContext(context); } return { state: {} }; diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_body.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_body.tsx index 60aa56c15dda8..c38d328a87bcb 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_body.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_body.tsx @@ -14,6 +14,8 @@ import { EuiHorizontalRule, EuiPanel, EuiSpacer, + useEuiTheme, + euiScrollBarStyles, } from '@elastic/eui'; import type { AuthenticatedUser } from '@kbn/security-plugin/common'; import { euiThemeVars } from '@kbn/ui-theme'; @@ -40,8 +42,9 @@ const fullHeightClassName = css` height: 100%; `; -const timelineClassName = css` +const timelineClassName = (scrollBarStyles: string) => css` overflow-y: auto; + ${scrollBarStyles} `; const promptEditorClassname = css` @@ -104,6 +107,8 @@ export function ChatBody({ }) { const license = useLicense(); const hasCorrectLicense = license?.hasAtLeast('enterprise'); + const euiTheme = useEuiTheme(); + const scrollBarStyles = euiScrollBarStyles(euiTheme); const chatService = useObservabilityAIAssistantChatService(); @@ -217,7 +222,7 @@ export function ChatBody({ } else { footer = ( <> - +
css` overflow-y: auto; + ${scrollBarStyles} `; const newChatButtonWrapperClassName = css` @@ -56,10 +59,12 @@ export function ConversationList({ onClickNewChat: () => void; onClickDeleteConversation: (id: string) => void; }) { + const euiTheme = useEuiTheme(); + const scrollBarStyles = euiScrollBarStyles(euiTheme); return ( - + diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 549c459b4944f..1c0f90d9ad92e 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -28790,8 +28790,6 @@ "xpack.observability.slo.alerting.burnRate.reasonForInstanceId": "{actionGroupName} : Le taux d'avancement pour le (les) dernier(s) {longWindowDuration} est de {longWindowBurnRate} et pour le (les) dernier(s) {shortWindowDuration} est de {shortWindowBurnRate} pour {instanceId}. Alerter si supérieur à {burnRateThreshold} pour les deux fenêtres", "xpack.observability.slo.burnRate.breachedStatustSubtitle": "Au rythme actuel, le budget d'erreur sera épuisé en {hour} heures.", "xpack.observability.slo.burnRate.threshold": "Le seuil est {threshold}x", - "xpack.observability.slo.clone.errorNotification": "Échec du clonage de {name}", - "xpack.observability.slo.clone.successNotification": "{name} créé avec succès", "xpack.observability.slo.create.errorNotification": "Un problème est survenu lors de la création de {name}", "xpack.observability.slo.create.successNotification": "{name} créé avec succès", "xpack.observability.slo.deleteConfirmationModal.title": "Supprimer {name} ?", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 8ab33bb6186bb..cc52a3743aca8 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -28790,8 +28790,6 @@ "xpack.observability.slo.alerting.burnRate.reasonForInstanceId": "{actionGroupName}:過去{longWindowDuration}のバーンレートは{longWindowBurnRate}、{instanceId}の過去{shortWindowDuration}のバーンレートは{shortWindowBurnRate}です。両期間とも{burnRateThreshold}を超えたらアラート", "xpack.observability.slo.burnRate.breachedStatustSubtitle": "現在のレートでは、エラー予算は{hour}時間後に使い果たされます。", "xpack.observability.slo.burnRate.threshold": "しきい値は{threshold}xです", - "xpack.observability.slo.clone.errorNotification": "{name}を複製できませんでした", - "xpack.observability.slo.clone.successNotification": "{name}の作成が正常に完了しました", "xpack.observability.slo.create.errorNotification": "{name}の作成中に問題が発生しました", "xpack.observability.slo.create.successNotification": "{name}の作成が正常に完了しました", "xpack.observability.slo.deleteConfirmationModal.title": "{name}を削除しますか?", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index d12e878252732..4a724bbb7c4a6 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -28787,8 +28787,6 @@ "xpack.observability.slo.alerting.burnRate.reasonForInstanceId": "{actionGroupName}:过去 {longWindowDuration} 的消耗速度为 {longWindowBurnRate},且对于 {instanceId},过去 {shortWindowDuration} 为 {shortWindowBurnRate}。两个窗口超出 {burnRateThreshold} 时告警", "xpack.observability.slo.burnRate.breachedStatustSubtitle": "按照当前的速率,错误预算将在 {hour} 小时后耗尽。", "xpack.observability.slo.burnRate.threshold": "阈值为 {threshold}x", - "xpack.observability.slo.clone.errorNotification": "无法克隆 {name}", - "xpack.observability.slo.clone.successNotification": "已成功创建 {name}", "xpack.observability.slo.create.errorNotification": "创建 {name} 时出现问题", "xpack.observability.slo.create.successNotification": "已成功创建 {name}", "xpack.observability.slo.deleteConfirmationModal.title": "删除 {name}?", diff --git a/x-pack/test/cloud_security_posture_api/config.ts b/x-pack/test/cloud_security_posture_api/config.ts index e7a34bafe9fb5..625236022cd14 100644 --- a/x-pack/test/cloud_security_posture_api/config.ts +++ b/x-pack/test/cloud_security_posture_api/config.ts @@ -18,6 +18,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { require.resolve('./telemetry/telemetry.ts'), require.resolve('./routes/vulnerabilities_dashboard.ts'), require.resolve('./routes/stats.ts'), + require.resolve('./routes/csp_benchmark_rules_bulk_update.ts'), ], junit: { reportName: 'X-Pack Cloud Security Posture API Tests', diff --git a/x-pack/test/cloud_security_posture_api/routes/csp_benchmark_rules_bulk_update.ts b/x-pack/test/cloud_security_posture_api/routes/csp_benchmark_rules_bulk_update.ts new file mode 100644 index 0000000000000..dad7845e60e31 --- /dev/null +++ b/x-pack/test/cloud_security_posture_api/routes/csp_benchmark_rules_bulk_update.ts @@ -0,0 +1,247 @@ +/* + * 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 { expect as expectExpect } from 'expect'; + +import { + ELASTIC_HTTP_VERSION_HEADER, + X_ELASTIC_INTERNAL_ORIGIN_REQUEST, +} from '@kbn/core-http-common'; +import type { FtrProviderContext } from '../ftr_provider_context'; + +interface RuleIdentifier { + benchmarkId: string; + benchmarkVersion: string; + ruleNumber: string; +} + +// eslint-disable-next-line import/no-default-export +export default function ({ getService }: FtrProviderContext) { + const retry = getService('retry'); + const supertest = getService('supertest'); + const log = getService('log'); + const kibanaServer = getService('kibanaServer'); + + const generateRuleKey = (ruleParams: RuleIdentifier): string => { + return `${ruleParams.benchmarkId};${ruleParams.benchmarkVersion};${ruleParams.ruleNumber}`; + }; + + const generateRandomRule = (): RuleIdentifier => { + const majorVersionNumber = Math.floor(Math.random() * 10); // Random major number between 0 and 9 + const minorVersionNumber = Math.floor(Math.random() * 10); + const benchmarksIds = ['cis_aws', 'cis_k8s', 'cis_k8s']; + const benchmarksVersions = ['v2.0.0', 'v2.0.1', 'v2.0.3', 'v3.0.0']; + const randomBenchmarkId = benchmarksIds[Math.floor(Math.random() * benchmarksIds.length)]; + const randomBenchmarkVersion = + benchmarksVersions[Math.floor(Math.random() * benchmarksVersions.length)]; + + return { + benchmarkId: randomBenchmarkId, + benchmarkVersion: randomBenchmarkVersion, + ruleNumber: `${majorVersionNumber}.${minorVersionNumber}`, + }; + }; + + /** + * required before indexing findings + */ + const waitForPluginInitialized = (): Promise => + retry.try(async () => { + log.debug('Check CSP plugin is initialized'); + const response = await supertest + .get('/internal/cloud_security_posture/status?check=init') + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .expect(200); + expect(response.body).to.eql({ isPluginInitialized: true }); + log.debug('CSP plugin is initialized'); + }); + + describe('Verify update csp rules states API', async () => { + before(async () => { + await waitForPluginInitialized(); + }); + + afterEach(async () => { + await kibanaServer.savedObjects.clean({ + types: ['cloud-security-posture-settings'], + }); + }); + + it('mute rules successfully', async () => { + const rule1 = generateRandomRule(); + const rule2 = generateRandomRule(); + + const { body } = await supertest + .post(`/internal/cloud_security_posture/rules/_bulk_action`) + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .set('kbn-xsrf', 'xxxx') + .send({ + action: 'mute', + rules: [ + { + benchmark_id: rule1.benchmarkId, + benchmark_version: rule1.benchmarkVersion, + rule_number: rule1.ruleNumber, + }, + { + benchmark_id: rule2.benchmarkId, + benchmark_version: rule2.benchmarkVersion, + rule_number: rule2.ruleNumber, + }, + ], + }) + .expect(200); + + expectExpect(body.updated_benchmark_rules).toEqual( + expectExpect.objectContaining({ + [generateRuleKey(rule1)]: { muted: true }, + [generateRuleKey(rule2)]: { muted: true }, + }) + ); + }); + + it('unmute rules successfully', async () => { + const rule1 = generateRandomRule(); + const rule2 = generateRandomRule(); + + const { body } = await supertest + .post(`/internal/cloud_security_posture/rules/_bulk_action`) + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .set('kbn-xsrf', 'xxxx') + .send({ + action: 'unmute', + rules: [ + { + benchmark_id: rule1.benchmarkId, + benchmark_version: rule1.benchmarkVersion, + rule_number: rule1.ruleNumber, + }, + { + benchmark_id: rule2.benchmarkId, + benchmark_version: rule2.benchmarkVersion, + rule_number: rule2.ruleNumber, + }, + ], + }) + .expect(200); + + expectExpect(body.updated_benchmark_rules).toEqual( + expectExpect.objectContaining({ + [generateRuleKey(rule1)]: { muted: false }, + [generateRuleKey(rule2)]: { muted: false }, + }) + ); + }); + + it('verify new rules are added and existing rules are set.', async () => { + const rule1 = generateRandomRule(); + const rule2 = generateRandomRule(); + const rule3 = generateRandomRule(); + + // unmute rule1 and rule2 + const cspSettingsResponse = await supertest + .post(`/internal/cloud_security_posture/rules/_bulk_action`) + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .set('kbn-xsrf', 'xxxx') + .send({ + action: 'unmute', + rules: [ + { + benchmark_id: rule1.benchmarkId, + benchmark_version: rule1.benchmarkVersion, + rule_number: rule1.ruleNumber, + }, + { + benchmark_id: rule2.benchmarkId, + benchmark_version: rule2.benchmarkVersion, + rule_number: rule2.ruleNumber, + }, + ], + }) + .expect(200); + + expectExpect(cspSettingsResponse.body.updated_benchmark_rules).toEqual( + expectExpect.objectContaining({ + [generateRuleKey(rule1)]: { muted: false }, + [generateRuleKey(rule2)]: { muted: false }, + }) + ); + + // mute rule1 and rule3 + const updatedCspSettingsResponse = await supertest + .post(`/internal/cloud_security_posture/rules/_bulk_action`) + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .set('kbn-xsrf', 'xxxx') + .send({ + action: 'mute', + rules: [ + { + benchmark_id: rule1.benchmarkId, + benchmark_version: rule1.benchmarkVersion, + rule_number: rule1.ruleNumber, + }, + { + benchmark_id: rule3.benchmarkId, + benchmark_version: rule3.benchmarkVersion, + rule_number: rule3.ruleNumber, + }, + ], + }) + .expect(200); + + expectExpect(updatedCspSettingsResponse.body.updated_benchmark_rules).toEqual( + expectExpect.objectContaining({ + [generateRuleKey(rule1)]: { muted: true }, + [generateRuleKey(rule3)]: { muted: true }, + }) + ); + }); + + it('set wrong action input', async () => { + const rule1 = generateRandomRule(); + + const { body } = await supertest + .post(`/internal/cloud_security_posture/rules/_bulk_action`) + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .set('kbn-xsrf', 'xxxx') + .send({ + action: 'foo', + rules: [ + { + benchmark_id: rule1.benchmarkId, + benchmark_version: rule1.benchmarkVersion, + rule_number: rule1.ruleNumber, + }, + ], + }); + + expect(body.error).to.eql('Bad Request'); + expect(body.statusCode).to.eql(400); + }); + + it('set wrong rule ids input', async () => { + const { body } = await supertest + .post(`/internal/cloud_security_posture/rules/_bulk_action`) + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .set('kbn-xsrf', 'xxxx') + .send({ + action: 'mute', + rule_ids: ['invalid_rule_structure'], + }); + + expect(body.error).to.eql('Bad Request'); + expect(body.statusCode).to.eql(400); + }); + }); +} diff --git a/x-pack/test/dataset_quality_api_integration/tests/data_streams/degraded_docs.spec.ts b/x-pack/test/dataset_quality_api_integration/tests/data_streams/degraded_docs.spec.ts new file mode 100644 index 0000000000000..ba38cc432ff8c --- /dev/null +++ b/x-pack/test/dataset_quality_api_integration/tests/data_streams/degraded_docs.spec.ts @@ -0,0 +1,98 @@ +/* + * 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 { log, timerange } from '@kbn/apm-synthtrace-client'; +import expect from '@kbn/expect'; +import { DatasetQualityApiClientKey } from '../../common/config'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; + +export default function ApiTest({ getService }: FtrProviderContext) { + const registry = getService('registry'); + const synthtrace = getService('logSynthtraceEsClient'); + const datasetQualityApiClient = getService('datasetQualityApiClient'); + const start = '2023-12-11T18:00:00.000Z'; + const end = '2023-12-11T18:01:00.000Z'; + + async function callApiAs(user: DatasetQualityApiClientKey) { + return await datasetQualityApiClient[user]({ + endpoint: 'GET /internal/dataset_quality/data_streams/degraded_docs', + params: { + query: { + type: 'logs', + start, + end, + }, + }, + }); + } + + registry.when('Degraded docs', { config: 'basic' }, () => { + describe('and there are log documents', () => { + before(async () => { + await synthtrace.index([ + timerange(start, end) + .interval('1m') + .rate(1) + .generator((timestamp) => + log + .create() + .message('This is a log message') + .timestamp(timestamp) + .dataset('synth.1') + .defaults({ + 'log.file.path': '/my-service.log', + }) + ), + timerange(start, end) + .interval('1m') + .rate(1) + .generator((timestamp) => + log + .create() + .message('This is a log message') + .timestamp(timestamp) + .dataset('synth.2') + .logLevel( + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?' + ) + .defaults({ + 'log.file.path': '/my-service.log', + }) + ), + ]); + }); + + it('returns stats correctly', async () => { + const stats = await callApiAs('datasetQualityLogsUser'); + expect(stats.body.degradedDocs.length).to.be(2); + + const percentages = stats.body.degradedDocs.reduce( + (acc, curr) => ({ + ...acc, + [curr.dataset]: curr.percentage, + }), + {} as Record + ); + + expect(percentages['logs-synth.1-default']).to.be(0); + expect(percentages['logs-synth.2-default']).to.be(100); + }); + + after(async () => { + await synthtrace.clean(); + }); + }); + + describe('and there are not log documents', () => { + it('returns stats correctly', async () => { + const stats = await callApiAs('datasetQualityLogsUser'); + + expect(stats.body.degradedDocs.length).to.be(0); + }); + }); + }); +} diff --git a/x-pack/test/dataset_quality_api_integration/tests/es_utils.ts b/x-pack/test/dataset_quality_api_integration/tests/data_streams/es_utils.ts similarity index 100% rename from x-pack/test/dataset_quality_api_integration/tests/es_utils.ts rename to x-pack/test/dataset_quality_api_integration/tests/data_streams/es_utils.ts diff --git a/x-pack/test/dataset_quality_api_integration/tests/data_streams.spec.ts b/x-pack/test/dataset_quality_api_integration/tests/data_streams/stats.spec.ts similarity index 86% rename from x-pack/test/dataset_quality_api_integration/tests/data_streams.spec.ts rename to x-pack/test/dataset_quality_api_integration/tests/data_streams/stats.spec.ts index 6d11326bf213e..7fa60dcb4b118 100644 --- a/x-pack/test/dataset_quality_api_integration/tests/data_streams.spec.ts +++ b/x-pack/test/dataset_quality_api_integration/tests/data_streams/stats.spec.ts @@ -7,10 +7,10 @@ import { log, timerange } from '@kbn/apm-synthtrace-client'; import expect from '@kbn/expect'; -import { DatasetQualityApiClientKey } from '../common/config'; -import { DatasetQualityApiError } from '../common/dataset_quality_api_supertest'; -import { FtrProviderContext } from '../common/ftr_provider_context'; -import { expectToReject } from '../utils'; +import { DatasetQualityApiClientKey } from '../../common/config'; +import { DatasetQualityApiError } from '../../common/dataset_quality_api_supertest'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { expectToReject } from '../../utils'; import { cleanLogIndexTemplate, addIntegrationToLogIndexTemplate } from './es_utils'; export default function ApiTest({ getService }: FtrProviderContext) { @@ -43,7 +43,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); describe('when required privileges are set', () => { - describe('and uncategorized datastreams', () => { + describe('and categorized datastreams', () => { const integration = 'my-custom-integration'; before(async () => { @@ -67,8 +67,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(stats.body.dataStreamsStats.length).to.be(1); expect(stats.body.dataStreamsStats[0].integration).to.be(integration); expect(stats.body.dataStreamsStats[0].size).not.empty(); - expect(stats.body.dataStreamsStats[0].size_bytes).greaterThan(0); - expect(stats.body.dataStreamsStats[0].last_activity).greaterThan(0); + expect(stats.body.dataStreamsStats[0].sizeBytes).greaterThan(0); + expect(stats.body.dataStreamsStats[0].lastActivity).greaterThan(0); }); after(async () => { @@ -77,7 +77,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); }); - describe('and categorized datastreams', () => { + describe('and uncategorized datastreams', () => { before(async () => { await synthtrace.index([ timerange('2023-11-20T15:00:00.000Z', '2023-11-20T15:01:00.000Z') @@ -97,8 +97,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(stats.body.dataStreamsStats.length).to.be(1); expect(stats.body.dataStreamsStats[0].integration).not.ok(); expect(stats.body.dataStreamsStats[0].size).not.empty(); - expect(stats.body.dataStreamsStats[0].size_bytes).greaterThan(0); - expect(stats.body.dataStreamsStats[0].last_activity).greaterThan(0); + expect(stats.body.dataStreamsStats[0].sizeBytes).greaterThan(0); + expect(stats.body.dataStreamsStats[0].lastActivity).greaterThan(0); }); after(async () => {