diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/quickstart_client.gen.ts b/x-pack/solutions/security/plugins/security_solution/common/api/quickstart_client.gen.ts index 5f93b7eff890d..1def93db93f8d 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/api/quickstart_client.gen.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/api/quickstart_client.gen.ts @@ -399,6 +399,9 @@ import type { CreateDashboardMigrationDashboardsRequestBodyInput, GetDashboardMigrationRequestParamsInput, GetDashboardMigrationResponse, + GetDashboardMigrationDashboardsRequestQueryInput, + GetDashboardMigrationDashboardsRequestParamsInput, + GetDashboardMigrationDashboardsResponse, GetDashboardMigrationResourcesRequestQueryInput, GetDashboardMigrationResourcesRequestParamsInput, GetDashboardMigrationResourcesResponse, @@ -1501,6 +1504,26 @@ finalize it. }) .catch(catchAxiosErrorFormatAndThrow); } + /** + * Retrieves the dashboards added to an existing dashboard migration + */ + async getDashboardMigrationDashboards(props: GetDashboardMigrationDashboardsProps) { + this.log.info(`${new Date().toISOString()} Calling API GetDashboardMigrationDashboards`); + return this.kbnClient + .request({ + path: replaceParams( + '/internal/siem_migrations/dashboards/{migration_id}/dashboards', + props.params + ), + headers: { + [ELASTIC_HTTP_VERSION_HEADER]: '1', + }, + method: 'GET', + + query: props.query, + }) + .catch(catchAxiosErrorFormatAndThrow); + } /** * Retrieves resources for an existing SIEM dashboards migration */ @@ -3112,6 +3135,10 @@ export interface GetAssetCriticalityRecordProps { export interface GetDashboardMigrationProps { params: GetDashboardMigrationRequestParamsInput; } +export interface GetDashboardMigrationDashboardsProps { + query: GetDashboardMigrationDashboardsRequestQueryInput; + params: GetDashboardMigrationDashboardsRequestParamsInput; +} export interface GetDashboardMigrationResourcesProps { query: GetDashboardMigrationResourcesRequestQueryInput; params: GetDashboardMigrationResourcesRequestParamsInput; diff --git a/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/dashboards/types.ts b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/dashboards/types.ts index 6756a42cdb834..236ce7989a213 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/dashboards/types.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/dashboards/types.ts @@ -5,10 +5,10 @@ * 2.0. */ +import type { SiemMigrationGetItemsOptions } from '../../../server/lib/siem_migrations/common/data/types'; import type { SiemMigrationFilters } from '../types'; -export interface DashboardMigrationFilters extends SiemMigrationFilters { - searchTerm?: string; - installed?: boolean; - installable?: boolean; -} +export type DashboardMigrationFilters = SiemMigrationFilters; + +export type DashboardMigrationGetDashboardOptions = + SiemMigrationGetItemsOptions; diff --git a/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/api/dashboards/dashboard_migration.gen.ts b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/api/dashboards/dashboard_migration.gen.ts index a6735126a9465..1520c63f02d52 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/api/dashboards/dashboard_migration.gen.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/api/dashboards/dashboard_migration.gen.ts @@ -15,11 +15,12 @@ */ import { z } from '@kbn/zod'; -import { ArrayFromString } from '@kbn/zod-helpers'; +import { ArrayFromString, BooleanFromString } from '@kbn/zod-helpers'; import { NonEmptyString } from '../../../../api/model/primitives.gen'; import { DashboardMigration, + DashboardMigrationDashboard, DashboardMigrationTaskExecutionSettings, DashboardMigrationRetryFilter, DashboardMigrationTaskStats, @@ -82,6 +83,46 @@ export type GetDashboardMigrationRequestParamsInput = z.input< export type GetDashboardMigrationResponse = z.infer; export const GetDashboardMigrationResponse = DashboardMigration; +export type GetDashboardMigrationDashboardsRequestQuery = z.infer< + typeof GetDashboardMigrationDashboardsRequestQuery +>; +export const GetDashboardMigrationDashboardsRequestQuery = z.object({ + page: z.coerce.number().optional(), + per_page: z.coerce.number().optional(), + sort_field: NonEmptyString.optional(), + sort_direction: z.enum(['asc', 'desc']).optional(), + search_term: z.string().optional(), + ids: ArrayFromString(NonEmptyString).optional(), + is_installed: BooleanFromString.optional(), + is_fully_translated: BooleanFromString.optional(), + is_partially_translated: BooleanFromString.optional(), + is_untranslatable: BooleanFromString.optional(), + is_failed: BooleanFromString.optional(), +}); +export type GetDashboardMigrationDashboardsRequestQueryInput = z.input< + typeof GetDashboardMigrationDashboardsRequestQuery +>; + +export type GetDashboardMigrationDashboardsRequestParams = z.infer< + typeof GetDashboardMigrationDashboardsRequestParams +>; +export const GetDashboardMigrationDashboardsRequestParams = z.object({ + migration_id: NonEmptyString, +}); +export type GetDashboardMigrationDashboardsRequestParamsInput = z.input< + typeof GetDashboardMigrationDashboardsRequestParams +>; + +export type GetDashboardMigrationDashboardsResponse = z.infer< + typeof GetDashboardMigrationDashboardsResponse +>; +export const GetDashboardMigrationDashboardsResponse = z.object({ + /** + * The total number of rules in migration. + */ + total: z.number(), + data: z.array(DashboardMigrationDashboard), +}); export type GetDashboardMigrationResourcesRequestQuery = z.infer< typeof GetDashboardMigrationResourcesRequestQuery >; diff --git a/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/api/dashboards/dashboard_migration.schema.yaml b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/api/dashboards/dashboard_migration.schema.yaml index 3598156635d37..98042182248ce 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/api/dashboards/dashboard_migration.schema.yaml +++ b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/api/dashboards/dashboard_migration.schema.yaml @@ -93,6 +93,100 @@ paths: responses: 200: description: Indicates dashboards have been added to the migration successfully. + get: + summary: Retrieves dashboards for a migration + operationId: GetDashboardMigrationDashboards + x-codegen-enabled: true + x-internal: true + description: Retrieves the dashboards added to an existing dashboard migration + tags: + - SIEM Dashboard Migrations + parameters: + - name: migration_id + in: path + required: true + schema: + description: The migration id to start + $ref: '../../../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' + - name: page + in: query + required: false + schema: + type: number + - name: per_page + in: query + required: false + schema: + type: number + - name: sort_field + in: query + required: false + schema: + $ref: '../../../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' + - name: sort_direction + in: query + required: false + schema: + type: string + enum: + - asc + - desc + - name: search_term + in: query + required: false + schema: + type: string + - name: ids + in: query + required: false + schema: + type: array + items: + description: The rule migration id + $ref: '../../../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' + - name: is_installed + in: query + required: false + schema: + type: boolean + - name: is_fully_translated + in: query + required: false + schema: + type: boolean + - name: is_partially_translated + in: query + required: false + schema: + type: boolean + - name: is_untranslatable + in: query + required: false + schema: + type: boolean + - name: is_failed + in: query + required: false + schema: + type: boolean + responses: + 200: + description: Indicates dashboards have been retrieved correctly. + content: + application/json: + schema: + type: object + required: + - total + - data + properties: + total: + type: number + description: The total number of rules in migration. + data: + type: array + items: + $ref: '../../dashboard_migration.schema.yaml#/components/schemas/DashboardMigrationDashboard' /internal/siem_migrations/dashboards/{migration_id}/start: post: diff --git a/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/rules/types.ts b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/rules/types.ts index d284e61b36963..571e2ad67810a 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/rules/types.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/rules/types.ts @@ -8,9 +8,6 @@ import type { SiemMigrationFilters } from '../types'; export interface RuleMigrationFilters extends SiemMigrationFilters { - searchTerm?: string; - installed?: boolean; - installable?: boolean; prebuilt?: boolean; missingIndex?: boolean; } diff --git a/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/types.ts b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/types.ts index db65f22dcdfb4..cec5172ae9db5 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/types.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/types.ts @@ -24,6 +24,9 @@ export interface SiemMigrationFilters { fullyTranslated?: boolean; partiallyTranslated?: boolean; untranslatable?: boolean; + searchTerm?: string; + installed?: boolean; + installable?: boolean; } export type SiemMigrationVendor = OriginalRuleVendor | OriginalDashboardVendor; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/common/api/util/audit.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/common/api/util/audit.ts index 4e58fb3185365..247676fb9e1cf 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/common/api/util/audit.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/common/api/util/audit.ts @@ -15,7 +15,7 @@ export enum SiemMigrationsAuditActions { SIEM_MIGRATION_RETRIEVED = 'siem_migration_retrieved', SIEM_MIGRATION_DELETED = 'siem_migration_deleted', SIEM_MIGRATION_ADDED_RULES = 'siem_migration_added_rules', - SIEM_MIGRATION_RETRIEVED_RULES = 'siem_migration_retrieved_rules', + SIEM_MIGRATION_RETRIEVED_ITEMS = 'siem_migration_retrieved_items', SIEM_MIGRATION_UPLOADED_RESOURCES = 'siem_migration_uploaded_resources', SIEM_MIGRATION_RETRIEVED_RESOURCES = 'siem_migration_retrieved_resources', SIEM_MIGRATION_STARTED = 'siem_migration_started', @@ -62,7 +62,7 @@ export const siemMigrationAuditEventType: Record< [SiemMigrationsAuditActions.SIEM_MIGRATION_UPDATED_RULE]: AUDIT_TYPE.CHANGE, [SiemMigrationsAuditActions.SIEM_MIGRATION_INSTALLED_RULES]: AUDIT_TYPE.CREATION, [SiemMigrationsAuditActions.SIEM_MIGRATION_ADDED_RULES]: AUDIT_TYPE.CREATION, - [SiemMigrationsAuditActions.SIEM_MIGRATION_RETRIEVED_RULES]: AUDIT_TYPE.ACCESS, + [SiemMigrationsAuditActions.SIEM_MIGRATION_RETRIEVED_ITEMS]: AUDIT_TYPE.ACCESS, [SiemMigrationsAuditActions.SIEM_MIGRATION_DELETED]: AUDIT_TYPE.CHANGE, [SiemMigrationsAuditActions.SIEM_MIGRATION_RETRIEVED_INTEGRATIONS_STATS]: AUDIT_TYPE.ACCESS, [SiemMigrationsAuditActions.SIEM_MIGRATION_ADDED_DASHBOARDS]: AUDIT_TYPE.CREATION, @@ -161,11 +161,11 @@ export class SiemMigrationAuditLogger { }); } - public async logGetMigrationRules(params: { migrationId: string; error?: Error }): Promise { + public async logGetMigrationItems(params: { migrationId: string; error?: Error }): Promise { const { migrationId, error } = params; - const message = `User retrieved rules for SIEM migration with [id=${migrationId}]`; + const message = `User retrieved items for SIEM migration with [id=${migrationId}]`; return this.log({ - action: SiemMigrationsAuditActions.SIEM_MIGRATION_RETRIEVED_RULES, + action: SiemMigrationsAuditActions.SIEM_MIGRATION_RETRIEVED_ITEMS, message, error, }); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/common/data/siem_migrations_data_item_client.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/common/data/siem_migrations_data_item_client.ts index 959db38dac508..8f71eda10a7aa 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/common/data/siem_migrations_data_item_client.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/common/data/siem_migrations_data_item_client.ts @@ -27,6 +27,8 @@ import type { SiemMigrationAllDataStats, SiemMigrationDataStats, SiemMigrationFilters, + SiemMigrationGetItemsOptions, + SiemMigrationSort, } from './types'; import { dsl } from './dsl_queries'; @@ -35,18 +37,6 @@ export type CreateMigrationItemInput = Omit< '@timestamp' | 'id' | 'status' | 'created_by' | 'updated_by' | 'updated_at' >; -export interface SiemMigrationItemSort { - sortField?: string; - sortDirection?: estypes.SortOrder; -} - -export interface SiemMigrationGetItemsOptions { - filters?: F; - sort?: SiemMigrationItemSort; - from?: number; - size?: number; -} - /* BULK_MAX_SIZE defines the number to break down the bulk operations by. * The 500 number was chosen as a reasonable number to avoid large payloads. It can be adjusted if needed. */ const BULK_MAX_SIZE = 500 as const; @@ -373,5 +363,5 @@ export abstract class SiemMigrationsDataItemClient< return { bool: { filter } }; } - protected abstract getSortOptions(sort?: SiemMigrationItemSort): estypes.Sort; + protected abstract getSortOptions(sort?: SiemMigrationSort): estypes.Sort; } diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/common/data/sort.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/common/data/sort.ts new file mode 100644 index 0000000000000..f18881c7eeea1 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/common/data/sort.ts @@ -0,0 +1,69 @@ +/* + * 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 { estypes } from '@elastic/elasticsearch'; + +export type SiemMigrationSortHandler = ( + direction?: estypes.SortOrder +) => estypes.SortCombinations[]; + +export const commonSortingOptions = { + translationResult(direction: estypes.SortOrder = 'asc'): estypes.SortCombinations[] { + const field = 'translation_result'; + return [ + { + _script: { + order: direction, + type: 'number', + script: { + source: ` + if (doc.containsKey('${field}') && !doc['${field}'].empty) { + def value = doc['${field}'].value.toLowerCase(); + if (value == 'full') { return 2 } + if (value == 'partial') { return 1 } + if (value == 'untranslatable') { return 0 } + } + return -1; + `, + lang: 'painless', + }, + }, + }, + ]; + }, + + updated(direction: estypes.SortOrder = 'asc'): estypes.SortCombinations[] { + return [{ updated_at: direction }]; + }, +}; + +/** + * Sort Direction `asc` - Missing values come last + * Sort Direction `desc` - Missing values come first + * + * values that exist can be distinct but are treated as same. + * + * */ +export const getFieldExistenceSort = (field: string): SiemMigrationSortHandler => { + return (direction: estypes.SortOrder = 'asc') => [ + { + _script: { + order: direction, + type: 'number', + script: { + source: ` + if (doc.containsKey('${field}') && !doc['${field}'].empty) { + return 0; + } + return 1; + `, + lang: 'painless', + }, + }, + }, + ]; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/common/data/types.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/common/data/types.ts index 6a05babd54c0e..b4cf873db70f4 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/common/data/types.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/common/data/types.ts @@ -5,6 +5,7 @@ * 2.0. */ +import type { estypes } from '@elastic/elasticsearch'; import type { SiemMigrationStatus } from '../../../../../common/siem_migrations/constants'; import type { DashboardMigrationTaskStats } from '../../../../../common/siem_migrations/model/dashboard_migration.gen'; import type { RuleMigrationTaskStats } from '../../../../../common/siem_migrations/model/rule_migration.gen'; @@ -26,3 +27,15 @@ export interface SiemMigrationFilters { untranslatable?: boolean; searchTerm?: string; } + +export interface SiemMigrationSort { + sortField?: string; + sortDirection?: estypes.SortOrder; +} + +export interface SiemMigrationGetItemsOptions { + filters?: F; + sort?: SiemMigrationSort; + from?: number; + size?: number; +} diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/dashboards/api/dashboards/create.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/dashboards/api/dashboards/create.ts index b06e1869b0ecb..b4b5f2c8c9218 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/dashboards/api/dashboards/create.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/dashboards/api/dashboards/create.ts @@ -94,8 +94,6 @@ export const registerSiemDashboardMigrationsCreateDashboardsRoute = ( count: originalDashboardsCount, }); - logger.error(JSON.stringify({ items, originalDashboardsExport }, null, 2)); - const resourceIdentifier = new DashboardResourceIdentifier( items[0].original_dashboard.vendor ); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/dashboards/api/dashboards/get.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/dashboards/api/dashboards/get.ts new file mode 100644 index 0000000000000..060811ab2a6a4 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/dashboards/api/dashboards/get.ts @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; +import type { IKibanaResponse, Logger } from '@kbn/core/server'; +import type { DashboardMigrationGetDashboardOptions } from '../../../../../../common/siem_migrations/dashboards/types'; +import type { GetDashboardMigrationDashboardsResponse } from '../../../../../../common/siem_migrations/model/api/dashboards/dashboard_migration.gen'; +import { + GetDashboardMigrationDashboardsRequestParams, + GetDashboardMigrationDashboardsRequestQuery, +} from '../../../../../../common/siem_migrations/model/api/dashboards/dashboard_migration.gen'; +import { SIEM_DASHBOARD_MIGRATION_DASHBOARDS_PATH } from '../../../../../../common/siem_migrations/dashboards/constants'; +import type { SecuritySolutionPluginRouter } from '../../../../../types'; +import { authz } from '../../../common/api/util/authz'; +import { withLicense } from '../../../common/api/util/with_license'; +import { withExistingDashboardMigration } from '../util/with_existing_dashboard_migration'; +import { SiemMigrationAuditLogger } from '../../../common/api/util/audit'; + +export const registerSiemDashboardMigrationsGetDashboardsRoute = ( + router: SecuritySolutionPluginRouter, + logger: Logger +) => { + router.versioned + .get({ + path: SIEM_DASHBOARD_MIGRATION_DASHBOARDS_PATH, + access: 'internal', + security: { authz }, + }) + .addVersion( + { + version: '1', + validate: { + request: { + params: buildRouteValidationWithZod(GetDashboardMigrationDashboardsRequestParams), + query: buildRouteValidationWithZod(GetDashboardMigrationDashboardsRequestQuery), + }, + }, + }, + withLicense( + withExistingDashboardMigration( + async ( + context, + req, + res + ): Promise> => { + const { migration_id: migrationId } = req.params; + + const siemMigrationAuditLogger = new SiemMigrationAuditLogger( + context.securitySolution, + 'dashboards' + ); + try { + const ctx = await context.resolve(['securitySolution']); + const dashboardMigrationsClient = + ctx.securitySolution.siemMigrations.getDashboardsClient(); + + const { page, per_page: size } = req.query; + const options: DashboardMigrationGetDashboardOptions = { + filters: { + searchTerm: req.query.search_term, + ids: req.query.ids, + installed: req.query.is_installed, + fullyTranslated: req.query.is_fully_translated, + partiallyTranslated: req.query.is_partially_translated, + untranslatable: req.query.is_untranslatable, + failed: req.query.is_failed, + }, + sort: { sortField: req.query.sort_field, sortDirection: req.query.sort_direction }, + size, + from: page && size ? page * size : 0, + }; + + const result = await dashboardMigrationsClient.data.items.get(migrationId, options); + + await siemMigrationAuditLogger.logGetMigrationItems({ migrationId }); + return res.ok({ body: result }); + } catch (error) { + logger.error(error); + await siemMigrationAuditLogger.logGetMigrationItems({ migrationId, error }); + return res.badRequest({ body: error.message }); + } + } + ) + ) + ); +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/dashboards/api/index.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/dashboards/api/index.ts index ae2ad65449231..9dc627d2fd984 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/dashboards/api/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/dashboards/api/index.ts @@ -11,6 +11,7 @@ import { registerSiemDashboardMigrationsCreateDashboardsRoute } from './dashboar import { registerSiemDashboardMigrationsStatsRoute } from './stats'; import { registerSiemDashboardMigrationsGetRoute } from './get'; import { registerSiemDashboardMigrationsResourceGetMissingRoute } from './resources/missing'; +import { registerSiemDashboardMigrationsGetDashboardsRoute } from './dashboards/get'; export const registerSiemDashboardMigrationsRoutes = ( router: SecuritySolutionPluginRouter, @@ -25,6 +26,7 @@ export const registerSiemDashboardMigrationsRoutes = ( // ===== Dashboards ====== registerSiemDashboardMigrationsCreateDashboardsRoute(router, logger); + registerSiemDashboardMigrationsGetDashboardsRoute(router, logger); // ===== Resources ====== registerSiemDashboardMigrationsResourceGetMissingRoute(router, logger); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/dashboards/data/dashboard_migrations_data_dashboards_client.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/dashboards/data/dashboard_migrations_data_dashboards_client.ts index 5537d1f1e9d7e..011a7f4c53578 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/dashboards/data/dashboard_migrations_data_dashboards_client.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/dashboards/data/dashboard_migrations_data_dashboards_client.ts @@ -6,15 +6,38 @@ */ import type { estypes } from '@elastic/elasticsearch'; +import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; +import type { DashboardMigrationFilters } from '../../../../../common/siem_migrations/dashboards/types'; import type { DashboardMigrationDashboard } from '../../../../../common/siem_migrations/model/dashboard_migration.gen'; -import type { SiemMigrationItemSort } from '../../common/data/siem_migrations_data_item_client'; import { SiemMigrationsDataItemClient } from '../../common/data/siem_migrations_data_item_client'; +import type { SiemMigrationSort } from '../../common/data/types'; +import { dsl } from './dsl_queries'; +import { getSortingOptions } from './sort'; export class DashboardMigrationsDataDashboardsClient extends SiemMigrationsDataItemClient { protected type = 'dashboard' as const; - protected getSortOptions(sort: SiemMigrationItemSort = {}): estypes.Sort { - // TODO: implement sorting logic similar to getSortOptions in the rules client - return []; + protected getSortOptions(sort: SiemMigrationSort = {}): estypes.Sort { + return getSortingOptions(sort); + } + + protected getFilterQuery( + migrationId: string, + filters: DashboardMigrationFilters = {} + ): { bool: { filter: QueryDslQueryContainer[] } } { + const { filter } = super.getFilterQuery(migrationId, filters).bool; + + if (filters.searchTerm?.length) { + filter.push(dsl.matchTitle(filters.searchTerm)); + } + + if (filters.installed != null) { + filter.push(filters.installed ? dsl.isInstalled() : dsl.isNotInstalled()); + } + if (filters.installable != null) { + filter.push(...(filters.installable ? dsl.isInstallable() : dsl.isNotInstallable())); + } + + return { bool: { filter } }; } } diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/dashboards/data/dsl_queries.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/dashboards/data/dsl_queries.ts new file mode 100644 index 0000000000000..e51787d2ec8d2 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/dashboards/data/dsl_queries.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 type { QueryDslQueryContainer } from '@kbn/data-views-plugin/common/types'; +import { dsl as genericDsl } from '../../common/data/dsl_queries'; + +export const dsl = { + matchTitle(title: string): QueryDslQueryContainer { + return { match: { 'original_dashboard.title': title } }; + }, + + isInstalled(): QueryDslQueryContainer { + return { exists: { field: 'elastic_dashboard.id' } }; + }, + isNotInstalled(): QueryDslQueryContainer { + return { bool: { must_not: dsl.isInstalled() } }; + }, + + isInstallable(): QueryDslQueryContainer[] { + return [genericDsl.isFullyTranslated(), dsl.isNotInstalled()]; + }, + + isNotInstallable(): QueryDslQueryContainer[] { + return [genericDsl.isNotFullyTranslated(), dsl.isInstalled()]; + }, +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/dashboards/data/sort.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/dashboards/data/sort.ts new file mode 100644 index 0000000000000..8af2761c66fe8 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/dashboards/data/sort.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { estypes } from '@elastic/elasticsearch'; +import type { SiemMigrationSort } from '../../common/data/types'; +import type { SiemMigrationSortHandler } from '../../common/data/sort'; +import { commonSortingOptions, getFieldExistenceSort } from '../../common/data/sort'; + +const sortOptions = { + ...commonSortingOptions, + name(direction: estypes.SortOrder = 'asc'): estypes.SortCombinations[] { + return [{ 'original_dashboard.title.keyword': direction }]; + }, + installedDashboardId(direction: estypes.SortOrder = 'asc'): estypes.SortCombinations[] { + const field = 'elastic_dashboard.id'; + return getFieldExistenceSort(field)(direction); + }, + originalDashboardLastUpdate(direction: estypes.SortOrder = 'desc'): estypes.SortCombinations[] { + return [{ 'original_dashboard.last_updated': direction }]; + }, + splunkApp(direction: estypes.SortOrder = 'asc'): estypes.SortCombinations[] { + return [{ 'original_dashboard.splunk_properties.app': direction }]; + }, +}; + +const DEFAULT_SORT: estypes.Sort = [ + ...sortOptions.translationResult('desc'), + ...sortOptions.installedDashboardId('desc'), + ...sortOptions.name('asc'), +]; + +const sortOptionsMap: { + [key: string]: SiemMigrationSortHandler; +} = { + 'original_dashboard.title': sortOptions.name, + 'original_dashboard.last_updated': (direction?: estypes.SortOrder) => [ + ...sortOptions.originalDashboardLastUpdate(direction), + ...sortOptions.translationResult(direction), + ...sortOptions.installedDashboardId(direction), + ], + 'original_dashboard.splunk_properties.app': sortOptions.splunkApp, + updated: sortOptions.updated, +}; + +export const getSortingOptions = (sort?: SiemMigrationSort): estypes.Sort => { + if (!sort?.sortField) { + return DEFAULT_SORT; + } + + return sortOptionsMap[sort.sortField]?.(sort.sortDirection) ?? DEFAULT_SORT; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/rules/get.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/rules/get.ts index 4477d465940a0..d96eb83a2f7a6 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/rules/get.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/rules/get.ts @@ -73,11 +73,11 @@ export const registerSiemRuleMigrationsGetRulesRoute = ( const result = await ruleMigrationsClient.data.items.get(migrationId, options); - await siemMigrationAuditLogger.logGetMigrationRules({ migrationId }); + await siemMigrationAuditLogger.logGetMigrationItems({ migrationId }); return res.ok({ body: result }); } catch (error) { logger.error(error); - await siemMigrationAuditLogger.logGetMigrationRules({ migrationId, error }); + await siemMigrationAuditLogger.logGetMigrationItems({ migrationId, error }); return res.badRequest({ body: error.message }); } } diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_rules_client.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_rules_client.ts index 1050188d0557e..904e57423ec83 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_rules_client.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_rules_client.ts @@ -15,8 +15,8 @@ import type { import type { estypes } from '@elastic/elasticsearch'; import type { RuleMigrationFilters } from '../../../../../common/siem_migrations/rules/types'; import { - SiemMigrationStatus, SIEM_RULE_MIGRATION_INDEX_PATTERN_PLACEHOLDER, + SiemMigrationStatus, } from '../../../../../common/siem_migrations/constants'; import { type RuleMigrationTaskStats, @@ -24,25 +24,17 @@ import { type RuleMigrationAllIntegrationsStats, type RuleMigrationRule, } from '../../../../../common/siem_migrations/model/rule_migration.gen'; -import { getSortingOptions, type RuleMigrationSort } from './sort'; -import { MAX_ES_SEARCH_SIZE } from '../constants'; +import { getSortingOptions } from './sort'; import { dsl } from './dsl_queries'; -import type { - CreateMigrationItemInput, - SiemMigrationItemSort, -} from '../../common/data/siem_migrations_data_item_client'; +import { MAX_ES_SEARCH_SIZE } from '../constants'; +import type { CreateMigrationItemInput } from '../../common/data/siem_migrations_data_item_client'; import { SiemMigrationsDataItemClient } from '../../common/data/siem_migrations_data_item_client'; - +import type { SiemMigrationGetItemsOptions, SiemMigrationSort } from '../../common/data/types'; export type CreateRuleMigrationRulesInput = CreateMigrationItemInput; export type RuleMigrationDataStats = Omit; export type RuleMigrationAllDataStats = RuleMigrationDataStats[]; -export interface RuleMigrationGetRulesOptions { - filters?: RuleMigrationFilters; - sort?: RuleMigrationSort; - from?: number; - size?: number; -} +export type RuleMigrationGetRulesOptions = SiemMigrationGetItemsOptions; export class RuleMigrationsDataRulesClient extends SiemMigrationsDataItemClient { protected type = 'rule' as const; @@ -145,7 +137,7 @@ export class RuleMigrationsDataRulesClient extends SiemMigrationsDataItemClient< return { bool: { filter } }; } - protected getSortOptions(sort: SiemMigrationItemSort = {}): estypes.Sort { + protected getSortOptions(sort: SiemMigrationSort = {}): estypes.Sort { return getSortingOptions(sort); } diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/sort.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/sort.ts index 4d5ab4d194a77..564cb74dbaa8d 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/sort.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/sort.ts @@ -6,16 +6,18 @@ */ import type { estypes } from '@elastic/elasticsearch'; - -export interface RuleMigrationSort { - sortField?: string; - sortDirection?: estypes.SortOrder; -} +import type { SiemMigrationSort } from '../../common/data/types'; +import type { SiemMigrationSortHandler } from '../../common/data/sort'; +import { commonSortingOptions, getFieldExistenceSort } from '../../common/data/sort'; const sortMissingValue = (direction: estypes.SortOrder = 'asc') => direction === 'desc' ? '_last' : '_first'; const sortingOptions = { + ...commonSortingOptions, + installedRuleId(direction: estypes.SortOrder = 'asc'): estypes.SortCombinations[] { + return getFieldExistenceSort('elastic_rule.id')(direction); + }, matchedPrebuiltRule(direction: estypes.SortOrder = 'asc'): estypes.SortCombinations[] { return [ { @@ -53,45 +55,6 @@ const sortingOptions = { riskScore(direction: estypes.SortOrder = 'asc'): estypes.SortCombinations[] { return [{ 'elastic_rule.risk_score': direction }]; }, - status(direction: estypes.SortOrder = 'asc'): estypes.SortCombinations[] { - const field = 'translation_result'; - const installedRuleField = 'elastic_rule.id'; - return [ - { - _script: { - order: direction, - type: 'number', - script: { - source: ` - if (doc.containsKey('${field}') && !doc['${field}'].empty) { - def value = doc['${field}'].value.toLowerCase(); - if (value == 'full') { return 2 } - if (value == 'partial') { return 1 } - if (value == 'untranslatable') { return 0 } - } - return -1; - `, - lang: 'painless', - }, - }, - }, - { - _script: { - order: direction, - type: 'number', - script: { - source: ` - if (doc.containsKey('${installedRuleField}') && !doc['${installedRuleField}'].empty) { - return 0; - } - return -1; - `, - lang: 'painless', - }, - }, - }, - ]; - }, updated(direction: estypes.SortOrder = 'asc'): estypes.SortCombinations[] { return [{ updated_at: direction }]; }, @@ -101,7 +64,8 @@ const sortingOptions = { }; const DEFAULT_SORTING: estypes.Sort = [ - ...sortingOptions.status('desc'), + ...sortingOptions.translationResult('desc'), + ...sortingOptions.installedRuleId('desc'), ...sortingOptions.matchedPrebuiltRule('desc'), ...sortingOptions.severity(), ...sortingOptions.riskScore('desc'), @@ -109,29 +73,30 @@ const DEFAULT_SORTING: estypes.Sort = [ ]; const sortingOptionsMap: { - [key: string]: (direction?: estypes.SortOrder) => estypes.SortCombinations[]; + [key: string]: SiemMigrationSortHandler; } = { 'elastic_rule.title': sortingOptions.name, 'elastic_rule.severity': (direction?: estypes.SortOrder) => [ ...sortingOptions.severity(direction), ...sortingOptions.riskScore(direction), - ...sortingOptions.status('desc'), + ...sortingOptions.translationResult('desc'), + ...sortingOptions.installedRuleId('desc'), ...sortingOptions.matchedPrebuiltRule('desc'), ], 'elastic_rule.risk_score': (direction?: estypes.SortOrder) => [ ...sortingOptions.riskScore(direction), ...sortingOptions.severity(direction), - ...sortingOptions.status('desc'), + ...sortingOptions.translationResult('desc'), ...sortingOptions.matchedPrebuiltRule('desc'), ], 'elastic_rule.prebuilt_rule_id': (direction?: estypes.SortOrder) => [ ...sortingOptions.matchedPrebuiltRule(direction), - ...sortingOptions.status('desc'), + ...sortingOptions.translationResult('desc'), ...sortingOptions.severity('desc'), ...sortingOptions.riskScore(direction), ], translation_result: (direction?: estypes.SortOrder) => [ - ...sortingOptions.status(direction), + ...sortingOptions.translationResult(direction), ...sortingOptions.matchedPrebuiltRule('desc'), ...sortingOptions.severity('desc'), ...sortingOptions.riskScore(direction), @@ -139,7 +104,7 @@ const sortingOptionsMap: { updated_at: sortingOptions.updated, }; -export const getSortingOptions = (sort?: RuleMigrationSort): estypes.Sort => { +export const getSortingOptions = (sort?: SiemMigrationSort): estypes.Sort => { if (!sort?.sortField) { return DEFAULT_SORTING; } diff --git a/x-pack/solutions/security/test/api_integration/services/security_solution_api.gen.ts b/x-pack/solutions/security/test/api_integration/services/security_solution_api.gen.ts index a53dd615f1246..3027ce2c4aeea 100644 --- a/x-pack/solutions/security/test/api_integration/services/security_solution_api.gen.ts +++ b/x-pack/solutions/security/test/api_integration/services/security_solution_api.gen.ts @@ -85,6 +85,10 @@ import type { FindAssetCriticalityRecordsRequestQueryInput } from '@kbn/security import type { FindRulesRequestQueryInput } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_management/find_rules/find_rules_route.gen'; import type { GetAssetCriticalityRecordRequestQueryInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/asset_criticality/get_asset_criticality.gen'; import type { GetDashboardMigrationRequestParamsInput } from '@kbn/security-solution-plugin/common/siem_migrations/model/api/dashboards/dashboard_migration.gen'; +import type { + GetDashboardMigrationDashboardsRequestQueryInput, + GetDashboardMigrationDashboardsRequestParamsInput, +} from '@kbn/security-solution-plugin/common/siem_migrations/model/api/dashboards/dashboard_migration.gen'; import type { GetDashboardMigrationResourcesRequestQueryInput, GetDashboardMigrationResourcesRequestParamsInput, @@ -1038,6 +1042,28 @@ finalize it. .set(ELASTIC_HTTP_VERSION_HEADER, '1') .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana'); }, + /** + * Retrieves the dashboards added to an existing dashboard migration + */ + getDashboardMigrationDashboards( + props: GetDashboardMigrationDashboardsProps, + kibanaSpace: string = 'default' + ) { + return supertest + .get( + getRouteUrlForSpace( + replaceParams( + '/internal/siem_migrations/dashboards/{migration_id}/dashboards', + props.params + ), + kibanaSpace + ) + ) + .set('kbn-xsrf', 'true') + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .query(props.query); + }, /** * Retrieves resources for an existing SIEM dashboards migration */ @@ -2380,6 +2406,10 @@ export interface GetAssetCriticalityRecordProps { export interface GetDashboardMigrationProps { params: GetDashboardMigrationRequestParamsInput; } +export interface GetDashboardMigrationDashboardsProps { + query: GetDashboardMigrationDashboardsRequestQueryInput; + params: GetDashboardMigrationDashboardsRequestParamsInput; +} export interface GetDashboardMigrationResourcesProps { query: GetDashboardMigrationResourcesRequestQueryInput; params: GetDashboardMigrationResourcesRequestParamsInput; diff --git a/x-pack/solutions/security/test/security_solution_api_integration/test_suites/siem_migrations/rules/trial_license_complete_tier/dashboard_migrations/dashboards/get.ts b/x-pack/solutions/security/test/security_solution_api_integration/test_suites/siem_migrations/rules/trial_license_complete_tier/dashboard_migrations/dashboards/get.ts new file mode 100644 index 0000000000000..5abf6014259e0 --- /dev/null +++ b/x-pack/solutions/security/test/security_solution_api_integration/test_suites/siem_migrations/rules/trial_license_complete_tier/dashboard_migrations/dashboards/get.ts @@ -0,0 +1,343 @@ +/* + * 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 type { FtrProviderContext } from '../../../../../../ftr_provider_context'; +import { getDefaultDashboardMigrationDocumentWithOverrides } from '../../../../utils/dashboard_mocks'; +import { dashboardMigrationRouteFactory } from '../../../../utils/dashboards'; +import { + deleteAllDashboardMigrations, + indexMigrationDashboards, +} from '../../../../utils/es_queries_dashboards'; + +export default ({ getService }: FtrProviderContext) => { + const es = getService('es'); + const supertest = getService('supertest'); + + const dashboardMigrationRoutes = dashboardMigrationRouteFactory(supertest); + + describe('@ess @serverless @serverlessQA Get Dashboards API', () => { + let migrationId: string; + let indexedIds: string[]; + beforeEach(async () => { + deleteAllDashboardMigrations(es); + const migrationResponse = await dashboardMigrationRoutes.create({}); + migrationId = migrationResponse.body.migration_id; + + const candidateDashboard1 = getDefaultDashboardMigrationDocumentWithOverrides({ + original_dashboard: { + title: 'Dashboard 1 - First', + last_updated: '2023-11-09T12:00:00Z', + splunk_properties: { + app: 'first', + }, + }, + elastic_dashboard: { + id: 'some-installed-id', + }, + status: 'completed', + migration_id: migrationId, + translation_result: 'full', + }); + + const candidateDashboard2 = getDefaultDashboardMigrationDocumentWithOverrides({ + original_dashboard: { + title: 'Dashboard 2 - Second', + last_updated: '2023-11-10T12:00:00Z', + splunk_properties: { + app: 'second', + }, + }, + migration_id: migrationId, + translation_result: 'partial', + status: 'failed', + }); + + indexedIds = await indexMigrationDashboards(es, [candidateDashboard1, candidateDashboard2]); + }); + + it('should fetch existing rules for a given migrationId', async () => { + const response = await dashboardMigrationRoutes.getDashboards({ + migrationId, + }); + expect(response.body).toEqual({ + total: 2, + data: [ + expect.objectContaining({ + original_dashboard: expect.objectContaining({ + title: 'Dashboard 1 - First', + }), + }), + expect.objectContaining({ + original_dashboard: expect.objectContaining({ + title: 'Dashboard 2 - Second', + }), + }), + ], + }); + }); + + describe('Filtering', () => { + it('should filter by search term', async () => { + const response = await dashboardMigrationRoutes.getDashboards({ + migrationId, + queryParams: { + search_term: 'First', + }, + }); + expect(response.body).toEqual({ + total: 1, + data: [ + expect.objectContaining({ + original_dashboard: expect.objectContaining({ + title: 'Dashboard 1 - First', + }), + }), + ], + }); + }); + + it('should filter by ids', async () => { + const response = await dashboardMigrationRoutes.getDashboards({ + migrationId, + queryParams: { + ids: [indexedIds[0]], + }, + }); + expect(response.body).toEqual({ + total: 1, + data: [ + expect.objectContaining({ + original_dashboard: expect.objectContaining({ + title: 'Dashboard 1 - First', + }), + }), + ], + }); + }); + + it('should filter by is_fully_translated', async () => { + const response = await dashboardMigrationRoutes.getDashboards({ + migrationId, + queryParams: { + is_fully_translated: true, + }, + }); + + expect(response.body).toEqual({ + total: 1, + data: [ + expect.objectContaining({ + original_dashboard: expect.objectContaining({ + title: 'Dashboard 1 - First', + }), + }), + ], + }); + }); + + it('should filter by is_partially_translated', async () => { + const response = await dashboardMigrationRoutes.getDashboards({ + migrationId, + queryParams: { + is_partially_translated: true, + }, + }); + + expect(response.body).toEqual({ + total: 1, + data: [ + expect.objectContaining({ + original_dashboard: expect.objectContaining({ + title: 'Dashboard 2 - Second', + }), + }), + ], + }); + }); + + it('should filter by installed', async () => { + const response = await dashboardMigrationRoutes.getDashboards({ + migrationId, + queryParams: { + is_installed: true, + }, + }); + + expect(response.body).toEqual({ + total: 1, + data: [ + expect.objectContaining({ + original_dashboard: expect.objectContaining({ + title: 'Dashboard 1 - First', + }), + }), + ], + }); + }); + + it('should filter by failed', async () => { + const response = await dashboardMigrationRoutes.getDashboards({ + migrationId, + queryParams: { + is_failed: true, + }, + }); + + expect(response.body).toEqual({ + total: 1, + data: [ + expect.objectContaining({ + original_dashboard: expect.objectContaining({ + title: 'Dashboard 2 - Second', + }), + }), + ], + }); + }); + + it('should filter by `untranslatable`', async () => { + const response = await dashboardMigrationRoutes.getDashboards({ + migrationId, + queryParams: { + is_untranslatable: true, + }, + }); + + expect(response.body).toEqual({ + total: 0, + data: [], + }); + }); + }); + describe('Sorting', () => { + it('should sort by title asending', async () => { + const response = await dashboardMigrationRoutes.getDashboards({ + migrationId, + queryParams: { + sort_field: 'original_dashboard.title', + sort_direction: 'asc', + }, + }); + + expect(response.body).toEqual({ + total: 2, + data: [ + expect.objectContaining({ + original_dashboard: expect.objectContaining({ + title: 'Dashboard 1 - First', + }), + }), + expect.objectContaining({ + original_dashboard: expect.objectContaining({ + title: 'Dashboard 2 - Second', + }), + }), + ], + }); + }); + + it('should sort by title descending', async () => { + const response = await dashboardMigrationRoutes.getDashboards({ + migrationId, + queryParams: { + sort_field: 'original_dashboard.title', + sort_direction: 'desc', + }, + }); + + expect(response.body).toEqual({ + total: 2, + data: [ + expect.objectContaining({ + original_dashboard: expect.objectContaining({ + title: 'Dashboard 2 - Second', + }), + }), + expect.objectContaining({ + original_dashboard: expect.objectContaining({ + title: 'Dashboard 1 - First', + }), + }), + ], + }); + }); + + it('should sort by splunk app correctly', async () => { + const response = await dashboardMigrationRoutes.getDashboards({ + migrationId, + queryParams: { + sort_field: 'original_dashboard.splunk_properties.app', + sort_direction: 'desc', + }, + }); + + expect(response.body).toEqual({ + total: 2, + data: [ + expect.objectContaining({ + original_dashboard: expect.objectContaining({ + title: 'Dashboard 2 - Second', + }), + }), + expect.objectContaining({ + original_dashboard: expect.objectContaining({ + title: 'Dashboard 1 - First', + }), + }), + ], + }); + }); + }); + describe('Pagination', () => { + it('should paginate correctly', async () => { + const response = await dashboardMigrationRoutes.getDashboards({ + migrationId, + queryParams: { + page: 1, + per_page: 1, + sort_field: 'original_dashboard.title', + sort_direction: 'asc', + }, + }); + + expect(response.body).toEqual({ + total: 2, + data: [ + expect.objectContaining({ + original_dashboard: expect.objectContaining({ + title: 'Dashboard 2 - Second', + }), + }), + ], + }); + }); + + it('should return first page if only per_page is provided', async () => { + const response = await dashboardMigrationRoutes.getDashboards({ + migrationId, + queryParams: { + per_page: 1, + sort_field: 'original_dashboard.title', + sort_direction: 'asc', + }, + }); + + expect(response.body).toEqual({ + total: 2, + data: [ + expect.objectContaining({ + original_dashboard: expect.objectContaining({ + title: 'Dashboard 1 - First', + }), + }), + ], + }); + }); + }); + }); +}; diff --git a/x-pack/solutions/security/test/security_solution_api_integration/test_suites/siem_migrations/rules/trial_license_complete_tier/dashboard_migrations/index.ts b/x-pack/solutions/security/test/security_solution_api_integration/test_suites/siem_migrations/rules/trial_license_complete_tier/dashboard_migrations/index.ts index fe1ea96e40c1f..63bfceff3be21 100644 --- a/x-pack/solutions/security/test/security_solution_api_integration/test_suites/siem_migrations/rules/trial_license_complete_tier/dashboard_migrations/index.ts +++ b/x-pack/solutions/security/test/security_solution_api_integration/test_suites/siem_migrations/rules/trial_license_complete_tier/dashboard_migrations/index.ts @@ -14,5 +14,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./get')); loadTestFile(require.resolve('./dashboards/create')); loadTestFile(require.resolve('./resources/missing')); + loadTestFile(require.resolve('./dashboards/get')); }); } diff --git a/x-pack/solutions/security/test/security_solution_api_integration/test_suites/siem_migrations/utils/common.ts b/x-pack/solutions/security/test/security_solution_api_integration/test_suites/siem_migrations/utils/common.ts new file mode 100644 index 0000000000000..83c96c608b864 --- /dev/null +++ b/x-pack/solutions/security/test/security_solution_api_integration/test_suites/siem_migrations/utils/common.ts @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + MigrationTranslationResult, + SiemMigrationStatus, +} from '@kbn/security-solution-plugin/common/siem_migrations/constants'; +import type { RuleMigrationRuleData } from '@kbn/security-solution-plugin/common/siem_migrations/model/rule_migration.gen'; + +export const statsOverrideCallbackFactory = ({ + migrationId, + failed = 0, + pending = 0, + processing = 0, + completed = 0, + fullyTranslated = 0, + partiallyTranslated = 0, +}: { + migrationId: string; + failed?: number; + pending?: number; + processing?: number; + completed?: number; + fullyTranslated?: number; + partiallyTranslated?: number; +}) => { + const overrideCallback = ( + index: number + ): Pick => { + let translationResult; + let status = SiemMigrationStatus.PENDING; + + const pendingEndIndex = failed + pending; + const processingEndIndex = failed + pending + processing; + const completedEndIndex = failed + pending + processing + completed; + if (index < failed) { + status = SiemMigrationStatus.FAILED; + } else if (index < pendingEndIndex) { + status = SiemMigrationStatus.PENDING; + } else if (index < processingEndIndex) { + status = SiemMigrationStatus.PROCESSING; + } else if (index < completedEndIndex) { + status = SiemMigrationStatus.COMPLETED; + const fullyTranslatedEndIndex = completedEndIndex - completed + fullyTranslated; + const partiallyTranslatedEndIndex = + completedEndIndex - completed + fullyTranslated + partiallyTranslated; + if (index < fullyTranslatedEndIndex) { + translationResult = MigrationTranslationResult.FULL; + } else if (index < partiallyTranslatedEndIndex) { + translationResult = MigrationTranslationResult.PARTIAL; + } else { + translationResult = MigrationTranslationResult.UNTRANSLATABLE; + } + } + return { + migration_id: migrationId, + translation_result: translationResult, + status, + }; + }; + return overrideCallback; +}; diff --git a/x-pack/solutions/security/test/security_solution_api_integration/test_suites/siem_migrations/utils/dashboard_mocks.ts b/x-pack/solutions/security/test/security_solution_api_integration/test_suites/siem_migrations/utils/dashboard_mocks.ts index b6e57094e3a7b..a13edaf379039 100644 --- a/x-pack/solutions/security/test/security_solution_api_integration/test_suites/siem_migrations/utils/dashboard_mocks.ts +++ b/x-pack/solutions/security/test/security_solution_api_integration/test_suites/siem_migrations/utils/dashboard_mocks.ts @@ -5,6 +5,23 @@ * 2.0. */ +import type { DashboardMigrationDashboardData } from '@kbn/security-solution-plugin/common/siem_migrations/model/dashboard_migration.gen'; +import type { MigrationDocument } from '@kbn/security-solution-plugin/server/lib/siem_migrations/common/types'; +import { merge } from 'lodash'; +import type { DeepPartial } from 'utility-types'; + +export const getDefaultDashboardMigrationDoc: () => Omit = () => ({ + name: 'Default Migration', + created_by: 'elastic', + created_at: new Date().toISOString(), + last_execution: { + is_aborted: false, + started_at: new Date().toISOString(), + ended_at: null, + connector_id: 'preconfigured-bedrock', + }, +}); + export const defaultOriginalDashboardExports = { preview: false, result: { @@ -21,6 +38,49 @@ export const defaultOriginalDashboardExports = { }, }; +export const defaultDashboardMigrationDocument: DashboardMigrationDashboardData = { + migration_id: 'dac6570f-5f41-4e8e-972e-a1de368ee118', + original_dashboard: { + id: 'some_id', + title: 'Some Dashboard', + description: '', + data: '', + format: 'xml', + vendor: 'splunk', + last_updated: '1970-01-01T00:00:00+00:00', + splunk_properties: { + app: 'system', + owner: 'nobody', + sharing: 'system', + }, + }, + elastic_dashboard: { + title: 'Some Dashboard', + }, + '@timestamp': '2025-09-03T14:20:49.748Z', + status: 'pending', + updated_at: '2025-09-03T14:20:49.748Z', + translation_result: 'full', + created_by: 'elastic', +}; + +export const getDefaultDashboardMigrationDocumentWithOverrides = ( + overrides: DeepPartial +): DashboardMigrationDashboardData => { + const overrideWithElasticDashboardTitle = { + ...overrides, + elastic_dashboard: { + title: overrides.original_dashboard?.title, + ...overrides.elastic_dashboard, + }, + }; + + return merge( + structuredClone(defaultDashboardMigrationDocument), + overrideWithElasticDashboardTitle + ); +}; + export const splunkXMLWithMultipleQueries = `
diff --git a/x-pack/solutions/security/test/security_solution_api_integration/test_suites/siem_migrations/utils/dashboards.ts b/x-pack/solutions/security/test/security_solution_api_integration/test_suites/siem_migrations/utils/dashboards.ts index 0b0d03713f158..a64fd65507972 100644 --- a/x-pack/solutions/security/test/security_solution_api_integration/test_suites/siem_migrations/utils/dashboards.ts +++ b/x-pack/solutions/security/test/security_solution_api_integration/test_suites/siem_migrations/utils/dashboards.ts @@ -18,11 +18,15 @@ import { SIEM_DASHBOARD_MIGRATIONS_PATH, } from '@kbn/security-solution-plugin/common/siem_migrations/dashboards/constants'; import type { - CreateDashboardMigrationDashboardsRequestBody, - CreateDashboardMigrationRequestBody, - CreateDashboardMigrationResponse, - GetDashboardMigrationResourcesMissingResponse, - GetDashboardMigrationStatsResponse, + GetDashboardMigrationDashboardsRequestQuery, + GetDashboardMigrationDashboardsResponse, +} from '@kbn/security-solution-plugin/common/siem_migrations/model/api/dashboards/dashboard_migration.gen'; +import { + type CreateDashboardMigrationDashboardsRequestBody, + type CreateDashboardMigrationRequestBody, + type CreateDashboardMigrationResponse, + type GetDashboardMigrationResourcesMissingResponse, + type GetDashboardMigrationStatsResponse, } from '@kbn/security-solution-plugin/common/siem_migrations/model/api/dashboards/dashboard_migration.gen'; import type SuperTest from 'supertest'; import { replaceParams } from '@kbn/openapi-common/shared'; @@ -38,6 +42,10 @@ export type AddDashboardsToMigrationRequestBody = MigrationRequestParams & { expectedStatusCode?: number; }; +export type GetDashboardMigrationDashboardsParams = MigrationRequestParams & { + queryParams?: GetDashboardMigrationDashboardsRequestQuery; +}; + export const dashboardMigrationRouteFactory = (supertest: SuperTest.Agent) => { return { create: async ({ @@ -110,6 +118,28 @@ export const dashboardMigrationRouteFactory = (supertest: SuperTest.Agent) => { return response; }, + getDashboards: async ({ + migrationId, + queryParams = {}, + expectStatusCode = 200, + }: GetDashboardMigrationDashboardsParams): Promise<{ + body: GetDashboardMigrationDashboardsResponse; + }> => { + const url = replaceParams(SIEM_DASHBOARD_MIGRATION_DASHBOARDS_PATH, { + migration_id: migrationId, + }); + const response = await supertest + .get(url) + .query(queryParams) + .set('kbn-xsrf', 'true') + .set(ELASTIC_HTTP_VERSION_HEADER, API_VERSIONS.internal.v1) + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .send(); + + assertStatusCode(expectStatusCode, response); + return response; + }, + resources: { missing: async ({ migrationId, diff --git a/x-pack/solutions/security/test/security_solution_api_integration/test_suites/siem_migrations/utils/es_queries_dashboards.ts b/x-pack/solutions/security/test/security_solution_api_integration/test_suites/siem_migrations/utils/es_queries_dashboards.ts index 6350dbd3a3615..6840a26da6f49 100644 --- a/x-pack/solutions/security/test/security_solution_api_integration/test_suites/siem_migrations/utils/es_queries_dashboards.ts +++ b/x-pack/solutions/security/test/security_solution_api_integration/test_suites/siem_migrations/utils/es_queries_dashboards.ts @@ -6,7 +6,11 @@ */ import type { Client } from '@elastic/elasticsearch'; -import type { DashboardMigrationDashboard } from '@kbn/security-solution-plugin/common/siem_migrations/model/dashboard_migration.gen'; +import type { + DashboardMigrationDashboard, + DashboardMigrationDashboardData, +} from '@kbn/security-solution-plugin/common/siem_migrations/model/dashboard_migration.gen'; +import { getDefaultDashboardMigrationDoc } from './dashboard_mocks'; const SIEM_MIGRATIONS_DASHBOARDS_BASE_INDEX_PATTERN = `.kibana-siem-dashboard-migrations`; const MIGRATIONS_INDEX_PATTERN = `${SIEM_MIGRATIONS_DASHBOARDS_BASE_INDEX_PATTERN}-migrations-default`; @@ -76,3 +80,40 @@ export const deleteAllDashboardMigrations = async (es: Client): Promise => refresh: true, }); }; + +export const indexMigrationDashboards = async ( + es: Client, + dashboards: DashboardMigrationDashboardData[] +): Promise => { + const createdAt = new Date().toISOString(); + const addDashboardOperations = dashboards.flatMap((ruleMigration) => [ + { create: { _index: DASHBOARDS_INDEX_PATTERN } }, + { + ...ruleMigration, + '@timestamp': createdAt, + updated_at: createdAt, + }, + ]); + + const migrationIdsToBeCreated = new Set(dashboards.map((rule) => rule.migration_id)); + const createMigrationOperations = Array.from(migrationIdsToBeCreated).flatMap((migrationId) => [ + { create: { _index: MIGRATIONS_INDEX_PATTERN, _id: migrationId } }, + { + ...getDefaultDashboardMigrationDoc(), + }, + ]); + + const res = await es.bulk({ + refresh: 'wait_for', + operations: [...createMigrationOperations, ...addDashboardOperations], + }); + + const ids = res.items.reduce((acc, item) => { + if (item.create?._id && item.create._index === DASHBOARDS_INDEX_PATTERN) { + acc.push(item.create._id); + } + return acc; + }, [] as string[]); + + return ids; +}; diff --git a/x-pack/solutions/security/test/security_solution_api_integration/test_suites/siem_migrations/utils/index.ts b/x-pack/solutions/security/test/security_solution_api_integration/test_suites/siem_migrations/utils/index.ts index 2140c100f4e5a..804a874178f4f 100644 --- a/x-pack/solutions/security/test/security_solution_api_integration/test_suites/siem_migrations/utils/index.ts +++ b/x-pack/solutions/security/test/security_solution_api_integration/test_suites/siem_migrations/utils/index.ts @@ -5,6 +5,7 @@ * 2.0. */ +export * from './common'; export * from './mocks'; export * from './resources'; export * from './rules'; diff --git a/x-pack/solutions/security/test/security_solution_api_integration/test_suites/siem_migrations/utils/mocks.ts b/x-pack/solutions/security/test/security_solution_api_integration/test_suites/siem_migrations/utils/mocks.ts index 6c1df73be986e..43976fcf6ea21 100644 --- a/x-pack/solutions/security/test/security_solution_api_integration/test_suites/siem_migrations/utils/mocks.ts +++ b/x-pack/solutions/security/test/security_solution_api_integration/test_suites/siem_migrations/utils/mocks.ts @@ -6,10 +6,6 @@ */ import type { Client } from '@elastic/elasticsearch'; -import { - MigrationTranslationResult, - SiemMigrationStatus, -} from '@kbn/security-solution-plugin/common/siem_migrations/constants'; import type { ElasticRule, @@ -102,58 +98,6 @@ export const getMigrationRuleDocuments = ( return docs; }; -export const statsOverrideCallbackFactory = ({ - migrationId, - failed = 0, - pending = 0, - processing = 0, - completed = 0, - fullyTranslated = 0, - partiallyTranslated = 0, -}: { - migrationId: string; - failed?: number; - pending?: number; - processing?: number; - completed?: number; - fullyTranslated?: number; - partiallyTranslated?: number; -}) => { - const overrideCallback = (index: number): Partial => { - let translationResult; - let status = SiemMigrationStatus.PENDING; - - const pendingEndIndex = failed + pending; - const processingEndIndex = failed + pending + processing; - const completedEndIndex = failed + pending + processing + completed; - if (index < failed) { - status = SiemMigrationStatus.FAILED; - } else if (index < pendingEndIndex) { - status = SiemMigrationStatus.PENDING; - } else if (index < processingEndIndex) { - status = SiemMigrationStatus.PROCESSING; - } else if (index < completedEndIndex) { - status = SiemMigrationStatus.COMPLETED; - const fullyTranslatedEndIndex = completedEndIndex - completed + fullyTranslated; - const partiallyTranslatedEndIndex = - completedEndIndex - completed + fullyTranslated + partiallyTranslated; - if (index < fullyTranslatedEndIndex) { - translationResult = MigrationTranslationResult.FULL; - } else if (index < partiallyTranslatedEndIndex) { - translationResult = MigrationTranslationResult.PARTIAL; - } else { - translationResult = MigrationTranslationResult.UNTRANSLATABLE; - } - } - return { - migration_id: migrationId, - translation_result: translationResult, - status, - }; - }; - return overrideCallback; -}; - const getDefaultMigrationDoc: () => Omit = () => ({ name: 'Default Migration', created_by: SOME_USER_ID,