diff --git a/src/platform/packages/shared/kbn-alerts-as-data-utils/src/schemas/generated/security_attack_discovery_schema.ts b/src/platform/packages/shared/kbn-alerts-as-data-utils/src/schemas/generated/security_attack_discovery_schema.ts new file mode 100644 index 0000000000000..1749de0d7d5f1 --- /dev/null +++ b/src/platform/packages/shared/kbn-alerts-as-data-utils/src/schemas/generated/security_attack_discovery_schema.ts @@ -0,0 +1,145 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ +// ---------------------------------- WARNING ---------------------------------- +// this file was generated, and should not be edited by hand +// ---------------------------------- WARNING ---------------------------------- +import * as rt from 'io-ts'; +import type { Either } from 'fp-ts/lib/Either'; +import { AlertSchema } from './alert_schema'; +import { EcsSchema } from './ecs_schema'; +const ISO_DATE_PATTERN = /^d{4}-d{2}-d{2}Td{2}:d{2}:d{2}.d{3}Z$/; +export const IsoDateString = new rt.Type( + 'IsoDateString', + rt.string.is, + (input, context): Either => { + if (typeof input === 'string' && ISO_DATE_PATTERN.test(input)) { + return rt.success(input); + } else { + return rt.failure(input, context); + } + }, + rt.identity +); +export type IsoDateStringC = typeof IsoDateString; +export const schemaUnknown = rt.unknown; +export const schemaUnknownArray = rt.array(rt.unknown); +export const schemaString = rt.string; +export const schemaStringArray = rt.array(schemaString); +export const schemaNumber = rt.number; +export const schemaNumberArray = rt.array(schemaNumber); +export const schemaDate = rt.union([IsoDateString, schemaNumber]); +export const schemaDateArray = rt.array(schemaDate); +export const schemaDateRange = rt.partial({ + gte: schemaDate, + lte: schemaDate, +}); +export const schemaDateRangeArray = rt.array(schemaDateRange); +export const schemaStringOrNumber = rt.union([schemaString, schemaNumber]); +export const schemaStringOrNumberArray = rt.array(schemaStringOrNumber); +export const schemaBoolean = rt.boolean; +export const schemaBooleanArray = rt.array(schemaBoolean); +const schemaGeoPointCoords = rt.type({ + type: schemaString, + coordinates: schemaNumberArray, +}); +const schemaGeoPointString = schemaString; +const schemaGeoPointLatLon = rt.type({ + lat: schemaNumber, + lon: schemaNumber, +}); +const schemaGeoPointLocation = rt.type({ + location: schemaNumberArray, +}); +const schemaGeoPointLocationString = rt.type({ + location: schemaString, +}); +export const schemaGeoPoint = rt.union([ + schemaGeoPointCoords, + schemaGeoPointString, + schemaGeoPointLatLon, + schemaGeoPointLocation, + schemaGeoPointLocationString, +]); +export const schemaGeoPointArray = rt.array(schemaGeoPoint); +// prettier-ignore +const SecurityAttackDiscoveryAlertRequired = rt.type({ + '@timestamp': schemaDate, + 'kibana.alert.attack_discovery.alert_ids': schemaStringArray, + 'kibana.alert.attack_discovery.alerts_context_count': schemaNumber, + 'kibana.alert.attack_discovery.api_config': schemaUnknown, + 'kibana.alert.attack_discovery.details_markdown': schemaString, + 'kibana.alert.attack_discovery.details_markdown_with_replacements': schemaString, + 'kibana.alert.attack_discovery.summary_markdown': schemaString, + 'kibana.alert.attack_discovery.summary_markdown_with_replacements': schemaString, + 'kibana.alert.attack_discovery.title': schemaString, + 'kibana.alert.attack_discovery.title_with_replacements': schemaString, + 'kibana.alert.attack_discovery.users.id': schemaString, + 'kibana.alert.instance.id': schemaString, + 'kibana.alert.rule.category': schemaString, + 'kibana.alert.rule.consumer': schemaString, + 'kibana.alert.rule.name': schemaString, + 'kibana.alert.rule.producer': schemaString, + 'kibana.alert.rule.revision': schemaStringOrNumber, + 'kibana.alert.rule.rule_type_id': schemaString, + 'kibana.alert.rule.uuid': schemaString, + 'kibana.alert.status': schemaString, + 'kibana.alert.uuid': schemaString, + 'kibana.space_ids': schemaStringArray, +}); +// prettier-ignore +const SecurityAttackDiscoveryAlertOptional = rt.partial({ + 'event.action': schemaString, + 'event.kind': schemaString, + 'event.original': schemaString, + 'kibana.alert.action_group': schemaString, + 'kibana.alert.attack_discovery.api_config.model': schemaString, + 'kibana.alert.attack_discovery.api_config.provider': schemaString, + 'kibana.alert.attack_discovery.entity_summary_markdown': schemaString, + 'kibana.alert.attack_discovery.entity_summary_markdown_with_replacements': schemaString, + 'kibana.alert.attack_discovery.mitre_attack_tactics': schemaStringArray, + 'kibana.alert.attack_discovery.replacements': schemaUnknown, + 'kibana.alert.attack_discovery.user.id': schemaString, + 'kibana.alert.attack_discovery.users': rt.array( + rt.partial({ + name: schemaString, + }) + ), + 'kibana.alert.case_ids': schemaStringArray, + 'kibana.alert.consecutive_matches': schemaStringOrNumber, + 'kibana.alert.duration.us': schemaStringOrNumber, + 'kibana.alert.end': schemaDate, + 'kibana.alert.flapping': schemaBoolean, + 'kibana.alert.flapping_history': schemaBooleanArray, + 'kibana.alert.intended_timestamp': schemaDate, + 'kibana.alert.last_detected': schemaDate, + 'kibana.alert.maintenance_window_ids': schemaStringArray, + 'kibana.alert.pending_recovered_count': schemaStringOrNumber, + 'kibana.alert.previous_action_group': schemaString, + 'kibana.alert.reason': schemaString, + 'kibana.alert.risk_score': schemaNumber, + 'kibana.alert.rule.execution.timestamp': schemaDate, + 'kibana.alert.rule.execution.type': schemaString, + 'kibana.alert.rule.execution.uuid': schemaString, + 'kibana.alert.rule.parameters': schemaUnknown, + 'kibana.alert.rule.tags': schemaStringArray, + 'kibana.alert.severity_improving': schemaBoolean, + 'kibana.alert.start': schemaDate, + 'kibana.alert.time_range': schemaDateRange, + 'kibana.alert.url': schemaString, + 'kibana.alert.workflow_assignee_ids': schemaStringArray, + 'kibana.alert.workflow_status': schemaString, + 'kibana.alert.workflow_tags': schemaStringArray, + 'kibana.version': schemaString, + tags: schemaStringArray, +}); + +// prettier-ignore +export const SecurityAttackDiscoveryAlertSchema = rt.intersection([SecurityAttackDiscoveryAlertRequired, SecurityAttackDiscoveryAlertOptional, AlertSchema, EcsSchema]); +// prettier-ignore +export type SecurityAttackDiscoveryAlert = rt.TypeOf; diff --git a/src/platform/packages/shared/kbn-alerts-as-data-utils/src/schemas/index.ts b/src/platform/packages/shared/kbn-alerts-as-data-utils/src/schemas/index.ts index aec38bbf2f223..0ed860d25c21f 100644 --- a/src/platform/packages/shared/kbn-alerts-as-data-utils/src/schemas/index.ts +++ b/src/platform/packages/shared/kbn-alerts-as-data-utils/src/schemas/index.ts @@ -14,6 +14,7 @@ import type { ObservabilityMetricsAlert } from './generated/observability_metric import type { ObservabilitySloAlert } from './generated/observability_slo_schema'; import type { ObservabilityUptimeAlert } from './generated/observability_uptime_schema'; import type { SecurityAlert } from './generated/security_schema'; +import type { SecurityAttackDiscoveryAlert } from './generated/security_attack_discovery_schema'; import type { MlAnomalyDetectionAlert } from './generated/ml_anomaly_detection_schema'; import type { DefaultAlert } from './generated/default_schema'; import type { MlAnomalyDetectionHealthAlert } from './generated/ml_anomaly_detection_health_schema'; @@ -28,6 +29,7 @@ export type { ObservabilityMetricsAlert } from './generated/observability_metric export type { ObservabilitySloAlert } from './generated/observability_slo_schema'; export type { ObservabilityUptimeAlert } from './generated/observability_uptime_schema'; export type { SecurityAlert } from './generated/security_schema'; +export type { SecurityAttackDiscoveryAlert } from './generated/security_attack_discovery_schema'; export type { StackAlert } from './generated/stack_schema'; export type { MlAnomalyDetectionAlert } from './generated/ml_anomaly_detection_schema'; export type { MlAnomalyDetectionHealthAlert } from './generated/ml_anomaly_detection_health_schema'; @@ -42,6 +44,7 @@ export type AADAlert = | ObservabilitySloAlert | ObservabilityUptimeAlert | SecurityAlert + | SecurityAttackDiscoveryAlert | MlAnomalyDetectionAlert | MlAnomalyDetectionHealthAlert | TransformHealthAlert diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/attack_discovery/crud_attack_discovery_schedules_route.gen.ts b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/attack_discovery/crud_attack_discovery_schedules_route.gen.ts new file mode 100644 index 0000000000000..791e5d14b863b --- /dev/null +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/attack_discovery/crud_attack_discovery_schedules_route.gen.ts @@ -0,0 +1,157 @@ +/* + * 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. + */ + +/* + * NOTICE: Do not edit this file manually. + * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator. + * + * info: + * title: Attack discovery scheduling API endpoint + * version: 1 + */ + +import { z } from '@kbn/zod'; + +import { + AttackDiscoveryScheduleCreateProps, + AttackDiscoverySchedule, + AttackDiscoveryScheduleUpdateProps, +} from './schedules.gen'; +import { NonEmptyString } from '../common_attributes.gen'; + +/** + * Object containing Attack Discovery schedule. + */ +export type AttackDiscoveryGenericResponse = z.infer; +export const AttackDiscoveryGenericResponse = z.object({}).catchall(z.unknown()); + +/** + * An attack discovery generic error + */ +export type AttackDiscoveryGenericError = z.infer; +export const AttackDiscoveryGenericError = z.object({ + statusCode: z.number().optional(), + error: z.string().optional(), + message: z.string().optional(), +}); + +export type CreateAttackDiscoverySchedulesRequestBody = z.infer< + typeof CreateAttackDiscoverySchedulesRequestBody +>; +export const CreateAttackDiscoverySchedulesRequestBody = AttackDiscoveryScheduleCreateProps; +export type CreateAttackDiscoverySchedulesRequestBodyInput = z.input< + typeof CreateAttackDiscoverySchedulesRequestBody +>; + +export type CreateAttackDiscoverySchedulesResponse = z.infer< + typeof CreateAttackDiscoverySchedulesResponse +>; +export const CreateAttackDiscoverySchedulesResponse = AttackDiscoverySchedule; + +export type DeleteAttackDiscoverySchedulesRequestParams = z.infer< + typeof DeleteAttackDiscoverySchedulesRequestParams +>; +export const DeleteAttackDiscoverySchedulesRequestParams = z.object({ + /** + * The Attack Discovery schedule's `id` value. + */ + id: NonEmptyString, +}); +export type DeleteAttackDiscoverySchedulesRequestParamsInput = z.input< + typeof DeleteAttackDiscoverySchedulesRequestParams +>; + +export type DeleteAttackDiscoverySchedulesResponse = z.infer< + typeof DeleteAttackDiscoverySchedulesResponse +>; +export const DeleteAttackDiscoverySchedulesResponse = z.object({ + id: NonEmptyString, +}); + +export type DisableAttackDiscoverySchedulesRequestParams = z.infer< + typeof DisableAttackDiscoverySchedulesRequestParams +>; +export const DisableAttackDiscoverySchedulesRequestParams = z.object({ + /** + * The Attack Discovery schedule's `id` value. + */ + id: NonEmptyString, +}); +export type DisableAttackDiscoverySchedulesRequestParamsInput = z.input< + typeof DisableAttackDiscoverySchedulesRequestParams +>; + +export type DisableAttackDiscoverySchedulesResponse = z.infer< + typeof DisableAttackDiscoverySchedulesResponse +>; +export const DisableAttackDiscoverySchedulesResponse = z.object({ + id: NonEmptyString, +}); + +export type EnableAttackDiscoverySchedulesRequestParams = z.infer< + typeof EnableAttackDiscoverySchedulesRequestParams +>; +export const EnableAttackDiscoverySchedulesRequestParams = z.object({ + /** + * The Attack Discovery schedule's `id` value. + */ + id: NonEmptyString, +}); +export type EnableAttackDiscoverySchedulesRequestParamsInput = z.input< + typeof EnableAttackDiscoverySchedulesRequestParams +>; + +export type EnableAttackDiscoverySchedulesResponse = z.infer< + typeof EnableAttackDiscoverySchedulesResponse +>; +export const EnableAttackDiscoverySchedulesResponse = z.object({ + id: NonEmptyString, +}); + +export type GetAttackDiscoverySchedulesRequestParams = z.infer< + typeof GetAttackDiscoverySchedulesRequestParams +>; +export const GetAttackDiscoverySchedulesRequestParams = z.object({ + /** + * The Attack Discovery schedule's `id` value. + */ + id: NonEmptyString, +}); +export type GetAttackDiscoverySchedulesRequestParamsInput = z.input< + typeof GetAttackDiscoverySchedulesRequestParams +>; + +export type GetAttackDiscoverySchedulesResponse = z.infer< + typeof GetAttackDiscoverySchedulesResponse +>; +export const GetAttackDiscoverySchedulesResponse = AttackDiscoverySchedule; + +export type UpdateAttackDiscoverySchedulesRequestParams = z.infer< + typeof UpdateAttackDiscoverySchedulesRequestParams +>; +export const UpdateAttackDiscoverySchedulesRequestParams = z.object({ + /** + * The Attack Discovery schedule's `id` value. + */ + id: NonEmptyString, +}); +export type UpdateAttackDiscoverySchedulesRequestParamsInput = z.input< + typeof UpdateAttackDiscoverySchedulesRequestParams +>; + +export type UpdateAttackDiscoverySchedulesRequestBody = z.infer< + typeof UpdateAttackDiscoverySchedulesRequestBody +>; +export const UpdateAttackDiscoverySchedulesRequestBody = AttackDiscoveryScheduleUpdateProps; +export type UpdateAttackDiscoverySchedulesRequestBodyInput = z.input< + typeof UpdateAttackDiscoverySchedulesRequestBody +>; + +export type UpdateAttackDiscoverySchedulesResponse = z.infer< + typeof UpdateAttackDiscoverySchedulesResponse +>; +export const UpdateAttackDiscoverySchedulesResponse = AttackDiscoverySchedule; diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/attack_discovery/crud_attack_discovery_schedules_route.schema.yaml b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/attack_discovery/crud_attack_discovery_schedules_route.schema.yaml new file mode 100644 index 0000000000000..378faefeeb180 --- /dev/null +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/attack_discovery/crud_attack_discovery_schedules_route.schema.yaml @@ -0,0 +1,218 @@ +openapi: 3.0.0 +info: + title: Attack discovery scheduling API endpoint + version: '1' +components: + x-codegen-enabled: true + schemas: + AttackDiscoveryGenericResponse: + type: object + additionalProperties: true + description: Object containing Attack Discovery schedule. + AttackDiscoveryGenericError: + type: object + description: An attack discovery generic error + properties: + statusCode: + type: number + error: + type: string + message: + type: string + +paths: + /internal/elastic_assistant/attack_discovery/schedules: + post: + x-codegen-enabled: true + x-labels: [ess, serverless] + operationId: CreateAttackDiscoverySchedules + description: Creates attack discovery schedule + summary: Creates attack discovery schedule + tags: + - attack_discovery_schedule + requestBody: + required: true + content: + application/json: + schema: + $ref: './schedules.schema.yaml#/components/schemas/AttackDiscoveryScheduleCreateProps' + responses: + 200: + description: Successful response + content: + application/json: + schema: + $ref: './schedules.schema.yaml#/components/schemas/AttackDiscoverySchedule' + 400: + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/AttackDiscoveryGenericError' + + /internal/elastic_assistant/attack_discovery/schedules/{id}: + get: + x-codegen-enabled: true + x-labels: [ess, serverless] + operationId: GetAttackDiscoverySchedules + description: Gets attack discovery schedule + summary: Gets attack discovery schedule + tags: + - attack_discovery_schedule + parameters: + - name: id + in: path + required: true + description: The Attack Discovery schedule's `id` value. + schema: + $ref: '../common_attributes.schema.yaml#/components/schemas/NonEmptyString' + responses: + 200: + description: Successful request returning an Attack Discovery schedule + content: + application/json: + schema: + $ref: './schedules.schema.yaml#/components/schemas/AttackDiscoverySchedule' + 400: + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/AttackDiscoveryGenericError' + put: + x-codegen-enabled: true + x-labels: [ess, serverless] + operationId: UpdateAttackDiscoverySchedules + description: Updates attack discovery schedule + summary: Updates attack discovery schedule + tags: + - attack_discovery_schedule + parameters: + - name: id + in: path + required: true + description: The Attack Discovery schedule's `id` value. + schema: + $ref: '../common_attributes.schema.yaml#/components/schemas/NonEmptyString' + requestBody: + required: true + content: + application/json: + schema: + $ref: './schedules.schema.yaml#/components/schemas/AttackDiscoveryScheduleUpdateProps' + responses: + 200: + description: Successful request returning the updated Attack Discovery schedule + content: + application/json: + schema: + $ref: './schedules.schema.yaml#/components/schemas/AttackDiscoverySchedule' + 400: + description: Generic Error + content: + application/json: + schema: + $ref: '#/components/schemas/AttackDiscoveryGenericError' + delete: + x-codegen-enabled: true + x-labels: [ess, serverless] + operationId: DeleteAttackDiscoverySchedules + description: Deletes attack discovery schedule + summary: Deletes attack discovery schedule + tags: + - attack_discovery_schedule + parameters: + - name: id + in: path + required: true + description: The Attack Discovery schedule's `id` value. + schema: + $ref: '../common_attributes.schema.yaml#/components/schemas/NonEmptyString' + responses: + 200: + description: Successful request returning the deleted Attack Discovery schedule's ID + content: + application/json: + schema: + type: object + required: + - id + properties: + id: + $ref: '../common_attributes.schema.yaml#/components/schemas/NonEmptyString' + 400: + description: Generic Error + content: + application/json: + schema: + $ref: '#/components/schemas/AttackDiscoveryGenericError' + + /internal/elastic_assistant/attack_discovery/schedules/{id}/_enable: + put: + x-codegen-enabled: true + x-labels: [ess, serverless] + operationId: EnableAttackDiscoverySchedules + description: Enables attack discovery schedule + summary: Enables attack discovery schedule + tags: + - attack_discovery_schedule + parameters: + - name: id + in: path + required: true + description: The Attack Discovery schedule's `id` value. + schema: + $ref: '../common_attributes.schema.yaml#/components/schemas/NonEmptyString' + responses: + 200: + description: Successful request returning an Attack Discovery schedule + content: + application/json: + schema: + type: object + required: + - id + properties: + id: + $ref: '../common_attributes.schema.yaml#/components/schemas/NonEmptyString' + 400: + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/AttackDiscoveryGenericError' + + /internal/elastic_assistant/attack_discovery/schedules/{id}/_disable: + put: + x-codegen-enabled: true + x-labels: [ess, serverless] + operationId: DisableAttackDiscoverySchedules + description: Disables attack discovery schedule + summary: Disables attack discovery schedule + tags: + - attack_discovery_schedule + parameters: + - name: id + in: path + required: true + description: The Attack Discovery schedule's `id` value. + schema: + $ref: '../common_attributes.schema.yaml#/components/schemas/NonEmptyString' + responses: + 200: + description: Successful request returning an Attack Discovery schedule + content: + application/json: + schema: + type: object + required: + - id + properties: + id: + $ref: '../common_attributes.schema.yaml#/components/schemas/NonEmptyString' + 400: + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/AttackDiscoveryGenericError' diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/attack_discovery/find_attack_discovery_schedules_route.gen.ts b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/attack_discovery/find_attack_discovery_schedules_route.gen.ts new file mode 100644 index 0000000000000..4f46d506d2383 --- /dev/null +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/attack_discovery/find_attack_discovery_schedules_route.gen.ts @@ -0,0 +1,29 @@ +/* + * 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. + */ + +/* + * NOTICE: Do not edit this file manually. + * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator. + * + * info: + * title: Find Knowledge Base Entries API endpoint + * version: 2023-10-31 + */ + +import { z } from '@kbn/zod'; + +import { AttackDiscoverySchedule } from './schedules.gen'; + +export type FindAttackDiscoverySchedulesResponse = z.infer< + typeof FindAttackDiscoverySchedulesResponse +>; +export const FindAttackDiscoverySchedulesResponse = z.object({ + page: z.number(), + perPage: z.number(), + total: z.number(), + data: z.array(AttackDiscoverySchedule), +}); diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/attack_discovery/find_attack_discovery_schedules_route.schema.yaml b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/attack_discovery/find_attack_discovery_schedules_route.schema.yaml new file mode 100644 index 0000000000000..fa5c110a99614 --- /dev/null +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/attack_discovery/find_attack_discovery_schedules_route.schema.yaml @@ -0,0 +1,50 @@ +openapi: 3.0.0 +info: + title: Find Knowledge Base Entries API endpoint + version: '2023-10-31' +paths: + /internal/elastic_assistant/attack_discovery/schedules/_find: + get: + x-codegen-enabled: true + x-labels: [ess, serverless] + operationId: FindAttackDiscoverySchedules + description: Finds attack discovery schedules + summary: Finds attack discovery schedules + tags: + - attack_discovery_schedule + responses: + 200: + description: Successful response + content: + application/json: + schema: + type: object + required: + - page + - perPage + - total + - data + properties: + page: + type: number + perPage: + type: number + total: + type: number + data: + type: array + items: + $ref: './schedules.schema.yaml#/components/schemas/AttackDiscoverySchedule' + 400: + description: Generic Error + content: + application/json: + schema: + type: object + properties: + statusCode: + type: number + error: + type: string + message: + type: string diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/attack_discovery/schedules.gen.ts b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/attack_discovery/schedules.gen.ts new file mode 100644 index 0000000000000..532de603f2a49 --- /dev/null +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/attack_discovery/schedules.gen.ts @@ -0,0 +1,264 @@ +/* + * 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. + */ + +/* + * NOTICE: Do not edit this file manually. + * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator. + * + * info: + * title: Common Attack Discovery Schedule Types + * version: not applicable + */ + +import { z } from '@kbn/zod'; + +import { ApiConfig } from '../conversations/common_attributes.gen'; +import { NonEmptyString } from '../common_attributes.gen'; + +/** + * An attack discovery schedule params + */ +export type AttackDiscoveryScheduleParams = z.infer; +export const AttackDiscoveryScheduleParams = z.object({ + /** + * The index pattern to get alerts from + */ + alertsIndexPattern: z.string(), + /** + * LLM API configuration. + */ + apiConfig: ApiConfig, + end: z.string().optional(), + filter: z.object({}).catchall(z.unknown()).optional(), + size: z.number(), + start: z.string().optional(), +}); + +export type IntervalSchedule = z.infer; +export const IntervalSchedule = z.object({ + /** + * The schedule interval + */ + interval: z.string(), +}); + +/** + * Optionally groups actions by use cases. Use `default` for alert notifications. + */ +export type AttackDiscoveryScheduleActionGroup = z.infer; +export const AttackDiscoveryScheduleActionGroup = z.string(); + +/** + * The connector ID. + */ +export type AttackDiscoveryScheduleActionId = z.infer; +export const AttackDiscoveryScheduleActionId = z.string(); + +/** + * Object containing the allowed connector fields, which varies according to the connector type. + */ +export type AttackDiscoveryScheduleActionParams = z.infer< + typeof AttackDiscoveryScheduleActionParams +>; +export const AttackDiscoveryScheduleActionParams = z.object({}).catchall(z.unknown()); + +export type AttackDiscoveryScheduleActionAlertsFilter = z.infer< + typeof AttackDiscoveryScheduleActionAlertsFilter +>; +export const AttackDiscoveryScheduleActionAlertsFilter = z.object({}).catchall(z.unknown()); + +/** + * The condition for throttling the notification: `onActionGroupChange`, `onActiveAlert`, or `onThrottleInterval` + */ +export type AttackDiscoveryScheduleActionNotifyWhen = z.infer< + typeof AttackDiscoveryScheduleActionNotifyWhen +>; +export const AttackDiscoveryScheduleActionNotifyWhen = z.enum([ + 'onActiveAlert', + 'onThrottleInterval', + 'onActionGroupChange', +]); +export type AttackDiscoveryScheduleActionNotifyWhenEnum = + typeof AttackDiscoveryScheduleActionNotifyWhen.enum; +export const AttackDiscoveryScheduleActionNotifyWhenEnum = + AttackDiscoveryScheduleActionNotifyWhen.enum; + +/** + * Defines how often schedule actions are taken. Time interval in seconds, minutes, hours, or days. + */ +export type AttackDiscoveryScheduleActionThrottle = z.infer< + typeof AttackDiscoveryScheduleActionThrottle +>; +export const AttackDiscoveryScheduleActionThrottle = z.string().regex(/^[1-9]\d*[smhd]$/); + +/** + * The action frequency defines when the action runs (for example, only on schedule execution or at specific time intervals). + */ +export type AttackDiscoveryScheduleActionFrequency = z.infer< + typeof AttackDiscoveryScheduleActionFrequency +>; +export const AttackDiscoveryScheduleActionFrequency = z.object({ + /** + * Action summary indicates whether we will send a summary notification about all the generate alerts or notification per individual alert + */ + summary: z.boolean(), + notifyWhen: AttackDiscoveryScheduleActionNotifyWhen, + throttle: AttackDiscoveryScheduleActionThrottle.nullable(), +}); + +export type AttackDiscoveryScheduleAction = z.infer; +export const AttackDiscoveryScheduleAction = z.object({ + /** + * The action type used for sending notifications. + */ + actionTypeId: z.string(), + group: AttackDiscoveryScheduleActionGroup, + id: AttackDiscoveryScheduleActionId, + params: AttackDiscoveryScheduleActionParams, + uuid: NonEmptyString.optional(), + alertsFilter: AttackDiscoveryScheduleActionAlertsFilter.optional(), + frequency: AttackDiscoveryScheduleActionFrequency.optional(), +}); + +/** + * An attack discovery schedule execution status + */ +export type AttackDiscoveryScheduleExecutionStatus = z.infer< + typeof AttackDiscoveryScheduleExecutionStatus +>; +export const AttackDiscoveryScheduleExecutionStatus = z.enum([ + 'ok', + 'active', + 'error', + 'unknown', + 'warning', +]); +export type AttackDiscoveryScheduleExecutionStatusEnum = + typeof AttackDiscoveryScheduleExecutionStatus.enum; +export const AttackDiscoveryScheduleExecutionStatusEnum = + AttackDiscoveryScheduleExecutionStatus.enum; + +/** + * An attack discovery schedule execution information + */ +export type AttackDiscoveryScheduleExecution = z.infer; +export const AttackDiscoveryScheduleExecution = z.object({ + /** + * Date of the execution + */ + date: z.string().datetime(), + /** + * Duration of the execution + */ + duration: z.number().optional(), + /** + * Status of the execution + */ + status: AttackDiscoveryScheduleExecutionStatus, + message: z.string().optional(), +}); + +/** + * An attack discovery schedule + */ +export type AttackDiscoverySchedule = z.infer; +export const AttackDiscoverySchedule = z.object({ + /** + * UUID of attack discovery schedule + */ + id: z.string(), + /** + * The name of the schedule + */ + name: z.string(), + /** + * The name of the user that created the schedule + */ + createdBy: z.string(), + /** + * The name of the user that updated the schedule + */ + updatedBy: z.string(), + /** + * The date the schedule was created + */ + createdAt: z.string().datetime(), + /** + * The date the schedule was updated + */ + updatedAt: z.string().datetime(), + /** + * Indicates whether the schedule is enabled + */ + enabled: z.boolean(), + /** + * The attack discovery schedule configuration parameters + */ + params: AttackDiscoveryScheduleParams, + /** + * The attack discovery schedule interval + */ + schedule: IntervalSchedule, + /** + * The attack discovery schedule actions + */ + actions: z.array(AttackDiscoveryScheduleAction), + /** + * The attack discovery schedule last execution summary + */ + lastExecution: AttackDiscoveryScheduleExecution.optional(), +}); + +/** + * An attack discovery schedule create properties + */ +export type AttackDiscoveryScheduleCreateProps = z.infer; +export const AttackDiscoveryScheduleCreateProps = z.object({ + /** + * The name of the schedule + */ + name: z.string(), + /** + * Indicates whether the schedule is enabled + */ + enabled: z.boolean().optional(), + /** + * The attack discovery schedule configuration parameters + */ + params: AttackDiscoveryScheduleParams, + /** + * The attack discovery schedule interval + */ + schedule: IntervalSchedule, + /** + * The attack discovery schedule actions + */ + actions: z.array(AttackDiscoveryScheduleAction).optional(), +}); + +/** + * An attack discovery schedule update properties + */ +export type AttackDiscoveryScheduleUpdateProps = z.infer; +export const AttackDiscoveryScheduleUpdateProps = z.object({ + /** + * The name of the schedule + */ + name: z.string(), + /** + * The attack discovery schedule configuration parameters + */ + params: AttackDiscoveryScheduleParams, + /** + * The attack discovery schedule interval + */ + schedule: IntervalSchedule, + /** + * The attack discovery schedule actions + */ + actions: z.array(AttackDiscoveryScheduleAction), +}); diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/attack_discovery/schedules.schema.yaml b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/attack_discovery/schedules.schema.yaml new file mode 100644 index 0000000000000..ce8c966a1677e --- /dev/null +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/attack_discovery/schedules.schema.yaml @@ -0,0 +1,246 @@ +openapi: 3.0.0 +info: + title: Common Attack Discovery Schedule Types + version: 'not applicable' +paths: {} +components: + x-codegen-enabled: true + schemas: + AttackDiscoverySchedule: + type: object + description: An attack discovery schedule + required: + - id + - name + - createdBy + - updatedBy + - createdAt + - updatedAt + - enabled + - params + - schedule + - actions + properties: + id: + description: UUID of attack discovery schedule + type: string + name: + description: The name of the schedule + type: string + createdBy: + description: The name of the user that created the schedule + type: string + updatedBy: + description: The name of the user that updated the schedule + type: string + createdAt: + description: The date the schedule was created + type: string + format: date-time + updatedAt: + description: The date the schedule was updated + type: string + format: date-time + enabled: + description: Indicates whether the schedule is enabled + type: boolean + params: + description: The attack discovery schedule configuration parameters + $ref: '#/components/schemas/AttackDiscoveryScheduleParams' + schedule: + description: The attack discovery schedule interval + $ref: '#/components/schemas/IntervalSchedule' + actions: + description: The attack discovery schedule actions + type: array + items: + $ref: '#/components/schemas/AttackDiscoveryScheduleAction' + lastExecution: + description: The attack discovery schedule last execution summary + $ref: '#/components/schemas/AttackDiscoveryScheduleExecution' + + AttackDiscoveryScheduleParams: + type: object + description: An attack discovery schedule params + required: + - alertsIndexPattern + - apiConfig + - size + properties: + alertsIndexPattern: + description: The index pattern to get alerts from + type: string + apiConfig: + description: LLM API configuration. + $ref: '../conversations/common_attributes.schema.yaml#/components/schemas/ApiConfig' + end: + type: string + filter: + type: object + additionalProperties: true + size: + type: number + start: + type: string + + IntervalSchedule: + type: object + required: + - interval + properties: + interval: + description: The schedule interval + type: string + + AttackDiscoveryScheduleActionThrottle: + description: Defines how often schedule actions are taken. Time interval in seconds, minutes, hours, or days. + type: string + pattern: '^[1-9]\d*[smhd]$' # any number except zero followed by one of the suffixes 's', 'm', 'h', 'd' + example: '1h' + + AttackDiscoveryScheduleActionNotifyWhen: + type: string + enum: + - 'onActiveAlert' + - 'onThrottleInterval' + - 'onActionGroupChange' + description: 'The condition for throttling the notification: `onActionGroupChange`, `onActiveAlert`, or `onThrottleInterval`' + + AttackDiscoveryScheduleActionFrequency: + type: object + description: The action frequency defines when the action runs (for example, only on schedule execution or at specific time intervals). + properties: + summary: + type: boolean + description: Action summary indicates whether we will send a summary notification about all the generate alerts or notification per individual alert + notifyWhen: + $ref: '#/components/schemas/AttackDiscoveryScheduleActionNotifyWhen' + throttle: + $ref: '#/components/schemas/AttackDiscoveryScheduleActionThrottle' + nullable: true + required: + - summary + - notifyWhen + - throttle + + AttackDiscoveryScheduleActionAlertsFilter: + type: object + additionalProperties: true + + AttackDiscoveryScheduleActionParams: + type: object + description: Object containing the allowed connector fields, which varies according to the connector type. + additionalProperties: true + + AttackDiscoveryScheduleActionGroup: + type: string + description: Optionally groups actions by use cases. Use `default` for alert notifications. + + AttackDiscoveryScheduleActionId: + type: string + description: The connector ID. + + AttackDiscoveryScheduleAction: + type: object + properties: + actionTypeId: + type: string + description: The action type used for sending notifications. + group: + $ref: '#/components/schemas/AttackDiscoveryScheduleActionGroup' + id: + $ref: '#/components/schemas/AttackDiscoveryScheduleActionId' + params: + $ref: '#/components/schemas/AttackDiscoveryScheduleActionParams' + uuid: + $ref: '../common_attributes.schema.yaml#/components/schemas/NonEmptyString' + alertsFilter: + $ref: '#/components/schemas/AttackDiscoveryScheduleActionAlertsFilter' + frequency: + $ref: '#/components/schemas/AttackDiscoveryScheduleActionFrequency' + required: + - actionTypeId + - id + - group + - params + + AttackDiscoveryScheduleExecutionStatus: + type: string + description: An attack discovery schedule execution status + enum: + - ok + - active + - error + - unknown + - warning + + AttackDiscoveryScheduleExecution: + type: object + description: An attack discovery schedule execution information + required: + - date + - status + - lastDuration + properties: + date: + description: Date of the execution + type: string + format: date-time + duration: + description: Duration of the execution + type: number + status: + description: Status of the execution + $ref: '#/components/schemas/AttackDiscoveryScheduleExecutionStatus' + message: + type: string + + AttackDiscoveryScheduleCreateProps: + type: object + description: An attack discovery schedule create properties + required: + - name + - params + - schedule + properties: + name: + description: The name of the schedule + type: string + enabled: + description: Indicates whether the schedule is enabled + type: boolean + params: + description: The attack discovery schedule configuration parameters + $ref: '#/components/schemas/AttackDiscoveryScheduleParams' + schedule: + description: The attack discovery schedule interval + $ref: '#/components/schemas/IntervalSchedule' + actions: + description: The attack discovery schedule actions + type: array + items: + $ref: '#/components/schemas/AttackDiscoveryScheduleAction' + + AttackDiscoveryScheduleUpdateProps: + type: object + description: An attack discovery schedule update properties + required: + - name + - params + - schedule + - actions + properties: + name: + description: The name of the schedule + type: string + params: + description: The attack discovery schedule configuration parameters + $ref: '#/components/schemas/AttackDiscoveryScheduleParams' + schedule: + description: The attack discovery schedule interval + $ref: '#/components/schemas/IntervalSchedule' + actions: + description: The attack discovery schedule actions + type: array + items: + $ref: '#/components/schemas/AttackDiscoveryScheduleAction' diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/index.ts b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/index.ts index 02ac9b7b1ba90..6a3cba24de44e 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/index.ts +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/index.ts @@ -26,6 +26,9 @@ export * from './attack_discovery/common_attributes.gen'; export * from './attack_discovery/get_attack_discovery_route.gen'; export * from './attack_discovery/post_attack_discovery_route.gen'; export * from './attack_discovery/cancel_attack_discovery_route.gen'; +export * from './attack_discovery/crud_attack_discovery_schedules_route.gen'; +export * from './attack_discovery/find_attack_discovery_schedules_route.gen'; +export * from './attack_discovery/schedules.gen'; // Defend insight Schemas export * from './defend_insights'; diff --git a/x-pack/solutions/security/packages/features/src/attack_discovery/kibana_features.ts b/x-pack/solutions/security/packages/features/src/attack_discovery/kibana_features.ts index 1ac3f7b629ccb..beae654985d80 100644 --- a/x-pack/solutions/security/packages/features/src/attack_discovery/kibana_features.ts +++ b/x-pack/solutions/security/packages/features/src/attack_discovery/kibana_features.ts @@ -8,10 +8,18 @@ import { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common'; import { i18n } from '@kbn/i18n'; import { KibanaFeatureScope } from '@kbn/features-plugin/common'; +import { ATTACK_DISCOVERY_SCHEDULES_ALERT_TYPE_ID } from '@kbn/elastic-assistant-common/constants'; -import { APP_ID, ATTACK_DISCOVERY_FEATURE_ID } from '../constants'; +import { APP_ID, ATTACK_DISCOVERY_FEATURE_ID, SERVER_APP_ID } from '../constants'; import { type BaseKibanaFeatureConfig } from '../types'; +const alertingFeatures = [ + { + ruleTypeId: ATTACK_DISCOVERY_SCHEDULES_ALERT_TYPE_ID, + consumers: [SERVER_APP_ID], + }, +]; + export const getAttackDiscoveryBaseKibanaFeature = (): BaseKibanaFeatureConfig => ({ id: ATTACK_DISCOVERY_FEATURE_ID, name: i18n.translate( @@ -26,6 +34,7 @@ export const getAttackDiscoveryBaseKibanaFeature = (): BaseKibanaFeatureConfig = app: [ATTACK_DISCOVERY_FEATURE_ID, 'kibana'], catalogue: [APP_ID], minimumLicense: 'enterprise', + alerting: alertingFeatures, privileges: { all: { api: ['elasticAssistant'], @@ -35,6 +44,10 @@ export const getAttackDiscoveryBaseKibanaFeature = (): BaseKibanaFeatureConfig = all: [], read: [], }, + alerting: { + rule: { all: alertingFeatures }, + alert: { all: alertingFeatures }, + }, ui: [], }, read: { @@ -44,6 +57,10 @@ export const getAttackDiscoveryBaseKibanaFeature = (): BaseKibanaFeatureConfig = all: [], read: [], }, + alerting: { + rule: { read: alertingFeatures }, + alert: { all: alertingFeatures }, + }, ui: [], }, }, diff --git a/x-pack/solutions/security/packages/features/tsconfig.json b/x-pack/solutions/security/packages/features/tsconfig.json index bc415fbae622b..10e657c75d7e0 100644 --- a/x-pack/solutions/security/packages/features/tsconfig.json +++ b/x-pack/solutions/security/packages/features/tsconfig.json @@ -17,6 +17,7 @@ "@kbn/cases-plugin", "@kbn/securitysolution-rules", "@kbn/securitysolution-list-constants", + "@kbn/elastic-assistant-common", ], "exclude": ["target/**/*"] } diff --git a/x-pack/solutions/security/plugins/elastic_assistant/common/constants.ts b/x-pack/solutions/security/plugins/elastic_assistant/common/constants.ts index 86f8acd69ee70..d083e7e78d7bf 100755 --- a/x-pack/solutions/security/plugins/elastic_assistant/common/constants.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/common/constants.ts @@ -16,6 +16,11 @@ export const POST_ACTIONS_CONNECTOR_EXECUTE = `${BASE_PATH}/actions/connector/{c export const ATTACK_DISCOVERY = `${BASE_PATH}/attack_discovery`; export const ATTACK_DISCOVERY_BY_CONNECTOR_ID = `${ATTACK_DISCOVERY}/{connectorId}`; export const ATTACK_DISCOVERY_CANCEL_BY_CONNECTOR_ID = `${ATTACK_DISCOVERY}/cancel/{connectorId}`; +export const ATTACK_DISCOVERY_SCHEDULES = `${ATTACK_DISCOVERY}/schedules`; +export const ATTACK_DISCOVERY_SCHEDULES_BY_ID = `${ATTACK_DISCOVERY_SCHEDULES}/{id}`; +export const ATTACK_DISCOVERY_SCHEDULES_BY_ID_ENABLE = `${ATTACK_DISCOVERY_SCHEDULES}/{id}/_enable`; +export const ATTACK_DISCOVERY_SCHEDULES_BY_ID_DISABLE = `${ATTACK_DISCOVERY_SCHEDULES}/{id}/_disable`; +export const ATTACK_DISCOVERY_SCHEDULES_FIND = `${ATTACK_DISCOVERY_SCHEDULES}/_find`; export const CONVERSATIONS_TABLE_MAX_PAGE_SIZE = 100; export const ANONYMIZATION_FIELDS_TABLE_MAX_PAGE_SIZE = 100; diff --git a/x-pack/solutions/security/plugins/elastic_assistant/kibana.jsonc b/x-pack/solutions/security/plugins/elastic_assistant/kibana.jsonc index 2b06f1e0db65e..5407404ed173e 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/kibana.jsonc +++ b/x-pack/solutions/security/plugins/elastic_assistant/kibana.jsonc @@ -13,6 +13,7 @@ "server": true, "requiredPlugins": [ "actions", + "alerting", "data", "ml", "taskManager", diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/__mocks__/attack_discovery_schedules.mock.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/__mocks__/attack_discovery_schedules.mock.ts new file mode 100644 index 0000000000000..625809cd6b045 --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/__mocks__/attack_discovery_schedules.mock.ts @@ -0,0 +1,129 @@ +/* + * 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 { CreateRuleData } from '@kbn/alerting-plugin/server/application/rule/methods/create'; +import { UpdateRuleData } from '@kbn/alerting-plugin/server/application/rule/methods/update'; +import { + ATTACK_DISCOVERY_SCHEDULES_ALERT_TYPE_ID, + AttackDiscoverySchedule, + AttackDiscoveryScheduleCreateProps, + AttackDiscoveryScheduleParams, +} from '@kbn/elastic-assistant-common'; + +import { SanitizedRule, SanitizedRuleAction } from '@kbn/alerting-types'; + +export const getAttackDiscoveryCreateScheduleMock = ( + enabled = true +): CreateRuleData => { + return { + name: 'Test Schedule 1', + schedule: { + interval: '10m', + }, + params: { + alertsIndexPattern: '.alerts-security.alerts-default', + apiConfig: { + connectorId: 'gpt-4o', + actionTypeId: '.gen-ai', + }, + end: 'now', + size: 100, + start: 'now-24h', + }, + actions: [], + alertTypeId: ATTACK_DISCOVERY_SCHEDULES_ALERT_TYPE_ID, + consumer: 'siem', + enabled, + tags: [], + }; +}; + +export const getAttackDiscoveryUpdateScheduleMock = ( + id: string, + overrides: Partial> +): UpdateRuleData & { id: string } => { + return { + id, + ...getAttackDiscoveryCreateScheduleMock(), + ...overrides, + }; +}; + +export const getInternalAttackDiscoveryScheduleMock = ( + createParams: AttackDiscoveryScheduleCreateProps, + overrides?: Partial> +): SanitizedRule => { + const { actions = [], params, ...restAttributes } = createParams; + return { + id: '54fc45a4-9d1e-4228-8fec-dbf91ea15171', + enabled: false, + tags: [], + alertTypeId: 'attack-discovery', + consumer: 'siem', + actions: (actions as SanitizedRuleAction[]) ?? [], + systemActions: [], + params, + createdBy: 'elastic', + updatedBy: 'elastic', + createdAt: new Date('2025-03-31T17:38:03.544Z'), + updatedAt: new Date('2025-03-31T17:38:03.544Z'), + apiKeyOwner: null, + apiKeyCreatedByUser: null, + throttle: null, + muteAll: false, + notifyWhen: null, + mutedInstanceIds: [], + executionStatus: { + status: 'pending', + lastExecutionDate: new Date('2025-03-31T17:38:03.544Z'), + }, + revision: 0, + running: false, + ...restAttributes, + ...overrides, + }; +}; + +export const getAttackDiscoveryScheduleMock = ( + overrides?: Partial +): AttackDiscoverySchedule => { + return { + id: '31db8de1-65f2-4da2-a3e6-d15d9931817e', + name: 'Test Schedule', + createdBy: 'elastic', + updatedBy: 'elastic', + createdAt: '2025-03-31T09:57:42.194Z', + updatedAt: '2025-03-31T09:57:42.194Z', + enabled: false, + params: { + alertsIndexPattern: '.alerts-security.alerts-default', + apiConfig: { + connectorId: 'gpt-4o', + actionTypeId: '.gen-ai', + }, + end: 'now', + size: 100, + start: 'now-24h', + }, + schedule: { + interval: '10m', + }, + actions: [], + ...overrides, + }; +}; + +export const getInternalFindAttackDiscoverySchedulesMock = ( + schedules: Array> +) => { + return { + page: 1, + perPage: 20, + total: schedules.length, + data: schedules, + }; +}; diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/__mocks__/data_clients.mock.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/__mocks__/data_clients.mock.ts index 5196174825345..e4d43ae5596ef 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/__mocks__/data_clients.mock.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/__mocks__/data_clients.mock.ts @@ -10,11 +10,15 @@ import { AIAssistantConversationsDataClient } from '../ai_assistant_data_clients import { AIAssistantKnowledgeBaseDataClient } from '../ai_assistant_data_clients/knowledge_base'; import { AIAssistantDataClient } from '../ai_assistant_data_clients'; import { AttackDiscoveryDataClient } from '../lib/attack_discovery/persistence'; +import { AttackDiscoveryScheduleDataClient } from '../lib/attack_discovery/schedules/data_client'; type ConversationsDataClientContract = PublicMethodsOf; export type ConversationsDataClientMock = jest.Mocked; type AttackDiscoveryDataClientContract = PublicMethodsOf; export type AttackDiscoveryDataClientMock = jest.Mocked; +type AttackDiscoveryScheduleDataClientContract = PublicMethodsOf; +export type AttackDiscoveryScheduleDataClientMock = + jest.Mocked; type KnowledgeBaseDataClientContract = PublicMethodsOf & { isSetupInProgress: AIAssistantKnowledgeBaseDataClient['isSetupInProgress']; }; @@ -57,6 +61,22 @@ export const attackDiscoveryDataClientMock: { create: createAttackDiscoveryDataClientMock, }; +const createAttackDiscoveryScheduleDataClientMock = (): AttackDiscoveryScheduleDataClientMock => ({ + findSchedules: jest.fn(), + getSchedule: jest.fn(), + createSchedule: jest.fn(), + updateSchedule: jest.fn(), + deleteSchedule: jest.fn(), + enableSchedule: jest.fn(), + disableSchedule: jest.fn(), +}); + +export const attackDiscoveryScheduleDataClientMock: { + create: () => AttackDiscoveryScheduleDataClientMock; +} = { + create: createAttackDiscoveryScheduleDataClientMock, +}; + const createKnowledgeBaseDataClientMock = () => { const mocked: KnowledgeBaseDataClientMock = { addKnowledgeBaseDocuments: jest.fn(), diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/__mocks__/request.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/__mocks__/request.ts index e3c32bbc5ab5f..b793597fca9a2 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/__mocks__/request.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/__mocks__/request.ts @@ -9,13 +9,20 @@ import { ATTACK_DISCOVERY, ATTACK_DISCOVERY_BY_CONNECTOR_ID, ATTACK_DISCOVERY_CANCEL_BY_CONNECTOR_ID, + ATTACK_DISCOVERY_SCHEDULES, + ATTACK_DISCOVERY_SCHEDULES_BY_ID, + ATTACK_DISCOVERY_SCHEDULES_BY_ID_DISABLE, + ATTACK_DISCOVERY_SCHEDULES_BY_ID_ENABLE, + ATTACK_DISCOVERY_SCHEDULES_FIND, CAPABILITIES, } from '../../common/constants'; import type { + CreateAttackDiscoverySchedulesRequestBody, DefendInsightsGetRequestQuery, DefendInsightsPostRequestBody, DeleteKnowledgeBaseEntryRequestParams, KnowledgeBaseEntryUpdateProps, + UpdateAttackDiscoverySchedulesRequestBody, UpdateKnowledgeBaseEntryRequestParams, } from '@kbn/elastic-assistant-common'; import { @@ -296,3 +303,57 @@ export const postDefendInsightsRequest = (body: DefendInsightsPostRequestBody) = path: DEFEND_INSIGHTS, body, }); + +export const findAttackDiscoverySchedulesRequest = () => + requestMock.create({ + method: 'get', + path: ATTACK_DISCOVERY_SCHEDULES_FIND, + }); + +export const createAttackDiscoverySchedulesRequest = ( + body: CreateAttackDiscoverySchedulesRequestBody +) => + requestMock.create({ + method: 'post', + path: ATTACK_DISCOVERY_SCHEDULES, + body, + }); + +export const deleteAttackDiscoverySchedulesRequest = (id: string) => + requestMock.create({ + method: 'delete', + path: ATTACK_DISCOVERY_SCHEDULES_BY_ID, + params: { id }, + }); + +export const getAttackDiscoverySchedulesRequest = (id: string) => + requestMock.create({ + method: 'get', + path: ATTACK_DISCOVERY_SCHEDULES_BY_ID, + params: { id }, + }); + +export const updateAttackDiscoverySchedulesRequest = ( + id: string, + body: UpdateAttackDiscoverySchedulesRequestBody +) => + requestMock.create({ + method: 'put', + path: ATTACK_DISCOVERY_SCHEDULES_BY_ID, + params: { id }, + body, + }); + +export const enableAttackDiscoverySchedulesRequest = (id: string) => + requestMock.create({ + method: 'post', + path: ATTACK_DISCOVERY_SCHEDULES_BY_ID_ENABLE, + params: { id }, + }); + +export const disableAttackDiscoverySchedulesRequest = (id: string) => + requestMock.create({ + method: 'put', + path: ATTACK_DISCOVERY_SCHEDULES_BY_ID_DISABLE, + params: { id }, + }); diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/__mocks__/request_context.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/__mocks__/request_context.ts index e93e27a7b8c8a..75dc9ccfb3865 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/__mocks__/request_context.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/__mocks__/request_context.ts @@ -17,6 +17,7 @@ import { import { PluginStartContract as ActionsPluginStart } from '@kbn/actions-plugin/server'; import { attackDiscoveryDataClientMock, + attackDiscoveryScheduleDataClientMock, conversationsDataClientMock, dataClientMock, knowledgeBaseDataClientMock, @@ -31,6 +32,7 @@ import { defaultAssistantFeatures } from '@kbn/elastic-assistant-common'; import { AttackDiscoveryDataClient } from '../lib/attack_discovery/persistence'; import { DefendInsightsDataClient } from '../lib/defend_insights/persistence'; import { authenticatedUser } from './user'; +import { AttackDiscoveryScheduleDataClient } from '../lib/attack_discovery/schedules/data_client'; export const createMockClients = () => { const core = coreMock.createRequestHandlerContext(); @@ -49,6 +51,7 @@ export const createMockClients = () => { getAIAssistantKnowledgeBaseDataClient: knowledgeBaseDataClientMock.create(), getAIAssistantPromptsDataClient: dataClientMock.create(), getAttackDiscoveryDataClient: attackDiscoveryDataClientMock.create(), + getAttackDiscoverySchedulingDataClient: attackDiscoveryScheduleDataClientMock.create(), getDefendInsightsDataClient: dataClientMock.create(), getAIAssistantAnonymizationFieldsDataClient: dataClientMock.create(), getSpaceId: jest.fn(), @@ -129,6 +132,14 @@ const createElasticAssistantRequestContextMock = ( () => clients.elasticAssistant.getAttackDiscoveryDataClient ) as unknown as jest.MockInstance, [], unknown> & (() => Promise), + getAttackDiscoverySchedulingDataClient: jest.fn( + () => clients.elasticAssistant.getAttackDiscoverySchedulingDataClient + ) as unknown as jest.MockInstance< + Promise, + [], + unknown + > & + (() => Promise), getDefendInsightsDataClient: jest.fn( () => clients.elasticAssistant.getDefendInsightsDataClient ) as unknown as jest.MockInstance, [], unknown> & diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_service/index.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_service/index.ts index c5fc659db5658..d6352fc524e30 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_service/index.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_service/index.ts @@ -52,6 +52,10 @@ import { AttackDiscoveryDataClient } from '../lib/attack_discovery/persistence'; import { DefendInsightsDataClient } from '../lib/defend_insights/persistence'; import { createGetElserId, ensureProductDocumentationInstalled } from './helpers'; import { hasAIAssistantLicense } from '../routes/helpers'; +import { + AttackDiscoveryScheduleDataClient, + CreateAttackDiscoveryScheduleDataClientParams, +} from '../lib/attack_discovery/schedules/data_client'; const TOTAL_FIELDS_LIMIT = 2500; @@ -610,6 +614,14 @@ export class AIAssistantService { }); } + public async createAttackDiscoverySchedulingDataClient( + opts: CreateAttackDiscoveryScheduleDataClientParams + ): Promise { + return new AttackDiscoveryScheduleDataClient({ + rulesClient: opts.rulesClient, + }); + } + public async createDefendInsightsDataClient( opts: CreateAIAssistantClientParams ): Promise { diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/schedules/constants.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/schedules/constants.ts index fb90bfb04b8ef..8bdce63156083 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/schedules/constants.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/schedules/constants.ts @@ -6,12 +6,15 @@ */ import { IRuleTypeAlerts } from '@kbn/alerting-plugin/server'; -import { AttackDiscoveryAlert } from './types'; +import { SecurityAttackDiscoveryAlert } from '@kbn/alerts-as-data-utils'; import { attackDiscoveryAlertFieldMap } from './fields'; -export const ATTACK_DISCOVERY_ALERTS_AAD_CONFIG: IRuleTypeAlerts = { - context: 'security.attack.discovery', +export const ATTACK_DISCOVERY_ALERTS_CONTEXT = 'security.attack.discovery' as const; + +export const ATTACK_DISCOVERY_ALERTS_AAD_CONFIG: IRuleTypeAlerts = { + context: ATTACK_DISCOVERY_ALERTS_CONTEXT, mappings: { fieldMap: attackDiscoveryAlertFieldMap }, isSpaceAware: true, shouldWrite: true, + useEcs: true, }; diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/schedules/data_client/index.test.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/schedules/data_client/index.test.ts new file mode 100644 index 0000000000000..f6af55a6b5fe9 --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/schedules/data_client/index.test.ts @@ -0,0 +1,113 @@ +/* + * 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 { rulesClientMock } from '@kbn/alerting-plugin/server/rules_client.mock'; + +import { AttackDiscoveryScheduleDataClient, AttackDiscoveryScheduleDataClientParams } from '.'; +import { + getAttackDiscoveryCreateScheduleMock, + getAttackDiscoveryUpdateScheduleMock, +} from '../../../../__mocks__/attack_discovery_schedules.mock'; + +describe('AttackDiscoveryScheduleDataClient', () => { + let scheduleDataClientParams: AttackDiscoveryScheduleDataClientParams; + + beforeEach(() => { + jest.clearAllMocks(); + scheduleDataClientParams = { + rulesClient: rulesClientMock.create(), + }; + }); + + describe('findSchedules', () => { + it('should call `rulesClient.find` with the correct filter', async () => { + const scheduleDataClient = new AttackDiscoveryScheduleDataClient(scheduleDataClientParams); + await scheduleDataClient.findSchedules(); + + expect(scheduleDataClientParams.rulesClient.find).toHaveBeenCalledWith({ + options: { filter: `alert.attributes.alertTypeId: attack-discovery` }, + }); + }); + }); + + describe('getSchedule', () => { + it('should call `rulesClient.get` with the schedule id', async () => { + const scheduleId = 'schedule-1'; + const scheduleDataClient = new AttackDiscoveryScheduleDataClient(scheduleDataClientParams); + await scheduleDataClient.getSchedule(scheduleId); + + expect(scheduleDataClientParams.rulesClient.get).toHaveBeenCalledWith({ id: scheduleId }); + }); + }); + + describe('createSchedule', () => { + it('should call `rulesClient.create` with the schedule to create', async () => { + const scheduleCreateData = getAttackDiscoveryCreateScheduleMock(); + + const scheduleDataClient = new AttackDiscoveryScheduleDataClient(scheduleDataClientParams); + await scheduleDataClient.createSchedule(scheduleCreateData); + + expect(scheduleDataClientParams.rulesClient.create).toHaveBeenCalledWith({ + data: scheduleCreateData, + }); + }); + }); + + describe('updateSchedule', () => { + it('should call `rulesClient.update` with the update attributes', async () => { + const scheduleId = 'schedule-5'; + const scheduleUpdateData = getAttackDiscoveryUpdateScheduleMock(scheduleId, { + name: 'Updated schedule 5', + }); + + const scheduleDataClient = new AttackDiscoveryScheduleDataClient(scheduleDataClientParams); + await scheduleDataClient.updateSchedule(scheduleUpdateData); + + expect(scheduleDataClientParams.rulesClient.update).toHaveBeenCalledWith({ + id: scheduleId, + data: { ...getAttackDiscoveryCreateScheduleMock(), name: 'Updated schedule 5' }, + }); + }); + }); + + describe('deleteSchedule', () => { + it('should call `rulesClient.delete` with the schedule id to delete', async () => { + const scheduleId = 'schedule-3'; + + const scheduleDataClient = new AttackDiscoveryScheduleDataClient(scheduleDataClientParams); + await scheduleDataClient.deleteSchedule({ id: scheduleId }); + + expect(scheduleDataClientParams.rulesClient.delete).toHaveBeenCalledWith({ id: scheduleId }); + }); + }); + + describe('enableSchedule', () => { + it('should call `rulesClient.enableRule` with the schedule id to delete', async () => { + const scheduleId = 'schedule-7'; + + const scheduleDataClient = new AttackDiscoveryScheduleDataClient(scheduleDataClientParams); + await scheduleDataClient.enableSchedule({ id: scheduleId }); + + expect(scheduleDataClientParams.rulesClient.enableRule).toHaveBeenCalledWith({ + id: scheduleId, + }); + }); + }); + + describe('disableSchedule', () => { + it('should call `rulesClient.disableRule` with the schedule id to delete', async () => { + const scheduleId = 'schedule-8'; + + const scheduleDataClient = new AttackDiscoveryScheduleDataClient(scheduleDataClientParams); + await scheduleDataClient.disableSchedule({ id: scheduleId }); + + expect(scheduleDataClientParams.rulesClient.disableRule).toHaveBeenCalledWith({ + id: scheduleId, + }); + }); + }); +}); diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/schedules/data_client/index.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/schedules/data_client/index.ts new file mode 100644 index 0000000000000..e7f436bd5268d --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/schedules/data_client/index.ts @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { RulesClient } from '@kbn/alerting-plugin/server'; +import { CreateRuleData } from '@kbn/alerting-plugin/server/application/rule/methods/create'; +import { UpdateRuleData } from '@kbn/alerting-plugin/server/application/rule/methods/update'; +import { + ATTACK_DISCOVERY_SCHEDULES_ALERT_TYPE_ID, + AttackDiscoveryScheduleParams, +} from '@kbn/elastic-assistant-common'; + +/** + * Params for when creating AttackDiscoveryScheduleDataClient in Request Context Factory. Useful if needing to modify + * configuration after initial plugin start + */ +export interface CreateAttackDiscoveryScheduleDataClientParams { + rulesClient: RulesClient; +} + +export interface AttackDiscoveryScheduleDataClientParams { + rulesClient: RulesClient; +} + +export class AttackDiscoveryScheduleDataClient { + constructor(public readonly options: AttackDiscoveryScheduleDataClientParams) {} + + public findSchedules = async () => { + // TODO: add filtering + // TODO: add sorting + // TODO: add pagination + const rules = await this.options.rulesClient.find({ + options: { + filter: `alert.attributes.alertTypeId: ${ATTACK_DISCOVERY_SCHEDULES_ALERT_TYPE_ID}`, + }, + }); + return rules; + }; + + public getSchedule = async (id: string) => { + const rule = await this.options.rulesClient.get({ id }); + return rule; + }; + + public createSchedule = async (ruleToCreate: CreateRuleData) => { + const rule = await this.options.rulesClient.create({ + data: ruleToCreate, + }); + return rule; + }; + + public updateSchedule = async ( + ruleToUpdate: UpdateRuleData & { id: string } + ) => { + const { id, ...updatePayload } = ruleToUpdate; + const rule = await this.options.rulesClient.update({ + id, + data: updatePayload, + }); + return rule; + }; + + public deleteSchedule = async (ruleToDelete: { id: string }) => { + await this.options.rulesClient.delete(ruleToDelete); + }; + + public enableSchedule = async (ruleToEnable: { id: string }) => { + await this.options.rulesClient.enableRule(ruleToEnable); + }; + + public disableSchedule = async (ruleToDisable: { id: string }) => { + await this.options.rulesClient.disableRule(ruleToDisable); + }; +} diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/schedules/register_schedule/definition.test.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/schedules/register_schedule/definition.test.ts new file mode 100644 index 0000000000000..0829b1ee7b46b --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/schedules/register_schedule/definition.test.ts @@ -0,0 +1,49 @@ +/* + * 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 { loggerMock } from '@kbn/logging-mocks'; +import { + ATTACK_DISCOVERY_SCHEDULES_ALERT_TYPE_ID, + AttackDiscoveryScheduleParams, +} from '@kbn/elastic-assistant-common'; + +import { getAttackDiscoveryScheduleType } from '.'; +import { ATTACK_DISCOVERY_ALERTS_AAD_CONFIG } from '../constants'; + +describe('getAttackDiscoveryScheduleType', () => { + const mockLogger = loggerMock.create(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should return schedule type definition', async () => { + const scheduleType = getAttackDiscoveryScheduleType({ logger: mockLogger }); + + expect(scheduleType).toEqual( + expect.objectContaining({ + id: ATTACK_DISCOVERY_SCHEDULES_ALERT_TYPE_ID, + name: 'Attack Discovery Schedule', + actionGroups: [{ id: 'default', name: 'Default' }], + defaultActionGroupId: 'default', + category: 'securitySolution', + producer: 'assistant', + solution: 'security', + schemas: { + params: { type: 'zod', schema: AttackDiscoveryScheduleParams }, + }, + actionVariables: { + context: [{ name: 'server', description: 'the server' }], + }, + minimumLicenseRequired: 'basic', + isExportable: false, + autoRecoverAlerts: false, + alerts: ATTACK_DISCOVERY_ALERTS_AAD_CONFIG, + }) + ); + }); +}); diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/schedules/register_schedule/definition.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/schedules/register_schedule/definition.ts new file mode 100644 index 0000000000000..f5ea7f4ffa34f --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/schedules/register_schedule/definition.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { DEFAULT_APP_CATEGORIES, Logger } from '@kbn/core/server'; +import { + ATTACK_DISCOVERY_SCHEDULES_ALERT_TYPE_ID, + AttackDiscoveryScheduleParams, +} from '@kbn/elastic-assistant-common'; + +import { ATTACK_DISCOVERY_ALERTS_AAD_CONFIG } from '../constants'; +import { AttackDiscoveryExecutorOptions, AttackDiscoveryScheduleType } from '../types'; +import { attackDiscoveryScheduleExecutor } from './executor'; + +export interface GetAttackDiscoveryScheduleParams { + logger: Logger; +} + +export const getAttackDiscoveryScheduleType = ({ + logger, +}: GetAttackDiscoveryScheduleParams): AttackDiscoveryScheduleType => { + return { + id: ATTACK_DISCOVERY_SCHEDULES_ALERT_TYPE_ID, + name: 'Attack Discovery Schedule', + actionGroups: [{ id: 'default', name: 'Default' }], + defaultActionGroupId: 'default', + category: DEFAULT_APP_CATEGORIES.security.id, + producer: 'assistant', + solution: 'security', + validate: { + params: { + validate: (object: unknown) => { + return AttackDiscoveryScheduleParams.parse(object); + }, + }, + }, + schemas: { + params: { type: 'zod', schema: AttackDiscoveryScheduleParams }, + }, + actionVariables: { + context: [{ name: 'server', description: 'the server' }], + }, + minimumLicenseRequired: 'basic', + isExportable: false, + autoRecoverAlerts: false, + alerts: ATTACK_DISCOVERY_ALERTS_AAD_CONFIG, + executor: async (options: AttackDiscoveryExecutorOptions) => { + return attackDiscoveryScheduleExecutor({ + options, + logger, + }); + }, + }; +}; diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/schedules/register_schedule/executor.test.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/schedules/register_schedule/executor.test.ts new file mode 100644 index 0000000000000..af850219a8584 --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/schedules/register_schedule/executor.test.ts @@ -0,0 +1,37 @@ +/* + * 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 { loggerMock } from '@kbn/logging-mocks'; +import { AlertsClientError, RuleExecutorOptions } from '@kbn/alerting-plugin/server'; + +import { attackDiscoveryScheduleExecutor } from './executor'; + +describe('attackDiscoveryScheduleExecutor', () => { + const mockLogger = loggerMock.create(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should return execution state', async () => { + const results = await attackDiscoveryScheduleExecutor({ + logger: mockLogger, + options: { services: { alertsClient: {} } } as RuleExecutorOptions, + }); + + expect(results).toEqual({ state: {} }); + }); + + it('should throw `AlertsClientError` error if actions client is not available', async () => { + const attackDiscoveryScheduleExecutorPromise = attackDiscoveryScheduleExecutor({ + logger: mockLogger, + options: { services: {} } as RuleExecutorOptions, + }); + + await expect(attackDiscoveryScheduleExecutorPromise).rejects.toBeInstanceOf(AlertsClientError); + }); +}); diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/schedules/register_schedule/executor.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/schedules/register_schedule/executor.ts new file mode 100644 index 0000000000000..7e49ffb1880c7 --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/schedules/register_schedule/executor.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 { Logger } from '@kbn/core/server'; +import { AlertsClientError } from '@kbn/alerting-plugin/server'; +import { ATTACK_DISCOVERY_SCHEDULES_ALERT_TYPE_ID } from '@kbn/elastic-assistant-common'; + +import { AttackDiscoveryExecutorOptions } from '../types'; + +export interface AttackDiscoveryScheduleExecutorParams { + options: AttackDiscoveryExecutorOptions; + logger: Logger; +} + +export const attackDiscoveryScheduleExecutor = async ({ + options, + logger, +}: AttackDiscoveryScheduleExecutorParams) => { + const { services } = options; + const { alertsClient } = services; + if (!alertsClient) { + throw new AlertsClientError(); + } + + // TODO: implement "attack discovery schedule" executor handler + + logger.info( + `Attack discovery schedule "[${ATTACK_DISCOVERY_SCHEDULES_ALERT_TYPE_ID}]" executing...` + ); + + return { state: {} }; +}; diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/schedules/register_schedule/index.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/schedules/register_schedule/index.ts new file mode 100644 index 0000000000000..797716594e870 --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/schedules/register_schedule/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './definition'; diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/schedules/types.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/schedules/types.ts index 60d6e173e811f..35e09b5a77eaf 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/schedules/types.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/schedules/types.ts @@ -5,74 +5,26 @@ * 2.0. */ -import { DefaultAlert } from '@kbn/alerts-as-data-utils'; import { RuleExecutorOptions, RuleType, RuleTypeState } from '@kbn/alerting-plugin/server'; -import { - ALERT_ATTACK_DISCOVERY_ALERTS_CONTEXT_COUNT, - ALERT_ATTACK_DISCOVERY_ALERT_IDS, - ALERT_ATTACK_DISCOVERY_API_CONFIG, - ALERT_ATTACK_DISCOVERY_DETAILS_MARKDOWN, - ALERT_ATTACK_DISCOVERY_DETAILS_MARKDOWN_WITH_REPLACEMENTS, - ALERT_ATTACK_DISCOVERY_ENTITY_SUMMARY_MARKDOWN, - ALERT_ATTACK_DISCOVERY_ENTITY_SUMMARY_MARKDOWN_WITH_REPLACEMENTS, - ALERT_ATTACK_DISCOVERY_MITRE_ATTACK_TACTICS, - ALERT_ATTACK_DISCOVERY_REPLACEMENTS, - ALERT_ATTACK_DISCOVERY_SUMMARY_MARKDOWN, - ALERT_ATTACK_DISCOVERY_SUMMARY_MARKDOWN_WITH_REPLACEMENTS, - ALERT_ATTACK_DISCOVERY_TITLE, - ALERT_ATTACK_DISCOVERY_TITLE_WITH_REPLACEMENTS, - ALERT_ATTACK_DISCOVERY_USERS, - ALERT_ATTACK_DISCOVERY_USER_ID, - ALERT_RISK_SCORE, -} from './fields'; - -export type AttackDiscoveryAlert = DefaultAlert & { - [ALERT_ATTACK_DISCOVERY_ALERTS_CONTEXT_COUNT]?: number; - [ALERT_ATTACK_DISCOVERY_ALERT_IDS]: string[]; - [ALERT_ATTACK_DISCOVERY_API_CONFIG]: { - action_type_id: string; - connector_id: string; - model?: string; - name: string; - provider?: string; - }; - [ALERT_ATTACK_DISCOVERY_DETAILS_MARKDOWN]: string; - [ALERT_ATTACK_DISCOVERY_DETAILS_MARKDOWN_WITH_REPLACEMENTS]: string; - [ALERT_ATTACK_DISCOVERY_ENTITY_SUMMARY_MARKDOWN]?: string; - [ALERT_ATTACK_DISCOVERY_ENTITY_SUMMARY_MARKDOWN_WITH_REPLACEMENTS]?: string; - [ALERT_ATTACK_DISCOVERY_MITRE_ATTACK_TACTICS]?: string[]; - [ALERT_ATTACK_DISCOVERY_REPLACEMENTS]?: Array<{ - value?: string; - uuid?: string; - }>; - [ALERT_ATTACK_DISCOVERY_SUMMARY_MARKDOWN]: string; - [ALERT_ATTACK_DISCOVERY_SUMMARY_MARKDOWN_WITH_REPLACEMENTS]: string; - [ALERT_ATTACK_DISCOVERY_TITLE]: string; - [ALERT_ATTACK_DISCOVERY_TITLE_WITH_REPLACEMENTS]: string; - [ALERT_ATTACK_DISCOVERY_USER_ID]?: string; - [ALERT_ATTACK_DISCOVERY_USERS]: Array<{ - id?: string; - name?: string; - }>; - [ALERT_RISK_SCORE]?: number; -}; +import { SecurityAttackDiscoveryAlert } from '@kbn/alerts-as-data-utils'; +import { AttackDiscoveryScheduleParams } from '@kbn/elastic-assistant-common'; export type AttackDiscoveryExecutorOptions = RuleExecutorOptions< - {}, + AttackDiscoveryScheduleParams, RuleTypeState, {}, {}, 'default', - AttackDiscoveryAlert + SecurityAttackDiscoveryAlert >; export type AttackDiscoveryScheduleType = RuleType< - {}, + AttackDiscoveryScheduleParams, never, RuleTypeState, {}, {}, 'default', never, - AttackDiscoveryAlert + SecurityAttackDiscoveryAlert >; diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/plugin.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/plugin.ts index bf34bcb6dc827..49bb6c2bba4f1 100755 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/plugin.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/plugin.ts @@ -29,6 +29,7 @@ import { PLUGIN_ID } from '../common/constants'; import { registerRoutes } from './routes/register_routes'; import { CallbackIds, appContextService } from './services/app_context'; import { createGetElserId, removeLegacyQuickPrompt } from './ai_assistant_service/helpers'; +import { getAttackDiscoveryScheduleType } from './lib/attack_discovery/schedules/register_schedule/definition'; export class ElasticAssistantPlugin implements @@ -104,7 +105,14 @@ export class ElasticAssistantPlugin featureFlags.getBooleanValue(ATTACK_DISCOVERY_SCHEDULES_ENABLED_FEATURE_FLAG, false), // add more feature flags here ]).then(([assistantAttackDiscoverySchedulingEnabled]) => { - // TODO: use `assistantAttackDiscoverySchedulingEnabled` to conditionally create alerts index + if (assistantAttackDiscoverySchedulingEnabled) { + // Register Attack Discovery Schedule type + plugins.alerting.registerType( + getAttackDiscoveryScheduleType({ + logger: this.logger, + }) + ); + } }); }) .catch((error) => { diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/schedules/create.test.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/schedules/create.test.ts new file mode 100644 index 0000000000000..02f7db1460bdf --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/schedules/create.test.ts @@ -0,0 +1,116 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { elasticsearchServiceMock } from '@kbn/core-elasticsearch-server-mocks'; +import { CreateAttackDiscoverySchedulesRequestBody } from '@kbn/elastic-assistant-common'; +import { OpenAiProviderType } from '@kbn/stack-connectors-plugin/common/openai/constants'; + +import { createAttackDiscoverySchedulesRoute } from './create'; +import { serverMock } from '../../../__mocks__/server'; +import { requestContextMock } from '../../../__mocks__/request_context'; +import { createAttackDiscoverySchedulesRequest } from '../../../__mocks__/request'; +import { getInternalAttackDiscoveryScheduleMock } from '../../../__mocks__/attack_discovery_schedules.mock'; +import { AttackDiscoveryScheduleDataClient } from '../../../lib/attack_discovery/schedules/data_client'; + +const { clients, context } = requestContextMock.createTools(); +const server: ReturnType = serverMock.create(); +clients.core.elasticsearch.client = elasticsearchServiceMock.createScopedClusterClient(); + +const createAttackDiscoverySchedule = jest.fn(); +const mockSchedulingDataClient = { + findSchedules: jest.fn(), + getSchedule: jest.fn(), + createSchedule: createAttackDiscoverySchedule, + updateSchedule: jest.fn(), + deleteSchedule: jest.fn(), + enableSchedule: jest.fn(), + disableSchedule: jest.fn(), +} as unknown as AttackDiscoveryScheduleDataClient; +const mockApiConfig = { + connectorId: 'connector-id', + actionTypeId: '.bedrock', + model: 'model', + provider: OpenAiProviderType.OpenAi, +}; +const mockRequestBody: CreateAttackDiscoverySchedulesRequestBody = { + name: 'Test Schedule 1', + schedule: { + interval: '10m', + }, + params: { + alertsIndexPattern: '.alerts-security.alerts-default', + apiConfig: mockApiConfig, + end: 'now', + size: 25, + start: 'now-24h', + }, + enabled: true, +}; + +describe('createAttackDiscoverySchedulesRoute', () => { + beforeEach(() => { + jest.clearAllMocks(); + context.elasticAssistant.getAttackDiscoverySchedulingDataClient.mockResolvedValue( + mockSchedulingDataClient + ); + context.core.featureFlags.getBooleanValue.mockResolvedValue(true); + createAttackDiscoverySchedulesRoute(server.router); + createAttackDiscoverySchedule.mockResolvedValue( + getInternalAttackDiscoveryScheduleMock(mockRequestBody) + ); + }); + + it('should handle successful request', async () => { + const response = await server.inject( + createAttackDiscoverySchedulesRequest(mockRequestBody), + requestContextMock.convertContext(context) + ); + expect(response.status).toEqual(200); + expect(response.body).toEqual(expect.objectContaining({ ...mockRequestBody })); + }); + + it('should handle missing data client', async () => { + context.elasticAssistant.getAttackDiscoverySchedulingDataClient.mockResolvedValue(null); + const response = await server.inject( + createAttackDiscoverySchedulesRequest(mockRequestBody), + requestContextMock.convertContext(context) + ); + + expect(response.status).toEqual(500); + expect(response.body).toEqual({ + message: 'Attack discovery data client not initialized', + status_code: 500, + }); + }); + + it('should handle `dataClient.createSchedule` error', async () => { + (createAttackDiscoverySchedule as jest.Mock).mockRejectedValue(new Error('Oh no!')); + const response = await server.inject( + createAttackDiscoverySchedulesRequest(mockRequestBody), + requestContextMock.convertContext(context) + ); + expect(response.status).toEqual(500); + expect(response.body).toEqual({ + message: { + error: 'Oh no!', + success: false, + }, + status_code: 500, + }); + }); + + describe('Disabled feature flag', () => { + it('should return a 404 if scheduling feature is not registered', async () => { + context.core.featureFlags.getBooleanValue.mockResolvedValue(false); + const response = await server.inject( + createAttackDiscoverySchedulesRequest(mockRequestBody), + requestContextMock.convertContext(context) + ); + expect(response.status).toEqual(404); + }); + }); +}); diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/schedules/create.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/schedules/create.ts new file mode 100644 index 0000000000000..10cb6bc145495 --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/schedules/create.ts @@ -0,0 +1,113 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { IKibanaResponse, IRouter, Logger } from '@kbn/core/server'; +import { transformError } from '@kbn/securitysolution-es-utils'; +import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; +import { + API_VERSIONS, + ATTACK_DISCOVERY_SCHEDULES_ALERT_TYPE_ID, + CreateAttackDiscoverySchedulesRequestBody, + CreateAttackDiscoverySchedulesResponse, +} from '@kbn/elastic-assistant-common'; + +import { buildResponse } from '../../../lib/build_response'; +import { ATTACK_DISCOVERY_SCHEDULES } from '../../../../common/constants'; +import { ElasticAssistantRequestHandlerContext } from '../../../types'; +import { convertAlertingRuleToSchedule } from './utils/convert_alerting_rule_to_schedule'; +import { performChecks } from '../../helpers'; +import { isFeatureAvailable } from './utils/is_feature_available'; + +export const createAttackDiscoverySchedulesRoute = ( + router: IRouter +): void => { + router.versioned + .post({ + access: 'internal', + path: ATTACK_DISCOVERY_SCHEDULES, + security: { + authz: { + requiredPrivileges: ['elasticAssistant'], + }, + }, + }) + .addVersion( + { + version: API_VERSIONS.internal.v1, + validate: { + request: { + body: buildRouteValidationWithZod(CreateAttackDiscoverySchedulesRequestBody), + }, + response: { + 200: { + body: { + custom: buildRouteValidationWithZod(CreateAttackDiscoverySchedulesResponse), + }, + }, + }, + }, + }, + async ( + context, + request, + response + ): Promise> => { + const ctx = await context.resolve(['core', 'elasticAssistant', 'licensing']); + const resp = buildResponse(response); + const assistantContext = await context.elasticAssistant; + const logger: Logger = assistantContext.logger; + + // Check if scheduling feature available + if (!(await isFeatureAvailable(ctx))) { + return response.notFound(); + } + + // Perform license and authenticated user + const checkResponse = await performChecks({ + context: ctx, + request, + response, + }); + + if (!checkResponse.isSuccess) { + return checkResponse.response; + } + + const { actions = [], enabled = false, ...restScheduleAttributes } = request.body; + + try { + const dataClient = await assistantContext.getAttackDiscoverySchedulingDataClient(); + if (!dataClient) { + return resp.error({ + body: `Attack discovery data client not initialized`, + statusCode: 500, + }); + } + + const alertingRule = await dataClient.createSchedule({ + actions, + alertTypeId: ATTACK_DISCOVERY_SCHEDULES_ALERT_TYPE_ID, + consumer: 'siem', + enabled, + tags: [], + ...restScheduleAttributes, + }); + const schedule = convertAlertingRuleToSchedule(alertingRule); + + return response.ok({ body: schedule }); + } catch (err) { + logger.error(err); + const error = transformError(err); + + return resp.error({ + body: { success: false, error: error.message }, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/schedules/delete.test.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/schedules/delete.test.ts new file mode 100644 index 0000000000000..1cf0c8deebafa --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/schedules/delete.test.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 { elasticsearchServiceMock } from '@kbn/core-elasticsearch-server-mocks'; + +import { deleteAttackDiscoverySchedulesRoute } from './delete'; +import { serverMock } from '../../../__mocks__/server'; +import { requestContextMock } from '../../../__mocks__/request_context'; +import { deleteAttackDiscoverySchedulesRequest } from '../../../__mocks__/request'; +import { AttackDiscoveryScheduleDataClient } from '../../../lib/attack_discovery/schedules/data_client'; + +const { clients, context } = requestContextMock.createTools(); +const server: ReturnType = serverMock.create(); +clients.core.elasticsearch.client = elasticsearchServiceMock.createScopedClusterClient(); + +const deleteAttackDiscoverySchedule = jest.fn(); +const mockSchedulingDataClient = { + findSchedules: jest.fn(), + getSchedule: jest.fn(), + createSchedule: jest.fn(), + updateSchedule: jest.fn(), + deleteSchedule: deleteAttackDiscoverySchedule, + enableSchedule: jest.fn(), + disableSchedule: jest.fn(), +} as unknown as AttackDiscoveryScheduleDataClient; + +describe('deleteAttackDiscoverySchedulesRoute', () => { + beforeEach(() => { + jest.clearAllMocks(); + context.elasticAssistant.getAttackDiscoverySchedulingDataClient.mockResolvedValue( + mockSchedulingDataClient + ); + context.core.featureFlags.getBooleanValue.mockResolvedValue(true); + deleteAttackDiscoverySchedulesRoute(server.router); + }); + + it('should handle successful request', async () => { + const response = await server.inject( + deleteAttackDiscoverySchedulesRequest('schedule-1'), + requestContextMock.convertContext(context) + ); + expect(response.status).toEqual(200); + expect(response.body).toEqual({ id: 'schedule-1' }); + }); + + it('should handle missing data client', async () => { + context.elasticAssistant.getAttackDiscoverySchedulingDataClient.mockResolvedValue(null); + const response = await server.inject( + deleteAttackDiscoverySchedulesRequest('schedule-2'), + requestContextMock.convertContext(context) + ); + + expect(response.status).toEqual(500); + expect(response.body).toEqual({ + message: 'Attack discovery data client not initialized', + status_code: 500, + }); + }); + + it('should handle `dataClient.deleteSchedule` error', async () => { + (deleteAttackDiscoverySchedule as jest.Mock).mockRejectedValue(new Error('Oh no!')); + const response = await server.inject( + deleteAttackDiscoverySchedulesRequest('schedule-3'), + requestContextMock.convertContext(context) + ); + expect(response.status).toEqual(500); + expect(response.body).toEqual({ + message: { + error: 'Oh no!', + success: false, + }, + status_code: 500, + }); + }); + + describe('Disabled feature flag', () => { + it('should return a 404 if scheduling feature is not registered', async () => { + context.core.featureFlags.getBooleanValue.mockResolvedValue(false); + const response = await server.inject( + deleteAttackDiscoverySchedulesRequest('schedule-4'), + requestContextMock.convertContext(context) + ); + expect(response.status).toEqual(404); + }); + }); +}); diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/schedules/delete.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/schedules/delete.ts new file mode 100644 index 0000000000000..5f44f8fe3958e --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/schedules/delete.ts @@ -0,0 +1,103 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { IKibanaResponse, IRouter, Logger } from '@kbn/core/server'; +import { transformError } from '@kbn/securitysolution-es-utils'; +import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; + +import { + API_VERSIONS, + DeleteAttackDiscoverySchedulesRequestParams, + DeleteAttackDiscoverySchedulesResponse, +} from '@kbn/elastic-assistant-common'; +import { buildResponse } from '../../../lib/build_response'; +import { ATTACK_DISCOVERY_SCHEDULES_BY_ID } from '../../../../common/constants'; +import { ElasticAssistantRequestHandlerContext } from '../../../types'; +import { performChecks } from '../../helpers'; +import { isFeatureAvailable } from './utils/is_feature_available'; + +export const deleteAttackDiscoverySchedulesRoute = ( + router: IRouter +): void => { + router.versioned + .delete({ + access: 'internal', + path: ATTACK_DISCOVERY_SCHEDULES_BY_ID, + security: { + authz: { + requiredPrivileges: ['elasticAssistant'], + }, + }, + }) + .addVersion( + { + version: API_VERSIONS.internal.v1, + validate: { + request: { + params: buildRouteValidationWithZod(DeleteAttackDiscoverySchedulesRequestParams), + }, + response: { + 200: { + body: { + custom: buildRouteValidationWithZod(DeleteAttackDiscoverySchedulesResponse), + }, + }, + }, + }, + }, + async ( + context, + request, + response + ): Promise> => { + const ctx = await context.resolve(['core', 'elasticAssistant', 'licensing']); + const resp = buildResponse(response); + const assistantContext = await context.elasticAssistant; + const logger: Logger = assistantContext.logger; + + // Check if scheduling feature available + if (!(await isFeatureAvailable(ctx))) { + return response.notFound(); + } + + // Perform license and authenticated user + const checkResponse = await performChecks({ + context: ctx, + request, + response, + }); + + if (!checkResponse.isSuccess) { + return checkResponse.response; + } + + const { id } = request.params; + + try { + const dataClient = await assistantContext.getAttackDiscoverySchedulingDataClient(); + if (!dataClient) { + return resp.error({ + body: `Attack discovery data client not initialized`, + statusCode: 500, + }); + } + + await dataClient.deleteSchedule({ id }); + + return response.ok({ body: { id } }); + } catch (err) { + logger.error(err); + const error = transformError(err); + + return resp.error({ + body: { success: false, error: error.message }, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/schedules/disable.test.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/schedules/disable.test.ts new file mode 100644 index 0000000000000..426326539f298 --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/schedules/disable.test.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 { elasticsearchServiceMock } from '@kbn/core-elasticsearch-server-mocks'; + +import { disableAttackDiscoverySchedulesRoute } from './disable'; +import { serverMock } from '../../../__mocks__/server'; +import { requestContextMock } from '../../../__mocks__/request_context'; +import { disableAttackDiscoverySchedulesRequest } from '../../../__mocks__/request'; +import { AttackDiscoveryScheduleDataClient } from '../../../lib/attack_discovery/schedules/data_client'; + +const { clients, context } = requestContextMock.createTools(); +const server: ReturnType = serverMock.create(); +clients.core.elasticsearch.client = elasticsearchServiceMock.createScopedClusterClient(); + +const disableAttackDiscoverySchedule = jest.fn(); +const mockSchedulingDataClient = { + findSchedules: jest.fn(), + getSchedule: jest.fn(), + createSchedule: jest.fn(), + updateSchedule: jest.fn(), + deleteSchedule: jest.fn(), + enableSchedule: jest.fn(), + disableSchedule: disableAttackDiscoverySchedule, +} as unknown as AttackDiscoveryScheduleDataClient; + +describe('disableAttackDiscoverySchedulesRoute', () => { + beforeEach(() => { + jest.clearAllMocks(); + context.elasticAssistant.getAttackDiscoverySchedulingDataClient.mockResolvedValue( + mockSchedulingDataClient + ); + context.core.featureFlags.getBooleanValue.mockResolvedValue(true); + disableAttackDiscoverySchedulesRoute(server.router); + }); + + it('should handle successful request', async () => { + const response = await server.inject( + disableAttackDiscoverySchedulesRequest('schedule-1'), + requestContextMock.convertContext(context) + ); + expect(response.status).toEqual(200); + expect(response.body).toEqual({ id: 'schedule-1' }); + }); + + it('should handle missing data client', async () => { + context.elasticAssistant.getAttackDiscoverySchedulingDataClient.mockResolvedValue(null); + const response = await server.inject( + disableAttackDiscoverySchedulesRequest('schedule-2'), + requestContextMock.convertContext(context) + ); + + expect(response.status).toEqual(500); + expect(response.body).toEqual({ + message: 'Attack discovery data client not initialized', + status_code: 500, + }); + }); + + it('should handle `dataClient.disableSchedule` error', async () => { + (disableAttackDiscoverySchedule as jest.Mock).mockRejectedValue(new Error('Oh no!')); + const response = await server.inject( + disableAttackDiscoverySchedulesRequest('schedule-3'), + requestContextMock.convertContext(context) + ); + expect(response.status).toEqual(500); + expect(response.body).toEqual({ + message: { + error: 'Oh no!', + success: false, + }, + status_code: 500, + }); + }); + + describe('Disabled feature flag', () => { + it('should return a 404 if scheduling feature is not registered', async () => { + context.core.featureFlags.getBooleanValue.mockResolvedValue(false); + const response = await server.inject( + disableAttackDiscoverySchedulesRequest('schedule-4'), + requestContextMock.convertContext(context) + ); + expect(response.status).toEqual(404); + }); + }); +}); diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/schedules/disable.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/schedules/disable.ts new file mode 100644 index 0000000000000..377c806b2f261 --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/schedules/disable.ts @@ -0,0 +1,103 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { IKibanaResponse, IRouter, Logger } from '@kbn/core/server'; +import { transformError } from '@kbn/securitysolution-es-utils'; +import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; + +import { + API_VERSIONS, + DisableAttackDiscoverySchedulesRequestParams, + DisableAttackDiscoverySchedulesResponse, +} from '@kbn/elastic-assistant-common'; +import { buildResponse } from '../../../lib/build_response'; +import { ATTACK_DISCOVERY_SCHEDULES_BY_ID_DISABLE } from '../../../../common/constants'; +import { ElasticAssistantRequestHandlerContext } from '../../../types'; +import { performChecks } from '../../helpers'; +import { isFeatureAvailable } from './utils/is_feature_available'; + +export const disableAttackDiscoverySchedulesRoute = ( + router: IRouter +): void => { + router.versioned + .post({ + access: 'internal', + path: ATTACK_DISCOVERY_SCHEDULES_BY_ID_DISABLE, + security: { + authz: { + requiredPrivileges: ['elasticAssistant'], + }, + }, + }) + .addVersion( + { + version: API_VERSIONS.internal.v1, + validate: { + request: { + params: buildRouteValidationWithZod(DisableAttackDiscoverySchedulesRequestParams), + }, + response: { + 200: { + body: { + custom: buildRouteValidationWithZod(DisableAttackDiscoverySchedulesResponse), + }, + }, + }, + }, + }, + async ( + context, + request, + response + ): Promise> => { + const ctx = await context.resolve(['core', 'elasticAssistant', 'licensing']); + const resp = buildResponse(response); + const assistantContext = await context.elasticAssistant; + const logger: Logger = assistantContext.logger; + + // Check if scheduling feature available + if (!(await isFeatureAvailable(ctx))) { + return response.notFound(); + } + + // Perform license and authenticated user + const checkResponse = await performChecks({ + context: ctx, + request, + response, + }); + + if (!checkResponse.isSuccess) { + return checkResponse.response; + } + + const { id } = request.params; + + try { + const dataClient = await assistantContext.getAttackDiscoverySchedulingDataClient(); + if (!dataClient) { + return resp.error({ + body: `Attack discovery data client not initialized`, + statusCode: 500, + }); + } + + await dataClient.disableSchedule({ id }); + + return response.ok({ body: { id } }); + } catch (err) { + logger.error(err); + const error = transformError(err); + + return resp.error({ + body: { success: false, error: error.message }, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/schedules/enable.test.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/schedules/enable.test.ts new file mode 100644 index 0000000000000..5522ab299a3ac --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/schedules/enable.test.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 { elasticsearchServiceMock } from '@kbn/core-elasticsearch-server-mocks'; + +import { enableAttackDiscoverySchedulesRoute } from './enable'; +import { serverMock } from '../../../__mocks__/server'; +import { requestContextMock } from '../../../__mocks__/request_context'; +import { enableAttackDiscoverySchedulesRequest } from '../../../__mocks__/request'; +import { AttackDiscoveryScheduleDataClient } from '../../../lib/attack_discovery/schedules/data_client'; + +const { clients, context } = requestContextMock.createTools(); +const server: ReturnType = serverMock.create(); +clients.core.elasticsearch.client = elasticsearchServiceMock.createScopedClusterClient(); + +const enableAttackDiscoverySchedule = jest.fn(); +const mockSchedulingDataClient = { + findSchedules: jest.fn(), + getSchedule: jest.fn(), + createSchedule: jest.fn(), + updateSchedule: jest.fn(), + deleteSchedule: jest.fn(), + enableSchedule: enableAttackDiscoverySchedule, + disableSchedule: jest.fn(), +} as unknown as AttackDiscoveryScheduleDataClient; + +describe('enableAttackDiscoverySchedulesRoute', () => { + beforeEach(() => { + jest.clearAllMocks(); + context.elasticAssistant.getAttackDiscoverySchedulingDataClient.mockResolvedValue( + mockSchedulingDataClient + ); + context.core.featureFlags.getBooleanValue.mockResolvedValue(true); + enableAttackDiscoverySchedulesRoute(server.router); + }); + + it('should handle successful request', async () => { + const response = await server.inject( + enableAttackDiscoverySchedulesRequest('schedule-1'), + requestContextMock.convertContext(context) + ); + expect(response.status).toEqual(200); + expect(response.body).toEqual({ id: 'schedule-1' }); + }); + + it('should handle missing data client', async () => { + context.elasticAssistant.getAttackDiscoverySchedulingDataClient.mockResolvedValue(null); + const response = await server.inject( + enableAttackDiscoverySchedulesRequest('schedule-2'), + requestContextMock.convertContext(context) + ); + + expect(response.status).toEqual(500); + expect(response.body).toEqual({ + message: 'Attack discovery data client not initialized', + status_code: 500, + }); + }); + + it('should handle `dataClient.enableSchedule` error', async () => { + (enableAttackDiscoverySchedule as jest.Mock).mockRejectedValue(new Error('Oh no!')); + const response = await server.inject( + enableAttackDiscoverySchedulesRequest('schedule-3'), + requestContextMock.convertContext(context) + ); + expect(response.status).toEqual(500); + expect(response.body).toEqual({ + message: { + error: 'Oh no!', + success: false, + }, + status_code: 500, + }); + }); + + describe('Disabled feature flag', () => { + it('should return a 404 if scheduling feature is not registered', async () => { + context.core.featureFlags.getBooleanValue.mockResolvedValue(false); + const response = await server.inject( + enableAttackDiscoverySchedulesRequest('schedule-4'), + requestContextMock.convertContext(context) + ); + expect(response.status).toEqual(404); + }); + }); +}); diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/schedules/enable.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/schedules/enable.ts new file mode 100644 index 0000000000000..0239c764a0685 --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/schedules/enable.ts @@ -0,0 +1,103 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { IKibanaResponse, IRouter, Logger } from '@kbn/core/server'; +import { transformError } from '@kbn/securitysolution-es-utils'; +import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; + +import { + API_VERSIONS, + EnableAttackDiscoverySchedulesRequestParams, + EnableAttackDiscoverySchedulesResponse, +} from '@kbn/elastic-assistant-common'; +import { buildResponse } from '../../../lib/build_response'; +import { ATTACK_DISCOVERY_SCHEDULES_BY_ID_ENABLE } from '../../../../common/constants'; +import { ElasticAssistantRequestHandlerContext } from '../../../types'; +import { performChecks } from '../../helpers'; +import { isFeatureAvailable } from './utils/is_feature_available'; + +export const enableAttackDiscoverySchedulesRoute = ( + router: IRouter +): void => { + router.versioned + .post({ + access: 'internal', + path: ATTACK_DISCOVERY_SCHEDULES_BY_ID_ENABLE, + security: { + authz: { + requiredPrivileges: ['elasticAssistant'], + }, + }, + }) + .addVersion( + { + version: API_VERSIONS.internal.v1, + validate: { + request: { + params: buildRouteValidationWithZod(EnableAttackDiscoverySchedulesRequestParams), + }, + response: { + 200: { + body: { + custom: buildRouteValidationWithZod(EnableAttackDiscoverySchedulesResponse), + }, + }, + }, + }, + }, + async ( + context, + request, + response + ): Promise> => { + const ctx = await context.resolve(['core', 'elasticAssistant', 'licensing']); + const resp = buildResponse(response); + const assistantContext = await context.elasticAssistant; + const logger: Logger = assistantContext.logger; + + // Check if scheduling feature available + if (!(await isFeatureAvailable(ctx))) { + return response.notFound(); + } + + // Perform license and authenticated user + const checkResponse = await performChecks({ + context: ctx, + request, + response, + }); + + if (!checkResponse.isSuccess) { + return checkResponse.response; + } + + const { id } = request.params; + + try { + const dataClient = await assistantContext.getAttackDiscoverySchedulingDataClient(); + if (!dataClient) { + return resp.error({ + body: `Attack discovery data client not initialized`, + statusCode: 500, + }); + } + + await dataClient.enableSchedule({ id }); + + return response.ok({ body: { id } }); + } catch (err) { + logger.error(err); + const error = transformError(err); + + return resp.error({ + body: { success: false, error: error.message }, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/schedules/find.test.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/schedules/find.test.ts new file mode 100644 index 0000000000000..6864a0f2eadb2 --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/schedules/find.test.ts @@ -0,0 +1,125 @@ +/* + * 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 { elasticsearchServiceMock } from '@kbn/core-elasticsearch-server-mocks'; +import { OpenAiProviderType } from '@kbn/stack-connectors-plugin/common/openai/constants'; + +import { findAttackDiscoverySchedulesRoute } from './find'; +import { serverMock } from '../../../__mocks__/server'; +import { requestContextMock } from '../../../__mocks__/request_context'; +import { findAttackDiscoverySchedulesRequest } from '../../../__mocks__/request'; +import { + getInternalFindAttackDiscoverySchedulesMock, + getInternalAttackDiscoveryScheduleMock, +} from '../../../__mocks__/attack_discovery_schedules.mock'; +import { AttackDiscoveryScheduleDataClient } from '../../../lib/attack_discovery/schedules/data_client'; + +const { clients, context } = requestContextMock.createTools(); +const server: ReturnType = serverMock.create(); +clients.core.elasticsearch.client = elasticsearchServiceMock.createScopedClusterClient(); + +const findAttackDiscoverySchedule = jest.fn(); +const mockSchedulingDataClient = { + findSchedules: findAttackDiscoverySchedule, + getSchedule: jest.fn(), + createSchedule: jest.fn(), + updateSchedule: jest.fn(), + deleteSchedule: jest.fn(), + enableSchedule: jest.fn(), + disableSchedule: jest.fn(), +} as unknown as AttackDiscoveryScheduleDataClient; +const mockApiConfig = { + connectorId: 'connector-id', + actionTypeId: '.bedrock', + model: 'model', + provider: OpenAiProviderType.OpenAi, +}; +const basicAttackDiscoveryScheduleMock = { + name: 'Test Schedule', + schedule: { + interval: '100m', + }, + params: { + alertsIndexPattern: '.alerts-security.alerts-default', + apiConfig: mockApiConfig, + end: 'now', + size: 25, + start: 'now-24h', + }, + enabled: true, +}; + +describe('findAttackDiscoverySchedulesRoute', () => { + beforeEach(() => { + jest.clearAllMocks(); + context.elasticAssistant.getAttackDiscoverySchedulingDataClient.mockResolvedValue( + mockSchedulingDataClient + ); + context.core.featureFlags.getBooleanValue.mockResolvedValue(true); + findAttackDiscoverySchedulesRoute(server.router); + findAttackDiscoverySchedule.mockResolvedValue( + getInternalFindAttackDiscoverySchedulesMock([ + getInternalAttackDiscoveryScheduleMock(basicAttackDiscoveryScheduleMock), + ]) + ); + }); + + it('should handle successful request', async () => { + const response = await server.inject( + findAttackDiscoverySchedulesRequest(), + requestContextMock.convertContext(context) + ); + expect(response.status).toEqual(200); + expect(response.body).toEqual({ + page: 1, + perPage: 20, + total: 1, + data: [expect.objectContaining(basicAttackDiscoveryScheduleMock)], + }); + }); + + it('should handle missing data client', async () => { + context.elasticAssistant.getAttackDiscoverySchedulingDataClient.mockResolvedValue(null); + const response = await server.inject( + findAttackDiscoverySchedulesRequest(), + requestContextMock.convertContext(context) + ); + + expect(response.status).toEqual(500); + expect(response.body).toEqual({ + message: 'Attack discovery data client not initialized', + status_code: 500, + }); + }); + + it('should handle `dataClient.findSchedules` error', async () => { + (findAttackDiscoverySchedule as jest.Mock).mockRejectedValue(new Error('Oh no!')); + const response = await server.inject( + findAttackDiscoverySchedulesRequest(), + requestContextMock.convertContext(context) + ); + expect(response.status).toEqual(500); + expect(response.body).toEqual({ + message: { + error: 'Oh no!', + success: false, + }, + status_code: 500, + }); + }); + + describe('Disabled feature flag', () => { + it('should return a 404 if scheduling feature is not registered', async () => { + context.core.featureFlags.getBooleanValue.mockResolvedValue(false); + const response = await server.inject( + findAttackDiscoverySchedulesRequest(), + requestContextMock.convertContext(context) + ); + expect(response.status).toEqual(404); + }); + }); +}); diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/schedules/find.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/schedules/find.ts new file mode 100644 index 0000000000000..a9bc0412257fd --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/schedules/find.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 type { IKibanaResponse, IRouter, Logger } from '@kbn/core/server'; +import { transformError } from '@kbn/securitysolution-es-utils'; +import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; + +import { API_VERSIONS, FindAttackDiscoverySchedulesResponse } from '@kbn/elastic-assistant-common'; +import { buildResponse } from '../../../lib/build_response'; +import { ATTACK_DISCOVERY_SCHEDULES_FIND } from '../../../../common/constants'; +import { ElasticAssistantRequestHandlerContext } from '../../../types'; +import { convertAlertingRuleToSchedule } from './utils/convert_alerting_rule_to_schedule'; +import { performChecks } from '../../helpers'; +import { isFeatureAvailable } from './utils/is_feature_available'; + +export const findAttackDiscoverySchedulesRoute = ( + router: IRouter +): void => { + router.versioned + .get({ + access: 'internal', + path: ATTACK_DISCOVERY_SCHEDULES_FIND, + security: { + authz: { + requiredPrivileges: ['elasticAssistant'], + }, + }, + }) + .addVersion( + { + version: API_VERSIONS.internal.v1, + validate: { + response: { + 200: { + body: { + custom: buildRouteValidationWithZod(FindAttackDiscoverySchedulesResponse), + }, + }, + }, + }, + }, + async ( + context, + request, + response + ): Promise> => { + const ctx = await context.resolve(['core', 'elasticAssistant', 'licensing']); + const resp = buildResponse(response); + const assistantContext = await context.elasticAssistant; + const logger: Logger = assistantContext.logger; + + // Check if scheduling feature available + if (!(await isFeatureAvailable(ctx))) { + return response.notFound(); + } + + // Perform license and authenticated user + const checkResponse = await performChecks({ + context: ctx, + request, + response, + }); + + if (!checkResponse.isSuccess) { + return checkResponse.response; + } + + try { + const dataClient = await assistantContext.getAttackDiscoverySchedulingDataClient(); + if (!dataClient) { + return resp.error({ + body: `Attack discovery data client not initialized`, + statusCode: 500, + }); + } + + const results = await dataClient.findSchedules(); + const { page, perPage, total, data } = results; + + const schedules = data.map(convertAlertingRuleToSchedule); + + return response.ok({ body: { page, perPage, total, data: schedules } }); + } catch (err) { + logger.error(err); + const error = transformError(err); + + return resp.error({ + body: { success: false, error: error.message }, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/schedules/get.test.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/schedules/get.test.ts new file mode 100644 index 0000000000000..be57c6dc90efe --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/schedules/get.test.ts @@ -0,0 +1,115 @@ +/* + * 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 { elasticsearchServiceMock } from '@kbn/core-elasticsearch-server-mocks'; +import { OpenAiProviderType } from '@kbn/stack-connectors-plugin/common/openai/constants'; + +import { getAttackDiscoverySchedulesRoute } from './get'; +import { serverMock } from '../../../__mocks__/server'; +import { requestContextMock } from '../../../__mocks__/request_context'; +import { getAttackDiscoverySchedulesRequest } from '../../../__mocks__/request'; +import { getInternalAttackDiscoveryScheduleMock } from '../../../__mocks__/attack_discovery_schedules.mock'; +import { AttackDiscoveryScheduleDataClient } from '../../../lib/attack_discovery/schedules/data_client'; + +const { clients, context } = requestContextMock.createTools(); +const server: ReturnType = serverMock.create(); +clients.core.elasticsearch.client = elasticsearchServiceMock.createScopedClusterClient(); + +const getAttackDiscoverySchedule = jest.fn(); +const mockSchedulingDataClient = { + findSchedules: jest.fn(), + getSchedule: getAttackDiscoverySchedule, + createSchedule: jest.fn(), + updateSchedule: jest.fn(), + deleteSchedule: jest.fn(), + enableSchedule: jest.fn(), + disableSchedule: jest.fn(), +} as unknown as AttackDiscoveryScheduleDataClient; +const mockApiConfig = { + connectorId: 'connector-id', + actionTypeId: '.bedrock', + model: 'model', + provider: OpenAiProviderType.OpenAi, +}; +const basicAttackDiscoveryScheduleMock = { + name: 'Test Schedule', + schedule: { + interval: '100m', + }, + params: { + alertsIndexPattern: '.alerts-security.alerts-default', + apiConfig: mockApiConfig, + end: 'now', + size: 25, + start: 'now-24h', + }, + enabled: true, +}; + +describe('getAttackDiscoverySchedulesRoute', () => { + beforeEach(() => { + jest.clearAllMocks(); + context.elasticAssistant.getAttackDiscoverySchedulingDataClient.mockResolvedValue( + mockSchedulingDataClient + ); + context.core.featureFlags.getBooleanValue.mockResolvedValue(true); + getAttackDiscoverySchedulesRoute(server.router); + getAttackDiscoverySchedule.mockResolvedValue( + getInternalAttackDiscoveryScheduleMock(basicAttackDiscoveryScheduleMock) + ); + }); + + it('should handle successful request', async () => { + const response = await server.inject( + getAttackDiscoverySchedulesRequest('schedule-1'), + requestContextMock.convertContext(context) + ); + expect(response.status).toEqual(200); + expect(response.body).toEqual(expect.objectContaining(basicAttackDiscoveryScheduleMock)); + }); + + it('should handle missing data client', async () => { + context.elasticAssistant.getAttackDiscoverySchedulingDataClient.mockResolvedValue(null); + const response = await server.inject( + getAttackDiscoverySchedulesRequest('schedule-2'), + requestContextMock.convertContext(context) + ); + + expect(response.status).toEqual(500); + expect(response.body).toEqual({ + message: 'Attack discovery data client not initialized', + status_code: 500, + }); + }); + + it('should handle `dataClient.getSchedule` error', async () => { + (getAttackDiscoverySchedule as jest.Mock).mockRejectedValue(new Error('Oh no!')); + const response = await server.inject( + getAttackDiscoverySchedulesRequest('schedule-3'), + requestContextMock.convertContext(context) + ); + expect(response.status).toEqual(500); + expect(response.body).toEqual({ + message: { + error: 'Oh no!', + success: false, + }, + status_code: 500, + }); + }); + + describe('Disabled feature flag', () => { + it('should return a 404 if scheduling feature is not registered', async () => { + context.core.featureFlags.getBooleanValue.mockResolvedValue(false); + const response = await server.inject( + getAttackDiscoverySchedulesRequest('schedule-4'), + requestContextMock.convertContext(context) + ); + expect(response.status).toEqual(404); + }); + }); +}); diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/schedules/get.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/schedules/get.ts new file mode 100644 index 0000000000000..956edf4683b04 --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/schedules/get.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 type { IKibanaResponse, IRouter, Logger } from '@kbn/core/server'; +import { transformError } from '@kbn/securitysolution-es-utils'; +import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; + +import { + API_VERSIONS, + GetAttackDiscoverySchedulesRequestParams, + GetAttackDiscoverySchedulesResponse, +} from '@kbn/elastic-assistant-common'; +import { buildResponse } from '../../../lib/build_response'; +import { ATTACK_DISCOVERY_SCHEDULES_BY_ID } from '../../../../common/constants'; +import { ElasticAssistantRequestHandlerContext } from '../../../types'; +import { convertAlertingRuleToSchedule } from './utils/convert_alerting_rule_to_schedule'; +import { performChecks } from '../../helpers'; +import { isFeatureAvailable } from './utils/is_feature_available'; + +export const getAttackDiscoverySchedulesRoute = ( + router: IRouter +): void => { + router.versioned + .get({ + access: 'internal', + path: ATTACK_DISCOVERY_SCHEDULES_BY_ID, + security: { + authz: { + requiredPrivileges: ['elasticAssistant'], + }, + }, + }) + .addVersion( + { + version: API_VERSIONS.internal.v1, + validate: { + request: { + params: buildRouteValidationWithZod(GetAttackDiscoverySchedulesRequestParams), + }, + response: { + 200: { + body: { + custom: buildRouteValidationWithZod(GetAttackDiscoverySchedulesResponse), + }, + }, + }, + }, + }, + async ( + context, + request, + response + ): Promise> => { + const ctx = await context.resolve(['core', 'elasticAssistant', 'licensing']); + const resp = buildResponse(response); + const assistantContext = await context.elasticAssistant; + const logger: Logger = assistantContext.logger; + + // Check if scheduling feature available + if (!(await isFeatureAvailable(ctx))) { + return response.notFound(); + } + + // Perform license and authenticated user + const checkResponse = await performChecks({ + context: ctx, + request, + response, + }); + + if (!checkResponse.isSuccess) { + return checkResponse.response; + } + + const { id } = request.params; + + try { + const dataClient = await assistantContext.getAttackDiscoverySchedulingDataClient(); + if (!dataClient) { + return resp.error({ + body: `Attack discovery data client not initialized`, + statusCode: 500, + }); + } + + const alertingRule = await dataClient.getSchedule(id); + + const schedule = convertAlertingRuleToSchedule(alertingRule); + + return response.ok({ body: schedule }); + } catch (err) { + logger.error(err); + const error = transformError(err); + + return resp.error({ + body: { success: false, error: error.message }, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/schedules/update.test.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/schedules/update.test.ts new file mode 100644 index 0000000000000..31c60c90b291a --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/schedules/update.test.ts @@ -0,0 +1,116 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { elasticsearchServiceMock } from '@kbn/core-elasticsearch-server-mocks'; +import { UpdateAttackDiscoverySchedulesRequestBody } from '@kbn/elastic-assistant-common'; +import { OpenAiProviderType } from '@kbn/stack-connectors-plugin/common/openai/constants'; + +import { updateAttackDiscoverySchedulesRoute } from './update'; +import { serverMock } from '../../../__mocks__/server'; +import { requestContextMock } from '../../../__mocks__/request_context'; +import { updateAttackDiscoverySchedulesRequest } from '../../../__mocks__/request'; +import { getInternalAttackDiscoveryScheduleMock } from '../../../__mocks__/attack_discovery_schedules.mock'; +import { AttackDiscoveryScheduleDataClient } from '../../../lib/attack_discovery/schedules/data_client'; + +const { clients, context } = requestContextMock.createTools(); +const server: ReturnType = serverMock.create(); +clients.core.elasticsearch.client = elasticsearchServiceMock.createScopedClusterClient(); + +const updateAttackDiscoverySchedule = jest.fn(); +const mockSchedulingDataClient = { + findSchedules: jest.fn(), + getSchedule: jest.fn(), + createSchedule: jest.fn(), + updateSchedule: updateAttackDiscoverySchedule, + deleteSchedule: jest.fn(), + enableSchedule: jest.fn(), + disableSchedule: jest.fn(), +} as unknown as AttackDiscoveryScheduleDataClient; +const mockApiConfig = { + connectorId: 'connector-id', + actionTypeId: '.bedrock', + model: 'model', + provider: OpenAiProviderType.OpenAi, +}; +const mockRequestBody: UpdateAttackDiscoverySchedulesRequestBody = { + name: 'Test Schedule 2', + schedule: { + interval: '15m', + }, + params: { + alertsIndexPattern: '.alerts-security.alerts-default', + apiConfig: mockApiConfig, + end: 'now', + size: 50, + start: 'now-24h', + }, + actions: [], +}; + +describe('updateAttackDiscoverySchedulesRoute', () => { + beforeEach(() => { + jest.clearAllMocks(); + context.elasticAssistant.getAttackDiscoverySchedulingDataClient.mockResolvedValue( + mockSchedulingDataClient + ); + context.core.featureFlags.getBooleanValue.mockResolvedValue(true); + updateAttackDiscoverySchedulesRoute(server.router); + updateAttackDiscoverySchedule.mockResolvedValue( + getInternalAttackDiscoveryScheduleMock(mockRequestBody) + ); + }); + + it('should handle successful request', async () => { + const response = await server.inject( + updateAttackDiscoverySchedulesRequest('schedule-1', mockRequestBody), + requestContextMock.convertContext(context) + ); + expect(response.status).toEqual(200); + expect(response.body).toEqual(expect.objectContaining({ ...mockRequestBody })); + }); + + it('should handle missing data client', async () => { + context.elasticAssistant.getAttackDiscoverySchedulingDataClient.mockResolvedValue(null); + const response = await server.inject( + updateAttackDiscoverySchedulesRequest('schedule-2', mockRequestBody), + requestContextMock.convertContext(context) + ); + + expect(response.status).toEqual(500); + expect(response.body).toEqual({ + message: 'Attack discovery data client not initialized', + status_code: 500, + }); + }); + + it('should handle `dataClient.updateSchedule` error', async () => { + (updateAttackDiscoverySchedule as jest.Mock).mockRejectedValue(new Error('Oh no!')); + const response = await server.inject( + updateAttackDiscoverySchedulesRequest('schedule-3', mockRequestBody), + requestContextMock.convertContext(context) + ); + expect(response.status).toEqual(500); + expect(response.body).toEqual({ + message: { + error: 'Oh no!', + success: false, + }, + status_code: 500, + }); + }); + + describe('Disabled feature flag', () => { + it('should return a 404 if scheduling feature is not registered', async () => { + context.core.featureFlags.getBooleanValue.mockResolvedValue(false); + const response = await server.inject( + updateAttackDiscoverySchedulesRequest('schedule-4', mockRequestBody), + requestContextMock.convertContext(context) + ); + expect(response.status).toEqual(404); + }); + }); +}); diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/schedules/update.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/schedules/update.ts new file mode 100644 index 0000000000000..ddee1f42bf32d --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/schedules/update.ts @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { IKibanaResponse, IRouter, Logger } from '@kbn/core/server'; +import { transformError } from '@kbn/securitysolution-es-utils'; +import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; + +import { + API_VERSIONS, + UpdateAttackDiscoverySchedulesRequestBody, + UpdateAttackDiscoverySchedulesRequestParams, + UpdateAttackDiscoverySchedulesResponse, +} from '@kbn/elastic-assistant-common'; +import { buildResponse } from '../../../lib/build_response'; +import { ATTACK_DISCOVERY_SCHEDULES_BY_ID } from '../../../../common/constants'; +import { ElasticAssistantRequestHandlerContext } from '../../../types'; +import { convertAlertingRuleToSchedule } from './utils/convert_alerting_rule_to_schedule'; +import { performChecks } from '../../helpers'; +import { isFeatureAvailable } from './utils/is_feature_available'; + +export const updateAttackDiscoverySchedulesRoute = ( + router: IRouter +): void => { + router.versioned + .put({ + access: 'internal', + path: ATTACK_DISCOVERY_SCHEDULES_BY_ID, + security: { + authz: { + requiredPrivileges: ['elasticAssistant'], + }, + }, + }) + .addVersion( + { + version: API_VERSIONS.internal.v1, + validate: { + request: { + params: buildRouteValidationWithZod(UpdateAttackDiscoverySchedulesRequestParams), + body: buildRouteValidationWithZod(UpdateAttackDiscoverySchedulesRequestBody), + }, + response: { + 200: { + body: { + custom: buildRouteValidationWithZod(UpdateAttackDiscoverySchedulesResponse), + }, + }, + }, + }, + }, + async ( + context, + request, + response + ): Promise> => { + const ctx = await context.resolve(['core', 'elasticAssistant', 'licensing']); + const resp = buildResponse(response); + const assistantContext = await context.elasticAssistant; + const logger: Logger = assistantContext.logger; + + // Check if scheduling feature available + if (!(await isFeatureAvailable(ctx))) { + return response.notFound(); + } + + // Perform license and authenticated user + const checkResponse = await performChecks({ + context: ctx, + request, + response, + }); + + if (!checkResponse.isSuccess) { + return checkResponse.response; + } + + const { id } = request.params; + const scheduleAttributes = request.body; + + try { + const dataClient = await assistantContext.getAttackDiscoverySchedulingDataClient(); + if (!dataClient) { + return resp.error({ + body: `Attack discovery data client not initialized`, + statusCode: 500, + }); + } + + const alertingRule = await dataClient.updateSchedule({ + id, + tags: [], + ...scheduleAttributes, + }); + const schedule = convertAlertingRuleToSchedule(alertingRule); + + return response.ok({ body: schedule }); + } catch (err) { + logger.error(err); + const error = transformError(err); + + return resp.error({ + body: { success: false, error: error.message }, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/schedules/utils/convert_alerting_rule_to_schedule.test.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/schedules/utils/convert_alerting_rule_to_schedule.test.ts new file mode 100644 index 0000000000000..75c0ca4ac9413 --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/schedules/utils/convert_alerting_rule_to_schedule.test.ts @@ -0,0 +1,74 @@ +/* + * 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 { OpenAiProviderType } from '@kbn/stack-connectors-plugin/common/openai/constants'; + +import { convertAlertingRuleToSchedule } from './convert_alerting_rule_to_schedule'; +import { getInternalAttackDiscoveryScheduleMock } from '../../../../__mocks__/attack_discovery_schedules.mock'; + +const mockApiConfig = { + connectorId: 'connector-id', + actionTypeId: '.bedrock', + model: 'model', + provider: OpenAiProviderType.OpenAi, +}; +const basicAttackDiscoveryScheduleMock = { + name: 'Test Schedule', + schedule: { + interval: '10m', + }, + params: { + alertsIndexPattern: '.alerts-security.alerts-default', + apiConfig: mockApiConfig, + end: 'now', + size: 25, + start: 'now-24h', + }, + enabled: true, + actions: [], +}; + +describe('convertAlertingRuleToSchedule', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should convert basic internal schedule', async () => { + const internalRule = getInternalAttackDiscoveryScheduleMock(basicAttackDiscoveryScheduleMock); + const { id, createdBy, updatedBy, createdAt, updatedAt } = internalRule; + const schedule = convertAlertingRuleToSchedule(internalRule); + + expect(schedule).toEqual({ + id, + createdBy, + updatedBy, + createdAt: createdAt.toISOString(), + updatedAt: updatedAt.toISOString(), + ...basicAttackDiscoveryScheduleMock, + }); + }); + + it('should default to `elastic` as a user if `createdBy` and/or `updatedBy` set to null', async () => { + const internalRule = getInternalAttackDiscoveryScheduleMock(basicAttackDiscoveryScheduleMock); + const { createdBy: _, updatedBy: __, ...restInternalRule } = internalRule; + const { id, createdAt, updatedAt } = internalRule; + const schedule = convertAlertingRuleToSchedule({ + ...restInternalRule, + createdBy: null, + updatedBy: null, + }); + + expect(schedule).toEqual({ + id, + createdBy: 'elastic', + updatedBy: 'elastic', + createdAt: createdAt.toISOString(), + updatedAt: updatedAt.toISOString(), + ...basicAttackDiscoveryScheduleMock, + }); + }); +}); diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/schedules/utils/convert_alerting_rule_to_schedule.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/schedules/utils/convert_alerting_rule_to_schedule.ts new file mode 100644 index 0000000000000..584843af6b1a8 --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/schedules/utils/convert_alerting_rule_to_schedule.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { SanitizedRule } from '@kbn/alerting-plugin/common'; +import { + AttackDiscoverySchedule, + AttackDiscoveryScheduleParams, +} from '@kbn/elastic-assistant-common'; +import { createScheduleExecutionSummary } from './create_schedule_execution_summary'; + +export const convertAlertingRuleToSchedule = ( + rule: SanitizedRule +): AttackDiscoverySchedule => { + const { + id, + name, + createdBy, + updatedBy, + createdAt, + updatedAt, + enabled, + params, + schedule, + actions, + } = rule; + return { + id, + name, + createdBy: createdBy ?? 'elastic', + updatedBy: updatedBy ?? 'elastic', + createdAt: createdAt.toISOString(), + updatedAt: updatedAt.toISOString(), + enabled, + params, + schedule, + actions, + lastExecution: createScheduleExecutionSummary(rule), + }; +}; diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/schedules/utils/create_schedule_execution_summary.test.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/schedules/utils/create_schedule_execution_summary.test.ts new file mode 100644 index 0000000000000..2bae492dbf6ba --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/schedules/utils/create_schedule_execution_summary.test.ts @@ -0,0 +1,148 @@ +/* + * 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 { OpenAiProviderType } from '@kbn/stack-connectors-plugin/common/openai/constants'; + +import { createScheduleExecutionSummary } from './create_schedule_execution_summary'; +import { getInternalAttackDiscoveryScheduleMock } from '../../../../__mocks__/attack_discovery_schedules.mock'; +import { + RuleExecutionStatusErrorReasons, + RuleExecutionStatusWarningReasons, +} from '@kbn/alerting-types'; + +const mockApiConfig = { + connectorId: 'connector-id', + actionTypeId: '.bedrock', + model: 'model', + provider: OpenAiProviderType.OpenAi, +}; +const basicAttackDiscoveryScheduleMock = { + name: 'Test Schedule', + schedule: { + interval: '10m', + }, + params: { + alertsIndexPattern: '.alerts-security.alerts-default', + apiConfig: mockApiConfig, + end: 'now', + size: 25, + start: 'now-24h', + }, + enabled: true, + actions: [], +}; + +describe('createScheduleExecutionSummary', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should not return execution summary if internal status is set to `pending`', async () => { + const now = new Date(); + const internalRule = getInternalAttackDiscoveryScheduleMock(basicAttackDiscoveryScheduleMock, { + executionStatus: { + status: 'pending', + lastExecutionDate: now, + }, + }); + const execution = createScheduleExecutionSummary(internalRule); + + expect(execution).toBeUndefined(); + }); + + it('should return status of the schedule execution', async () => { + const now = new Date(); + const internalRule = getInternalAttackDiscoveryScheduleMock(basicAttackDiscoveryScheduleMock, { + executionStatus: { + status: 'ok', + lastExecutionDate: now, + lastDuration: 22, + }, + }); + const execution = createScheduleExecutionSummary(internalRule); + + expect(execution?.status).toEqual('ok'); + }); + + it('should return data of the schedule execution', async () => { + const now = new Date(); + const internalRule = getInternalAttackDiscoveryScheduleMock(basicAttackDiscoveryScheduleMock, { + executionStatus: { + status: 'ok', + lastExecutionDate: now, + lastDuration: 22, + }, + }); + const execution = createScheduleExecutionSummary(internalRule); + + expect(execution?.date).toEqual(now.toISOString()); + }); + + it('should return duration of the schedule execution', async () => { + const now = new Date(); + const internalRule = getInternalAttackDiscoveryScheduleMock(basicAttackDiscoveryScheduleMock, { + executionStatus: { + status: 'ok', + lastExecutionDate: now, + lastDuration: 22, + }, + }); + const execution = createScheduleExecutionSummary(internalRule); + + expect(execution?.duration).toEqual(22); + }); + + it('should return empty message if neither error nor warning are specified', async () => { + const now = new Date(); + const internalRule = getInternalAttackDiscoveryScheduleMock(basicAttackDiscoveryScheduleMock, { + executionStatus: { + status: 'ok', + lastExecutionDate: now, + lastDuration: 22, + }, + }); + const execution = createScheduleExecutionSummary(internalRule); + + expect(execution?.message).toEqual(''); + }); + + it('should return error message if specified', async () => { + const now = new Date(); + const internalRule = getInternalAttackDiscoveryScheduleMock(basicAttackDiscoveryScheduleMock, { + executionStatus: { + status: 'error', + lastExecutionDate: now, + lastDuration: 22, + error: { + reason: RuleExecutionStatusErrorReasons.Execute, + message: 'Test Error Message', + }, + }, + }); + const execution = createScheduleExecutionSummary(internalRule); + + expect(execution?.message).toEqual('Test Error Message'); + }); + + it('should return warning message if specified', async () => { + const now = new Date(); + const internalRule = getInternalAttackDiscoveryScheduleMock(basicAttackDiscoveryScheduleMock, { + executionStatus: { + status: 'error', + lastExecutionDate: now, + lastDuration: 22, + warning: { + reason: RuleExecutionStatusWarningReasons.MAX_ALERTS, + message: 'Test Warning Message', + }, + }, + }); + const execution = createScheduleExecutionSummary(internalRule); + + expect(execution?.message).toEqual('Test Warning Message'); + }); +}); diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/schedules/utils/create_schedule_execution_summary.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/schedules/utils/create_schedule_execution_summary.ts new file mode 100644 index 0000000000000..7c966e205803d --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/schedules/utils/create_schedule_execution_summary.ts @@ -0,0 +1,27 @@ +/* + * 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 { SanitizedRule } from '@kbn/alerting-plugin/common'; +import { + AttackDiscoveryScheduleExecution, + AttackDiscoveryScheduleParams, +} from '@kbn/elastic-assistant-common'; + +export const createScheduleExecutionSummary = ( + rule: SanitizedRule +): AttackDiscoveryScheduleExecution | undefined => { + const { executionStatus } = rule; + if (executionStatus.status === 'pending') { + return undefined; + } + return { + date: executionStatus.lastExecutionDate.toISOString(), + status: executionStatus.status, + duration: executionStatus.lastDuration, + message: executionStatus.error?.message ?? executionStatus.warning?.message ?? '', + }; +}; diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/schedules/utils/is_feature_available.test.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/schedules/utils/is_feature_available.test.ts new file mode 100644 index 0000000000000..1978dede7f74f --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/schedules/utils/is_feature_available.test.ts @@ -0,0 +1,34 @@ +/* + * 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 { AwaitedProperties } from '@kbn/utility-types'; +import { ElasticAssistantRequestHandlerContext } from '../../../../types'; +import { isFeatureAvailable } from './is_feature_available'; + +const getBooleanValueMock = jest.fn(); +const mockContext = { + core: { + featureFlags: { + getBooleanValue: getBooleanValueMock, + }, + }, +} as unknown as AwaitedProperties; + +describe('isFeatureAvailable', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should call feature flags service with the correct attributes', async () => { + void isFeatureAvailable(mockContext); + + expect(getBooleanValueMock).toHaveBeenCalledWith( + 'securitySolution.assistantAttackDiscoverySchedulingEnabled', + false + ); + }); +}); diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/schedules/utils/is_feature_available.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/schedules/utils/is_feature_available.ts new file mode 100644 index 0000000000000..52d28189c961a --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/schedules/utils/is_feature_available.ts @@ -0,0 +1,20 @@ +/* + * 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 { ATTACK_DISCOVERY_SCHEDULES_ENABLED_FEATURE_FLAG } from '@kbn/elastic-assistant-common'; +import { AwaitedProperties } from '@kbn/utility-types'; + +import { ElasticAssistantRequestHandlerContext } from '../../../../types'; + +export const isFeatureAvailable = async ( + context: AwaitedProperties> +): Promise => { + return context.core.featureFlags.getBooleanValue( + ATTACK_DISCOVERY_SCHEDULES_ENABLED_FEATURE_FLAG, + false + ); +}; diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/register_routes.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/register_routes.ts index 211b4649fc7c3..251756026b51d 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/register_routes.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/register_routes.ts @@ -41,6 +41,13 @@ import { import { deleteKnowledgeBaseEntryRoute } from './knowledge_base/entries/delete_route'; import { updateKnowledgeBaseEntryRoute } from './knowledge_base/entries/update_route'; import { getKnowledgeBaseEntryRoute } from './knowledge_base/entries/get_route'; +import { createAttackDiscoverySchedulesRoute } from './attack_discovery/schedules/create'; +import { getAttackDiscoverySchedulesRoute } from './attack_discovery/schedules/get'; +import { updateAttackDiscoverySchedulesRoute } from './attack_discovery/schedules/update'; +import { deleteAttackDiscoverySchedulesRoute } from './attack_discovery/schedules/delete'; +import { findAttackDiscoverySchedulesRoute } from './attack_discovery/schedules/find'; +import { disableAttackDiscoverySchedulesRoute } from './attack_discovery/schedules/disable'; +import { enableAttackDiscoverySchedulesRoute } from './attack_discovery/schedules/enable'; export const registerRoutes = ( router: ElasticAssistantPluginRouter, @@ -101,6 +108,15 @@ export const registerRoutes = ( postAttackDiscoveryRoute(router); cancelAttackDiscoveryRoute(router); + // Attack Discovery Schedules + createAttackDiscoverySchedulesRoute(router); + getAttackDiscoverySchedulesRoute(router); + findAttackDiscoverySchedulesRoute(router); + updateAttackDiscoverySchedulesRoute(router); + deleteAttackDiscoverySchedulesRoute(router); + disableAttackDiscoverySchedulesRoute(router); + enableAttackDiscoverySchedulesRoute(router); + // Defend insights getDefendInsightRoute(router); getDefendInsightsRoute(router); diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/request_context_factory.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/request_context_factory.ts index cdc2559e477b6..7adff6c909028 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/request_context_factory.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/request_context_factory.ts @@ -78,6 +78,7 @@ export class RequestContextFactory implements IRequestContextFactory { }; const savedObjectsClient = coreStart.savedObjects.getScopedClient(request); + const rulesClient = await startPlugins.alerting.getRulesClientWithRequest(request); return { core: coreContext, @@ -142,6 +143,12 @@ export class RequestContextFactory implements IRequestContextFactory { }); }), + getAttackDiscoverySchedulingDataClient: memoize(async () => { + return this.assistantService.createAttackDiscoverySchedulingDataClient({ + rulesClient, + }); + }), + getDefendInsightsDataClient: memoize(async () => { const currentUser = await getCurrentUser(); return this.assistantService.createDefendInsightsDataClient({ diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/types.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/types.ts index e464229f93387..54f1ce5428678 100755 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/types.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/types.ts @@ -50,6 +50,7 @@ import { import type { InferenceServerStart } from '@kbn/inference-plugin/server'; import { ProductDocBaseStartContract } from '@kbn/product-doc-base-plugin/server'; +import { AlertingServerSetup, AlertingServerStart } from '@kbn/alerting-plugin/server'; import type { GetAIAssistantKnowledgeBaseDataClientParams } from './ai_assistant_data_clients/knowledge_base'; import { AttackDiscoveryDataClient } from './lib/attack_discovery/persistence'; import { @@ -61,6 +62,7 @@ import { CallbackIds } from './services/app_context'; import { AIAssistantDataClient } from './ai_assistant_data_clients'; import { AIAssistantKnowledgeBaseDataClient } from './ai_assistant_data_clients/knowledge_base'; import type { DefendInsightsDataClient } from './lib/defend_insights/persistence'; +import { AttackDiscoveryScheduleDataClient } from './lib/attack_discovery/schedules/data_client'; export const PLUGIN_ID = 'elasticAssistant' as const; export { CallbackIds }; @@ -120,12 +122,14 @@ export interface ElasticAssistantPluginStart { export interface ElasticAssistantPluginSetupDependencies { actions: ActionsPluginSetup; + alerting: AlertingServerSetup; ml: MlPluginSetup; taskManager: TaskManagerSetupContract; spaces?: SpacesPluginSetup; } export interface ElasticAssistantPluginStartDependencies { actions: ActionsPluginStart; + alerting: AlertingServerStart; llmTasks: LlmTasksPluginStart; inference: InferenceServerStart; spaces?: SpacesPluginStart; @@ -150,6 +154,7 @@ export interface ElasticAssistantApiRequestHandlerContext { params?: GetAIAssistantKnowledgeBaseDataClientParams ) => Promise; getAttackDiscoveryDataClient: () => Promise; + getAttackDiscoverySchedulingDataClient: () => Promise; getDefendInsightsDataClient: () => Promise; getAIAssistantPromptsDataClient: () => Promise; getAIAssistantAnonymizationFieldsDataClient: () => Promise; diff --git a/x-pack/solutions/security/plugins/elastic_assistant/tsconfig.json b/x-pack/solutions/security/plugins/elastic_assistant/tsconfig.json index e208216a4d9fa..8bd9a88d27a22 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/tsconfig.json +++ b/x-pack/solutions/security/plugins/elastic_assistant/tsconfig.json @@ -59,6 +59,8 @@ "@kbn/alerts-as-data-utils", "@kbn/alerting-plugin", "@kbn/rule-data-utils", + "@kbn/alerting-types", + "@kbn/zod-helpers", ], "exclude": [ "target/**/*",