[Attack Discovery][Scheduling] Attack Discovery scheduling rule management (#12003) (#216656)

## Summary

Main ticket ([Internal
link](https://github.com/elastic/security-team/issues/12003))

To allow users to schedule Attack Discovery generations, we will use
either [Alerting
Framework](https://www.elastic.co/guide/en/kibana/current/alerting-getting-started.html).
These changes add functionality to manage new alerts type - Attack
Discovery Schedule.

### Introduced endpoints

- **Create** AD scheduling rule route: `POST
/internal/elastic_assistant/attack_discovery/schedules`
- **Read/Get** AD scheduling rule by id route: `GET
/internal/elastic_assistant/attack_discovery/schedules/{id}`
- **Update** AD scheduling rule by id route: `PUT
/internal/elastic_assistant/attack_discovery/schedules/{id}`
- **Delete** AD scheduling rule by id route: `DELETE
/internal/elastic_assistant/attack_discovery/schedules/{id}`
- **Enable** AD scheduling rule by id route: `POST
/internal/elastic_assistant/attack_discovery/schedules/{id}/_enable`
- **Disable** AD scheduling rule by id route: `POST
/internal/elastic_assistant/attack_discovery/schedules/{id}/_disable`
- **Find** all existing AD scheduling rules route: `GET
/internal/elastic_assistant/attack_discovery/schedules/_find`

## NOTES

The feature is hidden behind the feature flag:

> xpack.securitySolution.enableExperimental:
['assistantAttackDiscoverySchedulingEnabled']

## cURL examples

<details>
  <summary>Create AD scheduling rule route</summary>

```curl
curl --location 'http://localhost:5601/internal/elastic_assistant/attack_discovery/schedules' \
--header 'kbn-xsrf: true' \
--header 'elastic-api-version: 1' \
--header 'x-elastic-internal-origin: security-solution' \
--header 'Content-Type: application/json' \
--data '{
    "name": "Test Schedule",
    "schedule": {
        "interval": "10m"
    },
    "params": {
        "alertsIndexPattern": ".alerts-security.alerts-default",
        "apiConfig": {
            "connectorId": "gpt-4o",
            "actionTypeId": ".gen-ai"
        },
        "end": "now",
        "size": 100,
        "start": "now-24h"
    }
}'
```

</details>

<details>
  <summary>Read/Get AD scheduling rule by id route</summary>

```curl
curl --location 'http://localhost:5601/internal/elastic_assistant/attack_discovery/schedules/{id}' \
--header 'kbn-xsrf: true' \
--header 'elastic-api-version: 1' \
--header 'x-elastic-internal-origin: security-solution'
```

</details>

<details>
  <summary>Update AD scheduling rule by id route</summary>

```curl
curl --location --request PUT 'http://localhost:5601/internal/elastic_assistant/attack_discovery/schedules/{id}' \
--header 'kbn-xsrf: true' \
--header 'elastic-api-version: 1' \
--header 'x-elastic-internal-origin: security-solution' \
--header 'Content-Type: application/json' \
--data '{
    "name": "Test Schedule - Updated",
    "schedule": {
        "interval": "123m"
    },
    "params": {
        "alertsIndexPattern": ".alerts-security.alerts-default",
        "apiConfig": {
            "connectorId": "gpt-4o",
            "actionTypeId": ".gen-ai"
        },
        "end": "now",
        "size": 35,
        "start": "now-24h"
    },
    "actions": []
}'
```

</details>

<details>
  <summary>Delete AD scheduling rule by id route</summary>

```curl
curl --location --request DELETE 'http://localhost:5601/internal/elastic_assistant/attack_discovery/schedules/{id}' \
--header 'kbn-xsrf: true' \
--header 'elastic-api-version: 1' \
--header 'x-elastic-internal-origin: security-solution'
```

</details>

<details>
  <summary>Enable AD scheduling rule by id route</summary>

```curl
curl --location --request POST 'http://localhost:5601/internal/elastic_assistant/attack_discovery/schedules/{id}/_enable' \
--header 'kbn-xsrf: true' \
--header 'elastic-api-version: 1' \
--header 'x-elastic-internal-origin: security-solution'
```

</details>

<details>
  <summary>Disable AD scheduling rule by id route</summary>

```curl
curl --location --request POST 'http://localhost:5601/internal/elastic_assistant/attack_discovery/schedules/{id}/_disable' \
--header 'kbn-xsrf: true' \
--header 'elastic-api-version: 1' \
--header 'x-elastic-internal-origin: security-solution'
```

</details>

<details>
  <summary>Find all existing AD scheduling rules route</summary>

```curl
curl --location 'http://localhost:5601/internal/elastic_assistant/attack_discovery/schedules/_find' \
--header 'kbn-xsrf: true' \
--header 'elastic-api-version: 1' \
--header 'x-elastic-internal-origin: security-solution'
```

</details>

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Ievgen Sorokopud 2025-04-10 11:03:04 +02:00 committed by GitHub
parent 387e2d95ec
commit fc11ca94f5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
52 changed files with 3627 additions and 59 deletions

View file

@ -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<string, string, unknown>(
'IsoDateString',
rt.string.is,
(input, context): Either<rt.Errors, string> => {
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<typeof SecurityAttackDiscoveryAlertSchema>;

View file

@ -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

View file

@ -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<typeof AttackDiscoveryGenericResponse>;
export const AttackDiscoveryGenericResponse = z.object({}).catchall(z.unknown());
/**
* An attack discovery generic error
*/
export type AttackDiscoveryGenericError = z.infer<typeof AttackDiscoveryGenericError>;
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;

View file

@ -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'

View file

@ -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),
});

View file

@ -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

View file

@ -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<typeof AttackDiscoveryScheduleParams>;
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<typeof IntervalSchedule>;
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<typeof AttackDiscoveryScheduleActionGroup>;
export const AttackDiscoveryScheduleActionGroup = z.string();
/**
* The connector ID.
*/
export type AttackDiscoveryScheduleActionId = z.infer<typeof AttackDiscoveryScheduleActionId>;
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<typeof AttackDiscoveryScheduleAction>;
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<typeof AttackDiscoveryScheduleExecution>;
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<typeof AttackDiscoverySchedule>;
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<typeof AttackDiscoveryScheduleCreateProps>;
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<typeof AttackDiscoveryScheduleUpdateProps>;
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),
});

View file

@ -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'

View file

@ -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';

View file

@ -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: [],
},
},

View file

@ -17,6 +17,7 @@
"@kbn/cases-plugin",
"@kbn/securitysolution-rules",
"@kbn/securitysolution-list-constants",
"@kbn/elastic-assistant-common",
],
"exclude": ["target/**/*"]
}

View file

@ -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;

View file

@ -13,6 +13,7 @@
"server": true,
"requiredPlugins": [
"actions",
"alerting",
"data",
"ml",
"taskManager",

View file

@ -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<AttackDiscoveryScheduleParams> => {
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<CreateRuleData<AttackDiscoveryScheduleParams>>
): UpdateRuleData<AttackDiscoveryScheduleParams> & { id: string } => {
return {
id,
...getAttackDiscoveryCreateScheduleMock(),
...overrides,
};
};
export const getInternalAttackDiscoveryScheduleMock = (
createParams: AttackDiscoveryScheduleCreateProps,
overrides?: Partial<SanitizedRule<AttackDiscoveryScheduleParams>>
): SanitizedRule<AttackDiscoveryScheduleParams> => {
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>
): 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<SanitizedRule<AttackDiscoveryScheduleParams>>
) => {
return {
page: 1,
perPage: 20,
total: schedules.length,
data: schedules,
};
};

View file

@ -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<AIAssistantConversationsDataClient>;
export type ConversationsDataClientMock = jest.Mocked<ConversationsDataClientContract>;
type AttackDiscoveryDataClientContract = PublicMethodsOf<AttackDiscoveryDataClient>;
export type AttackDiscoveryDataClientMock = jest.Mocked<AttackDiscoveryDataClientContract>;
type AttackDiscoveryScheduleDataClientContract = PublicMethodsOf<AttackDiscoveryScheduleDataClient>;
export type AttackDiscoveryScheduleDataClientMock =
jest.Mocked<AttackDiscoveryScheduleDataClientContract>;
type KnowledgeBaseDataClientContract = PublicMethodsOf<AIAssistantKnowledgeBaseDataClient> & {
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(),

View file

@ -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 },
});

View file

@ -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<Promise<AttackDiscoveryDataClient | null>, [], unknown> &
(() => Promise<AttackDiscoveryDataClient | null>),
getAttackDiscoverySchedulingDataClient: jest.fn(
() => clients.elasticAssistant.getAttackDiscoverySchedulingDataClient
) as unknown as jest.MockInstance<
Promise<AttackDiscoveryScheduleDataClient | null>,
[],
unknown
> &
(() => Promise<AttackDiscoveryScheduleDataClient | null>),
getDefendInsightsDataClient: jest.fn(
() => clients.elasticAssistant.getDefendInsightsDataClient
) as unknown as jest.MockInstance<Promise<DefendInsightsDataClient | null>, [], unknown> &

View file

@ -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<AttackDiscoveryScheduleDataClient | null> {
return new AttackDiscoveryScheduleDataClient({
rulesClient: opts.rulesClient,
});
}
public async createDefendInsightsDataClient(
opts: CreateAIAssistantClientParams
): Promise<DefendInsightsDataClient | null> {

View file

@ -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<AttackDiscoveryAlert> = {
context: 'security.attack.discovery',
export const ATTACK_DISCOVERY_ALERTS_CONTEXT = 'security.attack.discovery' as const;
export const ATTACK_DISCOVERY_ALERTS_AAD_CONFIG: IRuleTypeAlerts<SecurityAttackDiscoveryAlert> = {
context: ATTACK_DISCOVERY_ALERTS_CONTEXT,
mappings: { fieldMap: attackDiscoveryAlertFieldMap },
isSpaceAware: true,
shouldWrite: true,
useEcs: true,
};

View file

@ -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,
});
});
});
});

View file

@ -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<AttackDiscoveryScheduleParams>({
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<AttackDiscoveryScheduleParams>({ id });
return rule;
};
public createSchedule = async (ruleToCreate: CreateRuleData<AttackDiscoveryScheduleParams>) => {
const rule = await this.options.rulesClient.create<AttackDiscoveryScheduleParams>({
data: ruleToCreate,
});
return rule;
};
public updateSchedule = async (
ruleToUpdate: UpdateRuleData<AttackDiscoveryScheduleParams> & { id: string }
) => {
const { id, ...updatePayload } = ruleToUpdate;
const rule = await this.options.rulesClient.update<AttackDiscoveryScheduleParams>({
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);
};
}

View file

@ -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,
})
);
});
});

View file

@ -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,
});
},
};
};

View file

@ -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);
});
});

View file

@ -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: {} };
};

View file

@ -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';

View file

@ -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
>;

View file

@ -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) => {

View file

@ -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<typeof serverMock.create> = 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);
});
});
});

View file

@ -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<ElasticAssistantRequestHandlerContext>
): 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<IKibanaResponse<CreateAttackDiscoverySchedulesResponse>> => {
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,
});
}
}
);
};

View file

@ -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<typeof serverMock.create> = 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);
});
});
});

View file

@ -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<ElasticAssistantRequestHandlerContext>
): 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<IKibanaResponse<DeleteAttackDiscoverySchedulesResponse>> => {
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,
});
}
}
);
};

View file

@ -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<typeof serverMock.create> = 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);
});
});
});

View file

@ -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<ElasticAssistantRequestHandlerContext>
): 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<IKibanaResponse<DisableAttackDiscoverySchedulesResponse>> => {
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,
});
}
}
);
};

View file

@ -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<typeof serverMock.create> = 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);
});
});
});

View file

@ -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<ElasticAssistantRequestHandlerContext>
): 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<IKibanaResponse<EnableAttackDiscoverySchedulesResponse>> => {
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,
});
}
}
);
};

View file

@ -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<typeof serverMock.create> = 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);
});
});
});

View file

@ -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<ElasticAssistantRequestHandlerContext>
): 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<IKibanaResponse<FindAttackDiscoverySchedulesResponse>> => {
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,
});
}
}
);
};

View file

@ -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<typeof serverMock.create> = 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);
});
});
});

View file

@ -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<ElasticAssistantRequestHandlerContext>
): 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<IKibanaResponse<GetAttackDiscoverySchedulesResponse>> => {
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,
});
}
}
);
};

View file

@ -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<typeof serverMock.create> = 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);
});
});
});

View file

@ -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<ElasticAssistantRequestHandlerContext>
): 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<IKibanaResponse<UpdateAttackDiscoverySchedulesResponse>> => {
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,
});
}
}
);
};

View file

@ -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,
});
});
});

View file

@ -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<AttackDiscoveryScheduleParams>
): 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),
};
};

View file

@ -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');
});
});

View file

@ -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<AttackDiscoveryScheduleParams>
): 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 ?? '',
};
};

View file

@ -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<ElasticAssistantRequestHandlerContext>;
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
);
});
});

View file

@ -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<Pick<ElasticAssistantRequestHandlerContext, 'core'>>
): Promise<boolean> => {
return context.core.featureFlags.getBooleanValue(
ATTACK_DISCOVERY_SCHEDULES_ENABLED_FEATURE_FLAG,
false
);
};

View file

@ -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);

View file

@ -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({

View file

@ -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<AIAssistantKnowledgeBaseDataClient | null>;
getAttackDiscoveryDataClient: () => Promise<AttackDiscoveryDataClient | null>;
getAttackDiscoverySchedulingDataClient: () => Promise<AttackDiscoveryScheduleDataClient | null>;
getDefendInsightsDataClient: () => Promise<DefendInsightsDataClient | null>;
getAIAssistantPromptsDataClient: () => Promise<AIAssistantDataClient | null>;
getAIAssistantAnonymizationFieldsDataClient: () => Promise<AIAssistantDataClient | null>;

View file

@ -59,6 +59,8 @@
"@kbn/alerts-as-data-utils",
"@kbn/alerting-plugin",
"@kbn/rule-data-utils",
"@kbn/alerting-types",
"@kbn/zod-helpers",
],
"exclude": [
"target/**/*",