[Automatic Migrations] Siem migrations new endpoint (#219597)

## Summary

Resolves https://github.com/elastic/security-team/issues/12483

This PR changes REST API Endpoints scheme to align with
https://github.com/elastic/security-team/issues/12483. Below is the
summary of changes done.

### API Scheme changes

The REST API scheme has been changed to reflect
https://github.com/elastic/security-team/issues/12483. This is pretty
much self explanatory as defined in below openapi schema yaml :

-
[x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.schema.yaml](https://github.com/elastic/kibana/pull/219597/files#diff-3025af9eca156f3308474e2b42808da1531423457b7791daf6660db95a53b978)

### Introduction of Delete Migration API

This PR also adds `DELETE` method on route
`/rules/siem_migrations/{migration_id}` for deleting a migrations.
Deleting a migration does below operations:

- Stops a migration if it is running
- Deletes the rules, resources related to migration and migration
document itself.

### File Reorganizations

Directly structure has been changed a little bit to reflect the
endpoint. There is a sub-directory called `rules` which deals with only
`rules` of the migration and the root directly only contains the
endpoints related to the migration.

#### Before

```
//siem_migrations/rules/api

├── create.ts
├── get.ts
├── start.ts
├── update.ts
├── 
├── 
├── 
```

#### After

```
//siem_migrations/rules/api


├── create.ts
├── delete.ts
├── get.ts
├── rules
│   ├── add.ts
│   ├── get.ts
│   └── update.ts
├── 
├── 
├── 
```

## Migration Strategy

### TL,DR; 
```mermaid
flowchart TD
    StartM[Start Migration] --> isMigExists{Does Migration Index Exists}
    isMigExists -->|Yes|FetchMDoc[Fetch Migration Docs]
    isMigExists -->|No|CreateMIndex[Create MigrationIndex]
    CreateMIndex --> FetchMDoc
    FetchMDoc --> FetchMRules[Fetch Migration Stats Rules index]
    FetchMRules --> FilterMigration{Filter Migration Docs not in Migration Index}
    FilterMigration --> |is Empty|END[END]
    FilterMigration --> |is Not Empty| CreateMDocs[Create Migration Docs]
    CreateMDocs --> END
```



At the time of merging this PR, the Migration indices can be in 3
states:

### There are migrations created after
https://github.com/elastic/kibana/pull/216164 and this means that there
are `some` migrations existing in
`.kibana-siem-migrations-migrations-<space_id>` and migrations created
before above mentioned PR will only exist in
`.kibana-siem-migrations-rules-<space_id>`.

In this case `migrateRuleMigrationIndex` will create migration in below
steps:

1. Look for **all** migration Documents in
`.kibana-siem-migrations-migrations-<space_id>`
2. Get **all** Migrations stats from
`.kibana-siem-migrations-rules-<space_id>` which includes below
properties
- migration_id : will help in reconciling the migration id in
.kibana-siem-migrations-migrations-<space_id>` index
    - created_at : Date on which migration_id was created.
    - created_by: User who created the migrations.
3. A new document with above migration will be added to
`.kibana-siem-migrations-migrations-<space_id>`.
4. Now both `.kibana-siem-migrations-migrations-<space_id>` and
`.kibana-siem-migrations-rules-<space_id>` will be in sync.

### Alternatively, there are no migration created after
https://github.com/elastic/kibana/pull/216164. In that case, there is a
possibility that `.kibana-siem-migrations-migrations-<space_id>`, will
not even exist.

In this case `migrateRuleMigrationIndex` will create migration in below
steps:

1. Create the `.kibana-siem-migrations-migrations-<space_id>` index.
2. Do steps mentioned in above scenario.

### Once the migrations has been run successfully, both
`.kibana-siem-migrations-migrations-<space_id>` index and
`.kibana-siem-migrations-rules-<space_id>` will be in sync.

1. In this case, migration will not run, since it tries to filter the
migrations by `id` which exist in
`kibana-siem-migrations-rules-<space_id>` but do not exist in
`kibana-siem-migrations-migrations-<space_id>`

### Checklist

Check the PR satisfies following conditions. 

Reviewers should verify this PR satisfies this list as well.

- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md)
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [x] The PR description includes the appropriate Release Notes section,
and the correct `release_note:*` label is applied per the
[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

### Identify risks

Does this PR introduce any risks? For example, consider risks like hard
to test bugs, performance regression, potential of data loss.

Describe the risk, its severity, and mitigation for each identified
risk. Invite stakeholders and evaluate how to proceed before merging.

- [ ] [See some risk
examples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx)
- [ ] ...

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Jatin Kathuria 2025-05-22 17:23:17 +02:00 committed by GitHub
parent a09b95d753
commit bf642b0039
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
119 changed files with 4580 additions and 1604 deletions

View file

@ -372,11 +372,11 @@ import type {
ResolveTimelineResponse,
} from './timeline/resolve_timeline/resolve_timeline_route.gen';
import type {
CreateRuleMigrationRequestParamsInput,
CreateRuleMigrationRequestBodyInput,
CreateRuleMigrationResponse,
CreateRuleMigrationRulesRequestParamsInput,
CreateRuleMigrationRulesRequestBodyInput,
DeleteRuleMigrationRequestParamsInput,
GetAllStatsRuleMigrationResponse,
GetRuleMigrationRequestQueryInput,
GetRuleMigrationRequestParamsInput,
GetRuleMigrationResponse,
GetRuleMigrationIntegrationsResponse,
@ -388,6 +388,9 @@ import type {
GetRuleMigrationResourcesResponse,
GetRuleMigrationResourcesMissingRequestParamsInput,
GetRuleMigrationResourcesMissingResponse,
GetRuleMigrationRulesRequestQueryInput,
GetRuleMigrationRulesRequestParamsInput,
GetRuleMigrationRulesResponse,
GetRuleMigrationStatsRequestParamsInput,
GetRuleMigrationStatsResponse,
GetRuleMigrationTranslationStatsRequestParamsInput,
@ -401,8 +404,10 @@ import type {
StopRuleMigrationRequestParamsInput,
StopRuleMigrationResponse,
UpdateRuleMigrationRequestParamsInput,
UpdateRuleMigrationRequestBodyInput,
UpdateRuleMigrationResponse,
UpdateRuleMigrationRulesRequestParamsInput,
UpdateRuleMigrationRulesRequestBodyInput,
UpdateRuleMigrationRulesResponse,
UpsertRuleMigrationResourcesRequestParamsInput,
UpsertRuleMigrationResourcesRequestBodyInput,
UpsertRuleMigrationResourcesResponse,
@ -726,13 +731,28 @@ For detailed information on Kibana actions and alerting, and additional API call
.catch(catchAxiosErrorFormatAndThrow);
}
/**
* Creates a new SIEM rules migration using the original vendor rules provided
* Creates a new rule migration and returns the corresponding migration_id
*/
async createRuleMigration(props: CreateRuleMigrationProps) {
async createRuleMigration() {
this.log.info(`${new Date().toISOString()} Calling API CreateRuleMigration`);
return this.kbnClient
.request<CreateRuleMigrationResponse>({
path: replaceParams('/internal/siem_migrations/rules/{migration_id}', props.params),
path: '/internal/siem_migrations/rules',
headers: {
[ELASTIC_HTTP_VERSION_HEADER]: '1',
},
method: 'PUT',
})
.catch(catchAxiosErrorFormatAndThrow);
}
/**
* Adds original vendor rules to an already existing migration. Can be called multiple times to add more rules
*/
async createRuleMigrationRules(props: CreateRuleMigrationRulesProps) {
this.log.info(`${new Date().toISOString()} Calling API CreateRuleMigrationRules`);
return this.kbnClient
.request({
path: replaceParams('/internal/siem_migrations/rules/{migration_id}/rules', props.params),
headers: {
[ELASTIC_HTTP_VERSION_HEADER]: '1',
},
@ -869,6 +889,21 @@ The difference between the `id` and `rule_id` is that the `id` is a unique rule
})
.catch(catchAxiosErrorFormatAndThrow);
}
/**
* Deletes a rule migration document stored in the system given the rule migration id
*/
async deleteRuleMigration(props: DeleteRuleMigrationProps) {
this.log.info(`${new Date().toISOString()} Calling API DeleteRuleMigration`);
return this.kbnClient
.request({
path: replaceParams('/internal/siem_migrations/rules/{migration_id}', props.params),
headers: {
[ELASTIC_HTTP_VERSION_HEADER]: '1',
},
method: 'DELETE',
})
.catch(catchAxiosErrorFormatAndThrow);
}
/**
* Delete one or more Timelines or Timeline templates.
*/
@ -1496,7 +1531,7 @@ finalize it.
.catch(catchAxiosErrorFormatAndThrow);
}
/**
* Retrieves the rule documents stored in the system given the rule migration id
* Retrieves the rule migration document stored in the system given the rule migration id
*/
async getRuleMigration(props: GetRuleMigrationProps) {
this.log.info(`${new Date().toISOString()} Calling API GetRuleMigration`);
@ -1507,8 +1542,6 @@ finalize it.
[ELASTIC_HTTP_VERSION_HEADER]: '1',
},
method: 'GET',
query: props.query,
})
.catch(catchAxiosErrorFormatAndThrow);
}
@ -1598,6 +1631,23 @@ finalize it.
})
.catch(catchAxiosErrorFormatAndThrow);
}
/**
* Retrieves the the list of rules included in a migration given the migration id
*/
async getRuleMigrationRules(props: GetRuleMigrationRulesProps) {
this.log.info(`${new Date().toISOString()} Calling API GetRuleMigrationRules`);
return this.kbnClient
.request<GetRuleMigrationRulesResponse>({
path: replaceParams('/internal/siem_migrations/rules/{migration_id}/rules', props.params),
headers: {
[ELASTIC_HTTP_VERSION_HEADER]: '1',
},
method: 'GET',
query: props.query,
})
.catch(catchAxiosErrorFormatAndThrow);
}
/**
* Retrieves the stats of a SIEM rules migration using the migration id provided
*/
@ -2331,7 +2381,7 @@ The difference between the `id` and `rule_id` is that the `id` is a unique rule
headers: {
[ELASTIC_HTTP_VERSION_HEADER]: '1',
},
method: 'PUT',
method: 'POST',
body: props.body,
})
.catch(catchAxiosErrorFormatAndThrow);
@ -2359,7 +2409,7 @@ The difference between the `id` and `rule_id` is that the `id` is a unique rule
headers: {
[ELASTIC_HTTP_VERSION_HEADER]: '1',
},
method: 'PUT',
method: 'POST',
})
.catch(catchAxiosErrorFormatAndThrow);
}
@ -2433,7 +2483,7 @@ The difference between the `id` and `rule_id` is that the `id` is a unique rule
.catch(catchAxiosErrorFormatAndThrow);
}
/**
* Updates rules migrations attributes
* Updates rules migrations data
*/
async updateRuleMigration(props: UpdateRuleMigrationProps) {
this.log.info(`${new Date().toISOString()} Calling API UpdateRuleMigration`);
@ -2443,7 +2493,22 @@ The difference between the `id` and `rule_id` is that the `id` is a unique rule
headers: {
[ELASTIC_HTTP_VERSION_HEADER]: '1',
},
method: 'PUT',
method: 'PATCH',
})
.catch(catchAxiosErrorFormatAndThrow);
}
/**
* Updates rules migrations attributes
*/
async updateRuleMigrationRules(props: UpdateRuleMigrationRulesProps) {
this.log.info(`${new Date().toISOString()} Calling API UpdateRuleMigrationRules`);
return this.kbnClient
.request<UpdateRuleMigrationRulesResponse>({
path: replaceParams('/internal/siem_migrations/rules/{migration_id}/rules', props.params),
headers: {
[ELASTIC_HTTP_VERSION_HEADER]: '1',
},
method: 'PATCH',
body: props.body,
})
.catch(catchAxiosErrorFormatAndThrow);
@ -2522,9 +2587,9 @@ export interface CreatePrivMonUserProps {
export interface CreateRuleProps {
body: CreateRuleRequestBodyInput;
}
export interface CreateRuleMigrationProps {
params: CreateRuleMigrationRequestParamsInput;
body: CreateRuleMigrationRequestBodyInput;
export interface CreateRuleMigrationRulesProps {
params: CreateRuleMigrationRulesRequestParamsInput;
body: CreateRuleMigrationRulesRequestBodyInput;
}
export interface CreateTimelinesProps {
body: CreateTimelinesRequestBodyInput;
@ -2549,6 +2614,9 @@ export interface DeletePrivMonUserProps {
export interface DeleteRuleProps {
query: DeleteRuleRequestQueryInput;
}
export interface DeleteRuleMigrationProps {
params: DeleteRuleMigrationRequestParamsInput;
}
export interface DeleteTimelinesProps {
body: DeleteTimelinesRequestBodyInput;
}
@ -2654,7 +2722,6 @@ export interface GetRuleExecutionResultsProps {
params: GetRuleExecutionResultsRequestParamsInput;
}
export interface GetRuleMigrationProps {
query: GetRuleMigrationRequestQueryInput;
params: GetRuleMigrationRequestParamsInput;
}
export interface GetRuleMigrationPrebuiltRulesProps {
@ -2667,6 +2734,10 @@ export interface GetRuleMigrationResourcesProps {
export interface GetRuleMigrationResourcesMissingProps {
params: GetRuleMigrationResourcesMissingRequestParamsInput;
}
export interface GetRuleMigrationRulesProps {
query: GetRuleMigrationRulesRequestQueryInput;
params: GetRuleMigrationRulesRequestParamsInput;
}
export interface GetRuleMigrationStatsProps {
params: GetRuleMigrationStatsRequestParamsInput;
}
@ -2793,7 +2864,10 @@ export interface UpdateRuleProps {
}
export interface UpdateRuleMigrationProps {
params: UpdateRuleMigrationRequestParamsInput;
body: UpdateRuleMigrationRequestBodyInput;
}
export interface UpdateRuleMigrationRulesProps {
params: UpdateRuleMigrationRulesRequestParamsInput;
body: UpdateRuleMigrationRulesRequestBodyInput;
}
export interface UpdateWorkflowInsightProps {
params: UpdateWorkflowInsightRequestParamsInput;

View file

@ -13,9 +13,8 @@ export const SIEM_RULE_MIGRATIONS_PATH = `${SIEM_MIGRATIONS_PATH}/rules` as cons
export const SIEM_RULE_MIGRATIONS_ALL_STATS_PATH = `${SIEM_RULE_MIGRATIONS_PATH}/stats` as const;
export const SIEM_RULE_MIGRATIONS_INTEGRATIONS_PATH =
`${SIEM_RULE_MIGRATIONS_PATH}/integrations` as const;
export const SIEM_RULE_MIGRATION_CREATE_PATH =
`${SIEM_RULE_MIGRATIONS_PATH}/{migration_id?}` as const;
export const SIEM_RULE_MIGRATION_PATH = `${SIEM_RULE_MIGRATIONS_PATH}/{migration_id}` as const;
export const SIEM_RULE_MIGRATION_RULES_PATH = `${SIEM_RULE_MIGRATION_PATH}/rules` as const;
export const SIEM_RULE_MIGRATION_START_PATH = `${SIEM_RULE_MIGRATION_PATH}/start` as const;
export const SIEM_RULE_MIGRATION_STATS_PATH = `${SIEM_RULE_MIGRATION_PATH}/stats` as const;
export const SIEM_RULE_MIGRATION_TRANSLATION_STATS_PATH =

View file

@ -19,9 +19,10 @@ import { ArrayFromString, BooleanFromString } from '@kbn/zod-helpers';
import {
RuleMigrationTaskStats,
OriginalRule,
UpdateRuleMigrationData,
RuleMigration,
OriginalRule,
RuleMigrationRule,
UpdateRuleMigrationRule,
RuleMigrationRetryFilter,
RuleMigrationTranslationStats,
PrebuiltRuleVersion,
@ -34,18 +35,6 @@ import { RelatedIntegration } from '../../../../api/detection_engine/model/rule_
import { NonEmptyString } from '../../../../api/model/primitives.gen';
import { ConnectorId, LangSmithOptions } from '../../common.gen';
export type CreateRuleMigrationRequestParams = z.infer<typeof CreateRuleMigrationRequestParams>;
export const CreateRuleMigrationRequestParams = z.object({
migration_id: NonEmptyString.optional(),
});
export type CreateRuleMigrationRequestParamsInput = z.input<
typeof CreateRuleMigrationRequestParams
>;
export type CreateRuleMigrationRequestBody = z.infer<typeof CreateRuleMigrationRequestBody>;
export const CreateRuleMigrationRequestBody = z.array(OriginalRule);
export type CreateRuleMigrationRequestBodyInput = z.input<typeof CreateRuleMigrationRequestBody>;
export type CreateRuleMigrationResponse = z.infer<typeof CreateRuleMigrationResponse>;
export const CreateRuleMigrationResponse = z.object({
/**
@ -54,24 +43,34 @@ export const CreateRuleMigrationResponse = z.object({
migration_id: NonEmptyString,
});
export type CreateRuleMigrationRulesRequestParams = z.infer<
typeof CreateRuleMigrationRulesRequestParams
>;
export const CreateRuleMigrationRulesRequestParams = z.object({
migration_id: NonEmptyString,
});
export type CreateRuleMigrationRulesRequestParamsInput = z.input<
typeof CreateRuleMigrationRulesRequestParams
>;
export type CreateRuleMigrationRulesRequestBody = z.infer<
typeof CreateRuleMigrationRulesRequestBody
>;
export const CreateRuleMigrationRulesRequestBody = z.array(OriginalRule);
export type CreateRuleMigrationRulesRequestBodyInput = z.input<
typeof CreateRuleMigrationRulesRequestBody
>;
export type DeleteRuleMigrationRequestParams = z.infer<typeof DeleteRuleMigrationRequestParams>;
export const DeleteRuleMigrationRequestParams = z.object({
migration_id: NonEmptyString,
});
export type DeleteRuleMigrationRequestParamsInput = z.input<
typeof DeleteRuleMigrationRequestParams
>;
export type GetAllStatsRuleMigrationResponse = z.infer<typeof GetAllStatsRuleMigrationResponse>;
export const GetAllStatsRuleMigrationResponse = z.array(RuleMigrationTaskStats);
export type GetRuleMigrationRequestQuery = z.infer<typeof GetRuleMigrationRequestQuery>;
export const GetRuleMigrationRequestQuery = z.object({
page: z.coerce.number().optional(),
per_page: z.coerce.number().optional(),
sort_field: NonEmptyString.optional(),
sort_direction: z.enum(['asc', 'desc']).optional(),
search_term: z.string().optional(),
ids: ArrayFromString(NonEmptyString).optional(),
is_prebuilt: BooleanFromString.optional(),
is_installed: BooleanFromString.optional(),
is_fully_translated: BooleanFromString.optional(),
is_partially_translated: BooleanFromString.optional(),
is_untranslatable: BooleanFromString.optional(),
is_failed: BooleanFromString.optional(),
});
export type GetRuleMigrationRequestQueryInput = z.input<typeof GetRuleMigrationRequestQuery>;
export type GetRuleMigrationRequestParams = z.infer<typeof GetRuleMigrationRequestParams>;
export const GetRuleMigrationRequestParams = z.object({
@ -80,13 +79,7 @@ export const GetRuleMigrationRequestParams = z.object({
export type GetRuleMigrationRequestParamsInput = z.input<typeof GetRuleMigrationRequestParams>;
export type GetRuleMigrationResponse = z.infer<typeof GetRuleMigrationResponse>;
export const GetRuleMigrationResponse = z.object({
/**
* The total number of rules in migration.
*/
total: z.number(),
data: z.array(RuleMigration),
});
export const GetRuleMigrationResponse = RuleMigration;
/**
* The map of related integrations, with the integration id as a key
@ -173,6 +166,41 @@ export type GetRuleMigrationResourcesMissingResponse = z.infer<
typeof GetRuleMigrationResourcesMissingResponse
>;
export const GetRuleMigrationResourcesMissingResponse = z.array(RuleMigrationResourceBase);
export type GetRuleMigrationRulesRequestQuery = z.infer<typeof GetRuleMigrationRulesRequestQuery>;
export const GetRuleMigrationRulesRequestQuery = z.object({
page: z.coerce.number().optional(),
per_page: z.coerce.number().optional(),
sort_field: NonEmptyString.optional(),
sort_direction: z.enum(['asc', 'desc']).optional(),
search_term: z.string().optional(),
ids: ArrayFromString(NonEmptyString).optional(),
is_prebuilt: BooleanFromString.optional(),
is_installed: BooleanFromString.optional(),
is_fully_translated: BooleanFromString.optional(),
is_partially_translated: BooleanFromString.optional(),
is_untranslatable: BooleanFromString.optional(),
is_failed: BooleanFromString.optional(),
});
export type GetRuleMigrationRulesRequestQueryInput = z.input<
typeof GetRuleMigrationRulesRequestQuery
>;
export type GetRuleMigrationRulesRequestParams = z.infer<typeof GetRuleMigrationRulesRequestParams>;
export const GetRuleMigrationRulesRequestParams = z.object({
migration_id: NonEmptyString,
});
export type GetRuleMigrationRulesRequestParamsInput = z.input<
typeof GetRuleMigrationRulesRequestParams
>;
export type GetRuleMigrationRulesResponse = z.infer<typeof GetRuleMigrationRulesResponse>;
export const GetRuleMigrationRulesResponse = z.object({
/**
* The total number of rules in migration.
*/
total: z.number(),
data: z.array(RuleMigrationRule),
});
export type GetRuleMigrationStatsRequestParams = z.infer<typeof GetRuleMigrationStatsRequestParams>;
export const GetRuleMigrationStatsRequestParams = z.object({
@ -275,12 +303,29 @@ export type UpdateRuleMigrationRequestParamsInput = z.input<
typeof UpdateRuleMigrationRequestParams
>;
export type UpdateRuleMigrationRequestBody = z.infer<typeof UpdateRuleMigrationRequestBody>;
export const UpdateRuleMigrationRequestBody = z.array(UpdateRuleMigrationData);
export type UpdateRuleMigrationRequestBodyInput = z.input<typeof UpdateRuleMigrationRequestBody>;
export type UpdateRuleMigrationResponse = z.infer<typeof UpdateRuleMigrationResponse>;
export const UpdateRuleMigrationResponse = z.object({
export const UpdateRuleMigrationResponse = RuleMigration;
export type UpdateRuleMigrationRulesRequestParams = z.infer<
typeof UpdateRuleMigrationRulesRequestParams
>;
export const UpdateRuleMigrationRulesRequestParams = z.object({
migration_id: NonEmptyString,
});
export type UpdateRuleMigrationRulesRequestParamsInput = z.input<
typeof UpdateRuleMigrationRulesRequestParams
>;
export type UpdateRuleMigrationRulesRequestBody = z.infer<
typeof UpdateRuleMigrationRulesRequestBody
>;
export const UpdateRuleMigrationRulesRequestBody = z.array(UpdateRuleMigrationRule);
export type UpdateRuleMigrationRulesRequestBodyInput = z.input<
typeof UpdateRuleMigrationRulesRequestBody
>;
export type UpdateRuleMigrationRulesResponse = z.infer<typeof UpdateRuleMigrationRulesResponse>;
export const UpdateRuleMigrationRulesResponse = z.object({
/**
* Indicates rules migrations have been updated.
*/

View file

@ -44,21 +44,113 @@ paths:
additionalProperties:
$ref: '../../../../../common/api/detection_engine/model/rule_schema/common_attributes.schema.yaml#/components/schemas/RelatedIntegration'
## Specific rule migration APIs
/internal/siem_migrations/rules/{migration_id}:
post:
/internal/siem_migrations/rules:
put:
summary: Creates a new rule migration
operationId: CreateRuleMigration
operationId: "CreateRuleMigration"
x-codegen-enabled: true
x-internal: true
description: Creates a new SIEM rules migration using the original vendor rules provided
description: Creates a new rule migration and returns the corresponding migration_id
tags:
- SIEM Rule Migrations
responses:
200:
description: The migration was created successfully and migrationId is returned
content:
application/json:
schema:
type: object
required:
- migration_id
properties:
migration_id:
description: The migration id created.
$ref: '../../../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString'
## Specific rule migration APIs
/internal/siem_migrations/rules/{migration_id}:
patch:
summary: Updates rule migration data
operationId: UpdateRuleMigration
x-codegen-enabled: true
x-internal: true
description: Updates rules migrations data
tags:
- SIEM Rule Migrations
parameters:
- name: migration_id
in: path
required: false
required: true
schema:
description: The migration id to start
$ref: '../../../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString'
responses:
200:
description: Indicates rules migrations have been updated correctly.
content:
application/json:
schema:
$ref: '../../rule_migration.schema.yaml#/components/schemas/RuleMigration'
404:
description: Indicates the migration id was not found.
get:
summary: Retrieves a rule migration
operationId: GetRuleMigration
x-codegen-enabled: true
x-internal: true
description: Retrieves the rule migration document stored in the system given the rule migration id
tags:
- SIEM Rule Migrations
parameters:
- name: migration_id
in: path
required: true
schema:
description: The migration id to retrieve
$ref: '../../../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString'
responses:
200:
description: Indicates rules migrations have been retrieved correctly.
content:
application/json:
schema:
$ref: '../../rule_migration.schema.yaml#/components/schemas/RuleMigration'
404:
description: Indicates the migration id was not found.
delete:
summary: Deletes a rule migration and its associated resources
operationId: DeleteRuleMigration
x-codegen-enabled: true
x-internal: true
description: Deletes a rule migration document stored in the system given the rule migration id
tags:
- SIEM Rule Migrations
parameters:
- name: migration_id
in: path
required: true
schema:
description: The migration id to delete
$ref: '../../../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString'
responses:
200:
description: Indicates rules migrations have been deleted correctly.
404:
description: Indicates the migration id was not found.
/internal/siem_migrations/rules/{migration_id}/rules:
post:
summary: Add rules to a rule migration
operationId: CreateRuleMigrationRules
x-codegen-enabled: true
x-internal: true
description: Adds original vendor rules to an already existing migration. Can be called multiple times to add more rules
tags:
- SIEM Rule Migrations
parameters:
- name: migration_id
in: path
required: true
schema:
description: The migration id to create rules for
$ref: '../../../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString'
@ -72,61 +164,13 @@ paths:
$ref: '../../rule_migration.schema.yaml#/components/schemas/OriginalRule'
responses:
200:
description: Indicates migration have been created correctly.
content:
application/json:
schema:
type: object
required:
- migration_id
properties:
migration_id:
description: The migration id created.
$ref: '../../../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString'
put:
summary: Updates rules of a migrations
operationId: UpdateRuleMigration
x-codegen-enabled: true
x-internal: true
description: Updates rules migrations attributes
tags:
- SIEM Rule Migrations
parameters:
- name: migration_id
in: path
required: true
schema:
description: The migration id to start
$ref: '../../../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString'
requestBody:
required: true
content:
application/json:
schema:
type: array
items:
$ref: '../../rule_migration.schema.yaml#/components/schemas/UpdateRuleMigrationData'
responses:
200:
description: Indicates rules migrations have been updated correctly.
content:
application/json:
schema:
type: object
required:
- updated
properties:
updated:
type: boolean
description: Indicates rules migrations have been updated.
description: Indicates rules have been added to the migration successfully.
get:
summary: Retrieves all the rules of a migration
operationId: GetRuleMigration
operationId: GetRuleMigrationRules
x-codegen-enabled: true
x-internal: true
description: Retrieves the rule documents stored in the system given the rule migration id
description: Retrieves the the list of rules included in a migration given the migration id
tags:
- SIEM Rule Migrations
parameters:
@ -202,7 +246,6 @@ paths:
required: false
schema:
type: boolean
responses:
200:
description: Indicates rule migration have been retrieved correctly.
@ -220,9 +263,45 @@ paths:
data:
type: array
items:
$ref: '../../rule_migration.schema.yaml#/components/schemas/RuleMigration'
204:
$ref: '../../rule_migration.schema.yaml#/components/schemas/RuleMigrationRule'
404:
description: Indicates the migration id was not found.
patch:
summary: Updates rules of a migrations
operationId: UpdateRuleMigrationRules
x-codegen-enabled: true
x-internal: true
description: Updates rules migrations attributes
tags:
- SIEM Rule Migrations
parameters:
- name: migration_id
in: path
required: true
schema:
description: The migration id to start
$ref: '../../../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString'
requestBody:
required: true
content:
application/json:
schema:
type: array
items:
$ref: '../../rule_migration.schema.yaml#/components/schemas/UpdateRuleMigrationRule'
responses:
200:
description: Indicates rules migrations have been updated correctly.
content:
application/json:
schema:
type: object
required:
- updated
properties:
updated:
type: boolean
description: Indicates rules migrations have been updated.
/internal/siem_migrations/rules/{migration_id}/install:
post:
@ -269,7 +348,7 @@ paths:
description: Indicates the number of successfully installed migration rules.
/internal/siem_migrations/rules/{migration_id}/start:
put:
post:
summary: Starts a rule migration
operationId: StartRuleMigration
x-codegen-enabled: true
@ -368,7 +447,7 @@ paths:
description: Indicates the migration id was not found.
/internal/siem_migrations/rules/{migration_id}/stop:
put:
post:
summary: Stops an existing rule migration
operationId: StopRuleMigration
x-codegen-enabled: true
@ -427,7 +506,6 @@ paths:
$ref: '../../rule_migration.schema.yaml#/components/schemas/PrebuiltRuleVersion'
# Rule migration resources APIs
/internal/siem_migrations/rules/{migration_id}/resources:
post:
summary: Creates or updates rule migration resources for a migration

View file

@ -141,6 +141,34 @@ export const PrebuiltRuleVersion = z.object({
current: RuleResponse.optional(),
});
/**
* The rule migration object ( without Id ) with its settings.
*/
export type RuleMigrationData = z.infer<typeof RuleMigrationData>;
export const RuleMigrationData = z.object({
/**
* The user profile ID of the user who created the migration.
*/
created_by: NonEmptyString,
/**
* The moment migration was created
*/
created_at: NonEmptyString,
});
/**
* The rule migration object with its settings.
*/
export type RuleMigration = z.infer<typeof RuleMigration>;
export const RuleMigration = z
.object({
/**
* The rule migration id
*/
id: NonEmptyString,
})
.merge(RuleMigrationData);
/**
* The rule translation result.
*/
@ -185,8 +213,8 @@ export const RuleMigrationComments = z.array(RuleMigrationComment);
/**
* The rule migration document object.
*/
export type RuleMigrationData = z.infer<typeof RuleMigrationData>;
export const RuleMigrationData = z.object({
export type RuleMigrationRuleData = z.infer<typeof RuleMigrationRuleData>;
export const RuleMigrationRuleData = z.object({
/**
* The moment of creation
*/
@ -232,15 +260,15 @@ export const RuleMigrationData = z.object({
/**
* The rule migration document object.
*/
export type RuleMigration = z.infer<typeof RuleMigration>;
export const RuleMigration = z
export type RuleMigrationRule = z.infer<typeof RuleMigrationRule>;
export const RuleMigrationRule = z
.object({
/**
* The rule migration id
*/
id: NonEmptyString,
})
.merge(RuleMigrationData);
.merge(RuleMigrationRuleData);
/**
* The status of the migration task.
@ -363,8 +391,8 @@ export const RuleMigrationTranslationStats = z.object({
/**
* The rule migration data object for rule update operation
*/
export type UpdateRuleMigrationData = z.infer<typeof UpdateRuleMigrationData>;
export const UpdateRuleMigrationData = z.object({
export type UpdateRuleMigrationRule = z.infer<typeof UpdateRuleMigrationRule>;
export const UpdateRuleMigrationRule = z.object({
/**
* The rule migration id
*/

View file

@ -117,7 +117,7 @@ components:
$ref: '../../../common/api/detection_engine/model/rule_schema/rule_schemas.schema.yaml#/components/schemas/RuleResponse'
RuleMigration:
description: The rule migration document object.
description: The rule migration object with its settings.
allOf:
- type: object
required:
@ -129,6 +129,33 @@ components:
- $ref: '#/components/schemas/RuleMigrationData'
RuleMigrationData:
type: object
description: The rule migration object ( without Id ) with its settings.
required:
- created_by
- created_at
properties:
created_by:
description: The user profile ID of the user who created the migration.
$ref: '../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString'
created_at:
description: The moment migration was created
$ref: '../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString'
RuleMigrationRule:
description: The rule migration document object.
allOf:
- type: object
required:
- id
properties:
id:
description: The rule migration id
$ref: '../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString'
- $ref: '#/components/schemas/RuleMigrationRuleData'
RuleMigrationRuleData:
type: object
description: The rule migration document object.
required:
@ -331,7 +358,7 @@ components:
description: The comments for the migration
$ref: '#/components/schemas/RuleMigrationComment'
UpdateRuleMigrationData:
UpdateRuleMigrationRule:
type: object
description: The rule migration data object for rule update operation
required:

View file

@ -68,7 +68,11 @@ export const RuleMigrationsPanels = React.memo<RuleMigrationsPanelsProps>(
</EuiFlexItem>
{latestMigrationsStats.map((migrationStats) => (
<EuiFlexItem grow={false} key={migrationStats.id}>
<EuiFlexItem
data-test-subj={`migration-${migrationStats.id}`}
grow={false}
key={migrationStats.id}
>
{(migrationStats.status === SiemMigrationTaskStatus.READY ||
migrationStats.status === SiemMigrationTaskStatus.STOPPED) && (
<MigrationReadyPanel migrationStats={migrationStats} />

View file

@ -8,7 +8,7 @@
import { v4 as uuidv4 } from 'uuid';
import { SiemMigrationTaskStatus } from '../../../../common/siem_migrations/constants';
import type {
GetRuleMigrationResponse,
GetRuleMigrationRulesResponse,
GetRuleMigrationTranslationStatsResponse,
} from '../../../../common/siem_migrations/model/api/rules/rule_migration.gen';
import type { RuleMigrationStats } from '../../rules/types';
@ -19,9 +19,9 @@ const getMockMigrationResultRule = ({
status = 'completed',
}: {
migrationId: string;
status?: GetRuleMigrationResponse['data'][number]['status'];
translationResult?: GetRuleMigrationResponse['data'][number]['translation_result'];
}): GetRuleMigrationResponse['data'][number] => {
status?: GetRuleMigrationRulesResponse['data'][number]['status'];
translationResult?: GetRuleMigrationRulesResponse['data'][number]['translation_result'];
}): GetRuleMigrationRulesResponse['data'][number] => {
const ruleId = uuidv4();
return {
migration_id: migrationId,
@ -92,7 +92,7 @@ export const mockedMigrationLatestStatsData: RuleMigrationStats[] = [
},
];
export const mockedMigrationResultsObj: Record<string, GetRuleMigrationResponse> = {
export const mockedMigrationResultsObj: Record<string, GetRuleMigrationRulesResponse> = {
'1': {
total: 2,
data: [

View file

@ -7,8 +7,8 @@
import { replaceParams } from '@kbn/openapi-common/shared';
import type { UpdateRuleMigrationRule } from '../../../../common/siem_migrations/model/rule_migration.gen';
import type { RuleMigrationFilters } from '../../../../common/siem_migrations/types';
import type { UpdateRuleMigrationData } from '../../../../common/siem_migrations/model/rule_migration.gen';
import type { LangSmithOptions } from '../../../../common/siem_migrations/model/common.gen';
import { KibanaServices } from '../../../common/lib/kibana';
@ -17,7 +17,6 @@ import {
SIEM_RULE_MIGRATIONS_PATH,
SIEM_RULE_MIGRATIONS_ALL_STATS_PATH,
SIEM_RULE_MIGRATION_INSTALL_PATH,
SIEM_RULE_MIGRATION_PATH,
SIEM_RULE_MIGRATION_START_PATH,
SIEM_RULE_MIGRATION_STATS_PATH,
SIEM_RULE_MIGRATION_TRANSLATION_STATS_PATH,
@ -26,12 +25,11 @@ import {
SIEM_RULE_MIGRATIONS_PREBUILT_RULES_PATH,
SIEM_RULE_MIGRATIONS_INTEGRATIONS_PATH,
SIEM_RULE_MIGRATION_MISSING_PRIVILEGES_PATH,
SIEM_RULE_MIGRATION_RULES_PATH,
} from '../../../../common/siem_migrations/constants';
import type {
CreateRuleMigrationRequestBody,
CreateRuleMigrationResponse,
GetAllStatsRuleMigrationResponse,
GetRuleMigrationResponse,
GetRuleMigrationTranslationStatsResponse,
InstallMigrationRulesResponse,
StartRuleMigrationRequestBody,
@ -44,6 +42,8 @@ import type {
StartRuleMigrationResponse,
GetRuleMigrationIntegrationsResponse,
GetRuleMigrationPrivilegesResponse,
GetRuleMigrationRulesResponse,
CreateRuleMigrationRulesRequestBody,
} from '../../../../common/siem_migrations/model/api/rules/rule_migration.gen';
export interface GetRuleMigrationStatsParams {
@ -78,22 +78,36 @@ export const getRuleMigrationsStatsAll = async ({
};
export interface CreateRuleMigrationParams {
/** Optional `id` of migration to add the rules to.
* The id is necessary only for batching the migration creation in multiple requests */
migrationId?: string;
/** The body containing the `connectorId` to use for the migration */
body: CreateRuleMigrationRequestBody;
/** Optional AbortSignal for cancelling request */
signal?: AbortSignal;
}
/** Starts a new migration with the provided rules. */
export const createRuleMigration = async ({
signal,
}: CreateRuleMigrationParams): Promise<CreateRuleMigrationResponse> => {
return KibanaServices.get().http.put<CreateRuleMigrationResponse>(SIEM_RULE_MIGRATIONS_PATH, {
version: '1',
signal,
});
};
export interface AddRulesToMigrationParams {
/** `id` of the migration to add the rules to */
migrationId: string;
/** The body containing the list of rules to be added to the migration */
body: CreateRuleMigrationRulesRequestBody;
/** Optional AbortSignal for cancelling request */
signal?: AbortSignal;
}
/** Adds provided rules to an existing migration */
export const addRulesToMigration = async ({
migrationId,
body,
signal,
}: CreateRuleMigrationParams): Promise<CreateRuleMigrationResponse> => {
return KibanaServices.get().http.post<CreateRuleMigrationResponse>(
`${SIEM_RULE_MIGRATIONS_PATH}${migrationId ? `/${migrationId}` : ''}`,
}: AddRulesToMigrationParams) => {
return KibanaServices.get().http.post<void>(
replaceParams(SIEM_RULE_MIGRATION_RULES_PATH, { migration_id: migrationId }),
{ body: JSON.stringify(body), version: '1', signal }
);
};
@ -160,13 +174,13 @@ export const startRuleMigration = async ({
retry,
langsmith_options: langSmithOptions,
};
return KibanaServices.get().http.put<StartRuleMigrationResponse>(
return KibanaServices.get().http.post<StartRuleMigrationResponse>(
replaceParams(SIEM_RULE_MIGRATION_START_PATH, { migration_id: migrationId }),
{ body: JSON.stringify(body), version: '1', signal }
);
};
export interface GetRuleMigrationParams {
export interface GetMigrationRulesParams {
/** `id` of the migration to get rules documents for */
migrationId: string;
/** Optional page number to retrieve */
@ -183,7 +197,7 @@ export interface GetRuleMigrationParams {
signal?: AbortSignal;
}
/** Retrieves all the migration rule documents of a specific migration. */
export const getRuleMigrations = async ({
export const getMigrationRules = async ({
migrationId,
page,
perPage,
@ -191,9 +205,9 @@ export const getRuleMigrations = async ({
sortDirection,
filters,
signal,
}: GetRuleMigrationParams): Promise<GetRuleMigrationResponse> => {
return KibanaServices.get().http.get<GetRuleMigrationResponse>(
replaceParams(SIEM_RULE_MIGRATION_PATH, { migration_id: migrationId }),
}: GetMigrationRulesParams): Promise<GetRuleMigrationRulesResponse> => {
return KibanaServices.get().http.get<GetRuleMigrationRulesResponse>(
replaceParams(SIEM_RULE_MIGRATION_RULES_PATH, { migration_id: migrationId }),
{
version: '1',
query: {
@ -306,7 +320,7 @@ export interface UpdateRulesParams {
/** `id` of the migration to install rules for */
migrationId: string;
/** The list of migration rules data to update */
rulesToUpdate: UpdateRuleMigrationData[];
rulesToUpdate: UpdateRuleMigrationRule[];
/** Optional AbortSignal for cancelling request */
signal?: AbortSignal;
}
@ -316,8 +330,8 @@ export const updateMigrationRules = async ({
rulesToUpdate,
signal,
}: UpdateRulesParams): Promise<UpdateRuleMigrationResponse> => {
return KibanaServices.get().http.put<UpdateRuleMigrationResponse>(
replaceParams(SIEM_RULE_MIGRATION_PATH, { migration_id: migrationId }),
return KibanaServices.get().http.patch<UpdateRuleMigrationResponse>(
replaceParams(SIEM_RULE_MIGRATION_RULES_PATH, { migration_id: migrationId }),
{ version: '1', body: JSON.stringify(rulesToUpdate), signal }
);
};

View file

@ -158,6 +158,7 @@ export const MigrationDataInputFlyout = React.memo<MigrationDataInputFlyoutProps
onClick={onStartMigration}
disabled={!migrationStats?.id}
isLoading={isStartLoading}
data-test-subj="startMigrationButton"
>
{isRetry ? (
<FormattedMessage

View file

@ -12,7 +12,7 @@ import type {
EuiFilePickerClass,
EuiFilePickerProps,
} from '@elastic/eui/src/components/form/file_picker/file_picker';
import type { CreateRuleMigrationRequestBody } from '../../../../../../../../../common/siem_migrations/model/api/rules/rule_migration.gen';
import type { CreateRuleMigrationRulesRequestBody } from '../../../../../../../../../common/siem_migrations/model/api/rules/rule_migration.gen';
import type { OriginalRule } from '../../../../../../../../../common/siem_migrations/model/rule_migration.gen';
import type { CreateMigration } from '../../../../../../service/hooks/use_create_migration';
import { FILE_UPLOAD_ERROR } from '../../../../translations';
@ -31,7 +31,7 @@ export interface RulesFileUploadProps {
}
export const RulesFileUpload = React.memo<RulesFileUploadProps>(
({ createMigration, apiError, isLoading, isCreated }) => {
const [rulesToUpload, setRulesToUpload] = useState<CreateRuleMigrationRequestBody>([]);
const [rulesToUpload, setRulesToUpload] = useState<CreateRuleMigrationRulesRequestBody>([]);
const filePickerRef = useRef<EuiFilePickerClass>(null);
const createRules = useCallback(() => {

View file

@ -147,7 +147,7 @@ export const MigrationResultPanel = React.memo<MigrationResultPanelProps>(
) : (
translationStats && (
<>
<EuiText size="m" style={{ textAlign: 'center' }}>
<EuiText size="m" css={{ textAlign: 'center' }}>
<b>{i18n.RULE_MIGRATION_SUMMARY_CHART_TITLE}</b>
</EuiText>
<TranslationResultsChart translationStats={translationStats} />
@ -248,7 +248,7 @@ const columns: Array<EuiBasicTableColumn<TranslationResultsTableItem>> = [
name: i18n.RULE_MIGRATION_TABLE_COLUMN_STATUS,
render: (title: string, { color }) => (
<EuiHealth color={color} textSize="xs">
{title}
<span data-test-subj={`translationStatus-${title}`}>{title} </span>
</EuiHealth>
),
},
@ -256,7 +256,11 @@ const columns: Array<EuiBasicTableColumn<TranslationResultsTableItem>> = [
field: 'value',
name: i18n.RULE_MIGRATION_TABLE_COLUMN_RULES,
align: 'right',
render: (value: string) => <EuiText size="xs">{value}</EuiText>,
render: (value: string, { title }) => (
<EuiText size="xs" data-test-subj={`translationStatusCount-${title}`}>
{value}
</EuiText>
),
},
];
@ -290,6 +294,13 @@ const TranslationResultsTable = React.memo<{
[translationStats, translationResultColors]
);
return <EuiBasicTable items={items} columns={columns} compressed />;
return (
<EuiBasicTable
data-test-subj="translatedResultsTable"
items={items}
columns={columns}
compressed
/>
);
});
TranslationResultsTable.displayName = 'TranslationResultsTable';

View file

@ -28,7 +28,7 @@ import {
import type { EuiTabbedContentTab, EuiTabbedContentProps, EuiFlyoutProps } from '@elastic/eui';
import { RuleTranslationResult } from '../../../../../common/siem_migrations/constants';
import type { RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen';
import type { RuleMigrationRule } from '../../../../../common/siem_migrations/model/rule_migration.gen';
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
import {
RuleOverviewTab,
@ -70,7 +70,7 @@ export const TabContentPadding: FC<PropsWithChildren<unknown>> = ({ children })
);
interface MigrationRuleDetailsFlyoutProps {
ruleMigration: RuleMigration;
migrationRule: RuleMigrationRule;
ruleActions?: React.ReactNode;
matchedPrebuiltRule?: RuleResponse;
size?: EuiFlyoutProps['size'];
@ -82,7 +82,7 @@ interface MigrationRuleDetailsFlyoutProps {
export const MigrationRuleDetailsFlyout: React.FC<MigrationRuleDetailsFlyoutProps> = React.memo(
({
ruleActions,
ruleMigration,
migrationRule,
matchedPrebuiltRule,
size = 'm',
extraTabs = [],
@ -93,7 +93,7 @@ export const MigrationRuleDetailsFlyout: React.FC<MigrationRuleDetailsFlyoutProp
const { expandedOverviewSections, toggleOverviewSection } = useOverviewTabSections();
const { mutateAsync: updateMigrationRule } = useUpdateMigrationRule(ruleMigration);
const { mutateAsync: updateMigrationRule } = useUpdateMigrationRule(migrationRule);
const [isUpdating, setIsUpdating] = useState(false);
const isLoading = isDataLoading || isUpdating;
@ -106,7 +106,7 @@ export const MigrationRuleDetailsFlyout: React.FC<MigrationRuleDetailsFlyoutProp
setIsUpdating(true);
try {
await updateMigrationRule({
id: ruleMigration.id,
id: migrationRule.id,
elastic_rule: {
title: ruleName,
query: ruleQuery,
@ -119,16 +119,16 @@ export const MigrationRuleDetailsFlyout: React.FC<MigrationRuleDetailsFlyoutProp
setIsUpdating(false);
}
},
[isLoading, updateMigrationRule, ruleMigration, addError]
[isLoading, updateMigrationRule, migrationRule, addError]
);
const ruleDetailsToOverview = useMemo(() => {
const elasticRule = ruleMigration?.elastic_rule;
const elasticRule = migrationRule?.elastic_rule;
if (isMigrationCustomRule(elasticRule)) {
return convertMigrationCustomRuleToSecurityRulePayload(elasticRule, false);
}
return matchedPrebuiltRule;
}, [ruleMigration, matchedPrebuiltRule]);
}, [migrationRule, matchedPrebuiltRule]);
const translationTab: EuiTabbedContentTab = useMemo(
() => ({
@ -137,14 +137,14 @@ export const MigrationRuleDetailsFlyout: React.FC<MigrationRuleDetailsFlyoutProp
content: (
<TabContentPadding>
<TranslationTab
ruleMigration={ruleMigration}
migrationRule={migrationRule}
matchedPrebuiltRule={matchedPrebuiltRule}
onTranslationUpdate={handleTranslationUpdate}
/>
</TabContentPadding>
),
}),
[ruleMigration, handleTranslationUpdate, matchedPrebuiltRule]
[migrationRule, handleTranslationUpdate, matchedPrebuiltRule]
);
const overviewTab: EuiTabbedContentTab = useMemo(
@ -167,14 +167,14 @@ export const MigrationRuleDetailsFlyout: React.FC<MigrationRuleDetailsFlyoutProp
)}
</TabContentPadding>
),
disabled: ruleMigration.translation_result === RuleTranslationResult.UNTRANSLATABLE,
disabled: migrationRule.translation_result === RuleTranslationResult.UNTRANSLATABLE,
}),
[
ruleDetailsToOverview,
size,
expandedOverviewSections,
toggleOverviewSection,
ruleMigration.translation_result,
migrationRule.translation_result,
]
);
@ -184,11 +184,11 @@ export const MigrationRuleDetailsFlyout: React.FC<MigrationRuleDetailsFlyoutProp
name: i18n.SUMMARY_TAB_LABEL,
content: (
<TabContentPadding>
<SummaryTab ruleMigration={ruleMigration} />
<SummaryTab migrationRule={migrationRule} />
</TabContentPadding>
),
}),
[ruleMigration]
[migrationRule]
);
const tabs = useMemo(() => {
@ -237,12 +237,12 @@ export const MigrationRuleDetailsFlyout: React.FC<MigrationRuleDetailsFlyoutProp
<EuiTitle size="m">
<h2 id={migrationsRulesFlyoutTitleId}>
{ruleDetailsToOverview?.name ??
ruleMigration.original_rule.title ??
migrationRule.original_rule.title ??
i18n.UNKNOWN_MIGRATION_RULE_TITLE}
</h2>
</EuiTitle>
<EuiSpacer size="s" />
<UpdatedByLabel ruleMigration={ruleMigration} />
<UpdatedByLabel migrationRule={migrationRule} />
</EuiFlyoutHeader>
<EuiFlyoutBody
// EUI TODO: We need to set transform to 'none' to avoid drag/drop issues in the flyout caused by the

View file

@ -13,7 +13,7 @@ import { AssistantAvatar } from '@kbn/ai-assistant-icon';
import { UserAvatar } from '@kbn/user-profile-components';
import { USER_AVATAR_ITEM_TEST_ID } from '../../../../../../common/components/user_profiles/test_ids';
import { useBulkGetUserProfiles } from '../../../../../../common/components/user_profiles/use_bulk_get_user_profiles';
import { type RuleMigration } from '../../../../../../../common/siem_migrations/model/rule_migration.gen';
import { type RuleMigrationRule } from '../../../../../../../common/siem_migrations/model/rule_migration.gen';
import {
RuleTranslationResult,
SIEM_MIGRATIONS_ASSISTANT_USER,
@ -21,19 +21,19 @@ import {
import * as i18n from './translations';
interface SummaryTabProps {
ruleMigration: RuleMigration;
migrationRule: RuleMigrationRule;
}
export const SummaryTab: React.FC<SummaryTabProps> = React.memo(({ ruleMigration }) => {
export const SummaryTab: React.FC<SummaryTabProps> = React.memo(({ migrationRule }) => {
const userProfileIds = useMemo<Set<string>>(() => {
if (!ruleMigration.comments) {
if (!migrationRule.comments) {
return new Set();
}
return ruleMigration.comments.reduce((acc, { created_by: createdBy }) => {
return migrationRule.comments.reduce((acc, { created_by: createdBy }) => {
if (createdBy !== SIEM_MIGRATIONS_ASSISTANT_USER) acc.add(createdBy);
return acc;
}, new Set<string>());
}, [ruleMigration.comments]);
}, [migrationRule.comments]);
const { isLoading: isLoadingUserProfiles, data: userProfiles } = useBulkGetUserProfiles({
uids: userProfileIds,
});
@ -42,7 +42,7 @@ export const SummaryTab: React.FC<SummaryTabProps> = React.memo(({ ruleMigration
if (isLoadingUserProfiles) {
return undefined;
}
return ruleMigration.comments?.map(
return migrationRule.comments?.map(
({ message, created_at: createdAt, created_by: createdBy }) => {
const profile = userProfiles?.find(({ uid }) => uid === createdBy);
const isCreatedByAssistant = createdBy === SIEM_MIGRATIONS_ASSISTANT_USER || !profile;
@ -63,7 +63,7 @@ export const SummaryTab: React.FC<SummaryTabProps> = React.memo(({ ruleMigration
/>
),
event:
ruleMigration.translation_result === RuleTranslationResult.UNTRANSLATABLE
migrationRule.translation_result === RuleTranslationResult.UNTRANSLATABLE
? i18n.COMMENT_EVENT_UNTRANSLATABLE
: i18n.COMMENT_EVENT_TRANSLATED,
timestamp: moment(createdAt).format('ll'), // Date formats https://momentjs.com/docs/#/displaying/format/
@ -73,8 +73,8 @@ export const SummaryTab: React.FC<SummaryTabProps> = React.memo(({ ruleMigration
);
}, [
isLoadingUserProfiles,
ruleMigration.comments,
ruleMigration.translation_result,
migrationRule.comments,
migrationRule.translation_result,
userProfiles,
]);

View file

@ -9,10 +9,8 @@ import type { FC } from 'react';
import React from 'react';
import type { IconType } from '@elastic/eui';
import { EuiCallOut } from '@elastic/eui';
import {
type RuleMigration,
type RuleMigrationTranslationResult,
} from '../../../../../../../common/siem_migrations/model/rule_migration.gen';
import type { RuleMigrationRule } from '../../../../../../../common/siem_migrations/model/rule_migration.gen';
import { type RuleMigrationTranslationResult } from '../../../../../../../common/siem_migrations/model/rule_migration.gen';
import * as i18n from './translations';
type RuleMigrationTranslationCallOutMode = RuleMigrationTranslationResult | 'mapped';
@ -51,17 +49,17 @@ const getCallOutInfo = (
};
export interface TranslationCallOutProps {
ruleMigration: RuleMigration;
migrationRule: RuleMigrationRule;
}
export const TranslationCallOut: FC<TranslationCallOutProps> = React.memo(({ ruleMigration }) => {
if (!ruleMigration.translation_result) {
export const TranslationCallOut: FC<TranslationCallOutProps> = React.memo(({ migrationRule }) => {
if (!migrationRule.translation_result) {
return null;
}
const mode = ruleMigration.elastic_rule?.prebuilt_rule_id
const mode = migrationRule.elastic_rule?.prebuilt_rule_id
? 'mapped'
: ruleMigration.translation_result;
: migrationRule.translation_result;
const { title, message, icon, color } = getCallOutInfo(mode);
return (

View file

@ -21,7 +21,7 @@ import { css } from '@emotion/css';
import { FormattedMessage } from '@kbn/i18n-react';
import { RuleTranslationResult } from '../../../../../../../common/siem_migrations/constants';
import type { RuleResponse } from '../../../../../../../common/api/detection_engine';
import type { RuleMigration } from '../../../../../../../common/siem_migrations/model/rule_migration.gen';
import type { RuleMigrationRule } from '../../../../../../../common/siem_migrations/model/rule_migration.gen';
import { TranslationTabHeader } from './header';
import * as i18n from './translations';
import {
@ -32,23 +32,23 @@ import { TranslationCallOut } from './callout';
import { OriginalRuleQuery, TranslatedRuleQuery } from './query_details';
interface TranslationTabProps {
ruleMigration: RuleMigration;
migrationRule: RuleMigrationRule;
matchedPrebuiltRule?: RuleResponse;
onTranslationUpdate?: (ruleName: string, ruleQuery: string) => Promise<void>;
}
export const TranslationTab: React.FC<TranslationTabProps> = React.memo(
({ ruleMigration, matchedPrebuiltRule, onTranslationUpdate }) => {
({ migrationRule, matchedPrebuiltRule, onTranslationUpdate }) => {
const { euiTheme } = useEuiTheme();
const isInstalled = !!ruleMigration.elastic_rule?.id;
const isInstalled = !!migrationRule.elastic_rule?.id;
return (
<>
<EuiSpacer size="m" />
{ruleMigration.translation_result && !isInstalled && (
{migrationRule.translation_result && !isInstalled && (
<>
<TranslationCallOut ruleMigration={ruleMigration} />
<TranslationCallOut migrationRule={migrationRule} />
<EuiSpacer size="m" />
</>
)}
@ -74,11 +74,12 @@ export const TranslationTab: React.FC<TranslationTabProps> = React.memo(
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiBadge
color={convertTranslationResultIntoColor(ruleMigration.translation_result)}
data-test-subj="translationResultBadge"
color={convertTranslationResultIntoColor(migrationRule.translation_result)}
>
{isInstalled
? i18n.INSTALLED_LABEL
: convertTranslationResultIntoText(ruleMigration.translation_result)}
: convertTranslationResultIntoText(migrationRule.translation_result)}
</EuiBadge>
</EuiFlexItem>
</EuiFlexGroup>
@ -86,7 +87,7 @@ export const TranslationTab: React.FC<TranslationTabProps> = React.memo(
<EuiSplitPanel.Inner grow>
<EuiFlexGroup gutterSize="s" alignItems="flexStart">
<EuiFlexItem grow={1}>
<OriginalRuleQuery ruleMigration={ruleMigration} />
<OriginalRuleQuery migrationRule={migrationRule} />
</EuiFlexItem>
<EuiFlexItem
grow={0}
@ -97,7 +98,7 @@ export const TranslationTab: React.FC<TranslationTabProps> = React.memo(
/>
<EuiFlexItem grow={1}>
<TranslatedRuleQuery
ruleMigration={ruleMigration}
migrationRule={migrationRule}
matchedPrebuiltRule={matchedPrebuiltRule}
onTranslationUpdate={onTranslationUpdate}
/>
@ -107,8 +108,8 @@ export const TranslationTab: React.FC<TranslationTabProps> = React.memo(
</EuiSplitPanel.Outer>
</EuiFlexItem>
</EuiAccordion>
{ruleMigration.translation_result === RuleTranslationResult.FULL &&
!ruleMigration.elastic_rule?.id && (
{migrationRule.translation_result === RuleTranslationResult.FULL &&
!migrationRule.elastic_rule?.id && (
<>
<EuiSpacer size="m" />
<EuiCallOut

View file

@ -7,25 +7,25 @@
import React from 'react';
import { EuiHorizontalRule } from '@elastic/eui';
import type { RuleMigration } from '../../../../../../../../common/siem_migrations/model/rule_migration.gen';
import type { RuleMigrationRule } from '../../../../../../../../common/siem_migrations/model/rule_migration.gen';
import { QueryHeader } from './header';
import { QueryViewer } from './query_viewer';
import * as i18n from './translations';
interface OriginalRuleQueryProps {
ruleMigration: RuleMigration;
migrationRule: RuleMigrationRule;
}
export const OriginalRuleQuery: React.FC<OriginalRuleQueryProps> = React.memo(
({ ruleMigration }) => {
({ migrationRule }) => {
return (
<>
<QueryHeader title={i18n.SPLUNK_QUERY_TITLE} tooltip={i18n.SPLUNK_QUERY_TOOLTIP} />
<EuiHorizontalRule margin="xs" />
<QueryViewer
ruleName={ruleMigration.original_rule.title}
query={ruleMigration.original_rule.query}
language={ruleMigration.original_rule.query_language}
ruleName={migrationRule.original_rule.title}
query={migrationRule.original_rule.query}
language={migrationRule.original_rule.query_language}
/>
</>
);

View file

@ -64,7 +64,12 @@ export const QueryEditor: React.FC<QueryEditorProps> = React.memo(
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty iconType="save" onClick={handleSaveButtonClick} size="xs">
<EuiButtonEmpty
data-test-subj="saveTranslatedRuleBtn"
iconType="save"
onClick={handleSaveButtonClick}
size="xs"
>
{i18n.SAVE}
</EuiButtonEmpty>
</EuiFlexItem>

View file

@ -39,7 +39,12 @@ export const QueryViewer: React.FC<QueryViewerProps> = React.memo(
{onEdit ? (
<EuiFlexGroup direction="row" justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<EuiButtonEmpty iconType="pencil" onClick={onEdit} size="xs">
<EuiButtonEmpty
data-test-subj="editTranslatedRuleBtn"
iconType="pencil"
onClick={onEdit}
size="xs"
>
{i18n.EDIT}
</EuiButtonEmpty>
</EuiFlexItem>
@ -53,6 +58,7 @@ export const QueryViewer: React.FC<QueryViewerProps> = React.memo(
<EuiSpacer size="m" />
{query.length ? (
<EuiCodeBlock
data-test-subj="translatedRuleQueryViewer"
language={codeBlockLanguage}
fontSize="s"
paddingSize="s"

View file

@ -11,7 +11,7 @@ import type {
QueryLanguage,
RuleResponse,
} from '../../../../../../../../common/api/detection_engine';
import type { RuleMigration } from '../../../../../../../../common/siem_migrations/model/rule_migration.gen';
import type { RuleMigrationRule } from '../../../../../../../../common/siem_migrations/model/rule_migration.gen';
import { VALIDATION_WARNING_CODES } from '../../../../../../../detection_engine/rule_creation/constants/validation_warning_codes';
import { useFormWithWarnings } from '../../../../../../../common/hooks/use_form_with_warnings';
import type { RuleTranslationSchema } from '../types';
@ -35,22 +35,22 @@ const transformQueryLanguage = (language: QueryLanguage) => {
};
interface TranslatedRuleQueryProps {
ruleMigration: RuleMigration;
migrationRule: RuleMigrationRule;
matchedPrebuiltRule?: RuleResponse;
onTranslationUpdate?: (ruleName: string, ruleQuery: string) => Promise<void>;
}
export const TranslatedRuleQuery: React.FC<TranslatedRuleQueryProps> = React.memo(
({ ruleMigration, matchedPrebuiltRule, onTranslationUpdate }) => {
const isInstalled = !!ruleMigration.elastic_rule?.id;
({ migrationRule, matchedPrebuiltRule, onTranslationUpdate }) => {
const isInstalled = !!migrationRule.elastic_rule?.id;
const canEdit = !matchedPrebuiltRule && !isInstalled;
const translatedData = useMemo(() => {
let ruleName = ruleMigration.elastic_rule?.title ?? '';
let ruleName = migrationRule.elastic_rule?.title ?? '';
let title = i18n.CUSTOM_TRANSLATION_TITLE;
let titleTooltip = i18n.TRANSLATION_QUERY_TOOLTIP;
let query = ruleMigration.elastic_rule?.query ?? '';
let language = ruleMigration.elastic_rule?.query_language ?? '';
let query = migrationRule.elastic_rule?.query ?? '';
let language = migrationRule.elastic_rule?.query_language ?? '';
let queryPlaceholder = i18n.TRANSLATION_QUERY_PLACEHOLDER;
if (matchedPrebuiltRule) {
ruleName = matchedPrebuiltRule.name;
@ -72,7 +72,7 @@ export const TranslatedRuleQuery: React.FC<TranslatedRuleQueryProps> = React.mem
language,
queryPlaceholder,
};
}, [matchedPrebuiltRule, ruleMigration.elastic_rule]);
}, [matchedPrebuiltRule, migrationRule.elastic_rule]);
const formDefaultValue: RuleTranslationSchema = useMemo(() => {
return {

View file

@ -11,19 +11,19 @@ import { EuiText } from '@elastic/eui';
import { FormattedDate } from '../../../../../common/components/formatted_date';
import { useBulkGetUserProfiles } from '../../../../../common/components/user_profiles/use_bulk_get_user_profiles';
import type { RuleMigration } from '../../../../../../common/siem_migrations/model/rule_migration.gen';
import type { RuleMigrationRule } from '../../../../../../common/siem_migrations/model/rule_migration.gen';
import * as i18n from './translations';
interface UpdatedByLabelProps {
ruleMigration: RuleMigration;
migrationRule: RuleMigrationRule;
}
export const UpdatedByLabel: React.FC<UpdatedByLabelProps> = React.memo(
({ ruleMigration }: UpdatedByLabelProps) => {
({ migrationRule }: UpdatedByLabelProps) => {
const userProfileId = useMemo(
() => new Set([ruleMigration.updated_by ?? ruleMigration.created_by]),
[ruleMigration.created_by, ruleMigration.updated_by]
() => new Set([migrationRule.updated_by ?? migrationRule.created_by]),
[migrationRule.created_by, migrationRule.updated_by]
);
const { isLoading: isLoadingUserProfiles, data: userProfiles } = useBulkGetUserProfiles({
uids: userProfileId,
@ -35,7 +35,7 @@ export const UpdatedByLabel: React.FC<UpdatedByLabelProps> = React.memo(
const userProfile = userProfiles[0];
const updatedBy = userProfile.user.full_name ?? userProfile.user.username;
const updatedAt = ruleMigration.updated_at ?? ruleMigration['@timestamp'];
const updatedAt = migrationRule.updated_at ?? migrationRule['@timestamp'];
return (
<EuiText size="xs">
<FormattedMessage

View file

@ -21,7 +21,7 @@ import React, { useCallback, useMemo, useState } from 'react';
import type { RelatedIntegration, RuleResponse } from '../../../../../common/api/detection_engine';
import { isMigrationPrebuiltRule } from '../../../../../common/siem_migrations/rules/utils';
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
import type { RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen';
import type { RuleMigrationRule } from '../../../../../common/siem_migrations/model/rule_migration.gen';
import { EmptyMigration } from './empty_migration';
import { useMigrationRulesTableColumns } from '../../hooks/use_migration_rules_table_columns';
import { useMigrationRuleDetailsFlyout } from '../../hooks/use_migration_rule_preview_flyout';
@ -79,7 +79,7 @@ export const MigrationRulesTable: React.FC<MigrationRulesTableProps> = React.mem
const [pageIndex, setPageIndex] = useState(0);
const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE);
const [sortField, setSortField] = useState<keyof RuleMigration>(DEFAULT_SORT_FIELD);
const [sortField, setSortField] = useState<keyof RuleMigrationRule>(DEFAULT_SORT_FIELD);
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>(DEFAULT_SORT_DIRECTION);
const [searchTerm, setSearchTerm] = useState<string | undefined>();
@ -93,7 +93,7 @@ export const MigrationRulesTable: React.FC<MigrationRulesTableProps> = React.mem
useGetMigrationPrebuiltRules(migrationId);
const {
data: { ruleMigrations, total } = { ruleMigrations: [], total: 0 },
data: { migrationRules, total } = { migrationRules: [], total: 0 },
isLoading: isDataLoading,
} = useGetMigrationRules({
migrationId,
@ -107,13 +107,13 @@ export const MigrationRulesTable: React.FC<MigrationRulesTableProps> = React.mem
},
});
const [selectedRuleMigrations, setSelectedRuleMigrations] = useState<RuleMigration[]>([]);
const tableSelection: EuiTableSelectionType<RuleMigration> = useMemo(
const [selectedMigrationRules, setSelectedMigrationRules] = useState<RuleMigrationRule[]>([]);
const tableSelection: EuiTableSelectionType<RuleMigrationRule> = useMemo(
() => ({
selectable: (item: RuleMigration) => {
selectable: (item: RuleMigrationRule) => {
return !item.elastic_rule?.id && item.translation_result === RuleTranslationResult.FULL;
},
selectableMessage: (selectable: boolean, item: RuleMigration) => {
selectableMessage: (selectable: boolean, item: RuleMigrationRule) => {
if (selectable) {
return '';
}
@ -121,10 +121,10 @@ export const MigrationRulesTable: React.FC<MigrationRulesTableProps> = React.mem
? i18n.ALREADY_TRANSLATED_RULE_TOOLTIP
: i18n.NOT_FULLY_TRANSLATED_RULE_TOOLTIP;
},
onSelectionChange: setSelectedRuleMigrations,
selected: selectedRuleMigrations,
onSelectionChange: setSelectedMigrationRules,
selected: selectedMigrationRules,
}),
[selectedRuleMigrations]
[selectedMigrationRules]
);
const pagination = useMemo(() => {
@ -144,7 +144,8 @@ export const MigrationRulesTable: React.FC<MigrationRulesTableProps> = React.mem
};
}, [sortDirection, sortField]);
const onTableChange = useCallback(({ page, sort }: CriteriaWithPagination<RuleMigration>) => {
const onTableChange = useCallback(
({ page, sort }: CriteriaWithPagination<RuleMigrationRule>) => {
if (page) {
setPageIndex(page.index);
setPageSize(page.size);
@ -154,7 +155,9 @@ export const MigrationRulesTable: React.FC<MigrationRulesTableProps> = React.mem
setSortField(field);
setSortDirection(direction);
}
}, []);
},
[]
);
const handleOnSearch = useCallback((value: string) => {
setSearchTerm(value.trim());
@ -169,10 +172,10 @@ export const MigrationRulesTable: React.FC<MigrationRulesTableProps> = React.mem
const [isTableLoading, setTableLoading] = useState(false);
const installSingleRule = useCallback(
async (ruleMigration: RuleMigration, enabled?: boolean) => {
async (migrationRule: RuleMigrationRule, enabled?: boolean) => {
setTableLoading(true);
try {
await installMigrationRule({ ruleMigration, enabled });
await installMigrationRule({ migrationRule, enabled });
} catch (error) {
addError(error, { title: logicI18n.INSTALL_MIGRATION_RULES_FAILURE });
} finally {
@ -187,17 +190,17 @@ export const MigrationRulesTable: React.FC<MigrationRulesTableProps> = React.mem
setTableLoading(true);
try {
await installMigrationRules({
ids: selectedRuleMigrations.map((rule) => rule.id),
ids: selectedMigrationRules.map((rule) => rule.id),
enabled,
});
} catch (error) {
addError(error, { title: logicI18n.INSTALL_MIGRATION_RULES_FAILURE });
} finally {
setTableLoading(false);
setSelectedRuleMigrations([]);
setSelectedMigrationRules([]);
}
},
[addError, installMigrationRules, selectedRuleMigrations]
[addError, installMigrationRules, selectedMigrationRules]
);
const installTranslatedRules = useCallback(
@ -222,18 +225,18 @@ export const MigrationRulesTable: React.FC<MigrationRulesTableProps> = React.mem
isPrebuiltRulesLoading || isDataLoading || isTableLoading || isRetryLoading;
const ruleActionsFactory = useCallback(
(ruleMigration: RuleMigration, closeRulePreview: () => void) => {
(migrationRule: RuleMigrationRule, closeRulePreview: () => void) => {
const canMigrationRuleBeInstalled =
!isRulesLoading &&
!ruleMigration.elastic_rule?.id &&
ruleMigration.translation_result === RuleTranslationResult.FULL;
!migrationRule.elastic_rule?.id &&
migrationRule.translation_result === RuleTranslationResult.FULL;
return (
<EuiFlexGroup>
<EuiFlexItem>
<EuiButton
disabled={!canMigrationRuleBeInstalled}
onClick={() => {
installSingleRule(ruleMigration);
installSingleRule(migrationRule);
closeRulePreview();
}}
data-test-subj="installMigrationRuleFromFlyoutButton"
@ -241,12 +244,12 @@ export const MigrationRulesTable: React.FC<MigrationRulesTableProps> = React.mem
{i18n.INSTALL_WITHOUT_ENABLING_BUTTON_LABEL}
</EuiButton>
</EuiFlexItem>
{isMigrationPrebuiltRule(ruleMigration.elastic_rule) && (
{isMigrationPrebuiltRule(migrationRule.elastic_rule) && (
<EuiFlexItem>
<EuiButton
disabled={!canMigrationRuleBeInstalled}
onClick={() => {
installSingleRule(ruleMigration, true);
installSingleRule(migrationRule, true);
closeRulePreview();
}}
fill
@ -264,27 +267,32 @@ export const MigrationRulesTable: React.FC<MigrationRulesTableProps> = React.mem
const getMigrationRuleData = useCallback(
(ruleId: string) => {
if (!isRulesLoading && ruleMigrations.length) {
const ruleMigration = ruleMigrations.find((item) => item.id === ruleId);
if (!isRulesLoading && migrationRules.length) {
const migrationRule = migrationRules.find((item) => item.id === ruleId);
let matchedPrebuiltRule: RuleResponse | undefined;
let relatedIntegrations: RelatedIntegration[] = [];
if (ruleMigration) {
if (migrationRule) {
// Find matched prebuilt rule if any and prioritize its installed version
const prebuiltRuleId = ruleMigration.elastic_rule?.prebuilt_rule_id;
const prebuiltRuleId = migrationRule.elastic_rule?.prebuilt_rule_id;
const prebuiltRuleVersions = prebuiltRuleId ? prebuiltRules[prebuiltRuleId] : undefined;
matchedPrebuiltRule = prebuiltRuleVersions?.current ?? prebuiltRuleVersions?.target;
const integrationIds = ruleMigration.elastic_rule?.integration_ids;
const integrationIds = migrationRule.elastic_rule?.integration_ids;
if (integrations && integrationIds) {
relatedIntegrations = integrationIds
.map((integrationId) => integrations[integrationId])
.filter((integration) => integration != null);
}
}
return { ruleMigration, matchedPrebuiltRule, relatedIntegrations, isIntegrationsLoading };
return {
migrationRule,
matchedPrebuiltRule,
relatedIntegrations,
isIntegrationsLoading,
};
}
},
[integrations, isIntegrationsLoading, isRulesLoading, prebuiltRules, ruleMigrations]
[integrations, isIntegrationsLoading, isRulesLoading, prebuiltRules, migrationRules]
);
const {
@ -340,7 +348,7 @@ export const MigrationRulesTable: React.FC<MigrationRulesTableProps> = React.mem
isTableLoading={isRulesLoading}
numberOfFailedRules={translationStats.rules.failed}
numberOfTranslatedRules={translationStats.rules.success.installable}
numberOfSelectedRules={selectedRuleMigrations.length}
numberOfSelectedRules={selectedMigrationRules.length}
installTranslatedRule={installTranslatedRules}
installSelectedRule={installSelectedRule}
reprocessFailedRules={reprocessFailedRules}
@ -348,9 +356,9 @@ export const MigrationRulesTable: React.FC<MigrationRulesTableProps> = React.mem
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="m" />
<EuiBasicTable<RuleMigration>
<EuiBasicTable<RuleMigrationRule>
loading={isTableLoading}
items={ruleMigrations}
items={migrationRules}
pagination={pagination}
sorting={sorting}
onChange={onTableChange}

View file

@ -15,16 +15,16 @@ import {
} from '../../../../../common/siem_migrations/constants';
import { getRuleDetailsUrl } from '../../../../common/components/link_to';
import { SecurityPageName } from '../../../../../common';
import { type RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen';
import { type RuleMigrationRule } from '../../../../../common/siem_migrations/model/rule_migration.gen';
import * as i18n from './translations';
import { type TableColumn } from './constants';
import { TableHeader } from './header';
interface ActionNameProps {
disableActions?: boolean;
migrationRule: RuleMigration;
openMigrationRuleDetails: (migrationRule: RuleMigration) => void;
installMigrationRule: (migrationRule: RuleMigration, enable?: boolean) => void;
migrationRule: RuleMigrationRule;
openMigrationRuleDetails: (migrationRule: RuleMigrationRule) => void;
installMigrationRule: (migrationRule: RuleMigrationRule, enable?: boolean) => void;
}
const ActionName = ({
@ -82,8 +82,8 @@ const ActionName = ({
interface CreateActionsColumnProps {
disableActions?: boolean;
openMigrationRuleDetails: (migrationRule: RuleMigration) => void;
installMigrationRule: (migrationRule: RuleMigration, enable?: boolean) => void;
openMigrationRuleDetails: (migrationRule: RuleMigrationRule) => void;
installMigrationRule: (migrationRule: RuleMigrationRule, enable?: boolean) => void;
}
export const createActionsColumn = ({
@ -121,7 +121,7 @@ export const createActionsColumn = ({
}
/>
),
render: (_, rule: RuleMigration) => {
render: (_, rule: RuleMigrationRule) => {
return (
<ActionName
disableActions={disableActions}

View file

@ -9,7 +9,7 @@ import React from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, EuiIcon, EuiText } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { SiemMigrationStatus } from '../../../../../common/siem_migrations/constants';
import { type RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen';
import { type RuleMigrationRule } from '../../../../../common/siem_migrations/model/rule_migration.gen';
import * as i18n from './translations';
import { COLUMN_EMPTY_VALUE, type TableColumn } from './constants';
import { TableHeader } from './header';
@ -58,7 +58,7 @@ export const createAuthorColumn = (): TableColumn => {
}
/>
),
render: (_, rule: RuleMigration) => {
render: (_, rule: RuleMigrationRule) => {
return rule.status === SiemMigrationStatus.FAILED ? (
<>{COLUMN_EMPTY_VALUE}</>
) : (

View file

@ -6,8 +6,8 @@
*/
import type { EuiBasicTableColumn } from '@elastic/eui';
import type { RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen';
import type { RuleMigrationRule } from '../../../../../common/siem_migrations/model/rule_migration.gen';
export type TableColumn = EuiBasicTableColumn<RuleMigration>;
export type TableColumn = EuiBasicTableColumn<RuleMigrationRule>;
export const COLUMN_EMPTY_VALUE = '-';

View file

@ -10,7 +10,7 @@ import { EuiHorizontalRule, EuiLoadingSpinner, EuiText } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import type { RelatedIntegration } from '../../../../../common/api/detection_engine';
import { IntegrationsPopover } from '../../../../detections/components/rules/related_integrations/integrations_popover';
import type { RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen';
import type { RuleMigrationRule } from '../../../../../common/siem_migrations/model/rule_migration.gen';
import * as i18n from './translations';
import type { TableColumn } from './constants';
import { TableHeader } from './header';
@ -45,7 +45,7 @@ export const createIntegrationsColumn = ({
}
/>
),
render: (_, rule: RuleMigration) => {
render: (_, rule: RuleMigrationRule) => {
const migrationRuleData = getMigrationRuleData(rule.id);
if (migrationRuleData?.isIntegrationsLoading) {
return <EuiLoadingSpinner />;

View file

@ -8,13 +8,13 @@
import React from 'react';
import { EuiLink, EuiText } from '@elastic/eui';
import { SiemMigrationStatus } from '../../../../../common/siem_migrations/constants';
import { type RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen';
import { type RuleMigrationRule } from '../../../../../common/siem_migrations/model/rule_migration.gen';
import * as i18n from './translations';
import type { TableColumn } from './constants';
interface NameProps {
rule: RuleMigration;
openMigrationRuleDetails: (rule: RuleMigration) => void;
rule: RuleMigrationRule;
openMigrationRuleDetails: (rule: RuleMigrationRule) => void;
}
const Name = ({ rule, openMigrationRuleDetails }: NameProps) => {
@ -40,12 +40,12 @@ const Name = ({ rule, openMigrationRuleDetails }: NameProps) => {
export const createNameColumn = ({
openMigrationRuleDetails,
}: {
openMigrationRuleDetails: (rule: RuleMigration) => void;
openMigrationRuleDetails: (rule: RuleMigrationRule) => void;
}): TableColumn => {
return {
field: 'elastic_rule.title',
name: i18n.COLUMN_NAME,
render: (_, rule: RuleMigration) => (
render: (_, rule: RuleMigrationRule) => (
<Name rule={rule} openMigrationRuleDetails={openMigrationRuleDetails} />
),
sortable: true,

View file

@ -7,7 +7,7 @@
import React from 'react';
import { EuiText } from '@elastic/eui';
import { type RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen';
import { type RuleMigrationRule } from '../../../../../common/siem_migrations/model/rule_migration.gen';
import { SiemMigrationStatus } from '../../../../../common/siem_migrations/constants';
import * as i18n from './translations';
import { COLUMN_EMPTY_VALUE, type TableColumn } from './constants';
@ -16,7 +16,7 @@ export const createRiskScoreColumn = (): TableColumn => {
return {
field: 'elastic_rule.risk_score',
name: i18n.COLUMN_RISK_SCORE,
render: (riskScore, rule: RuleMigration) => (
render: (riskScore, rule: RuleMigrationRule) => (
<EuiText data-test-subj="riskScore" size="s">
{rule.status === SiemMigrationStatus.FAILED ? COLUMN_EMPTY_VALUE : riskScore}
</EuiText>

View file

@ -9,7 +9,7 @@ import React from 'react';
import type { Severity } from '@kbn/securitysolution-io-ts-alerting-types';
import { EuiHorizontalRule, EuiText } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { type RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen';
import type { RuleMigrationRule } from '../../../../../common/siem_migrations/model/rule_migration.gen';
import { SiemMigrationStatus } from '../../../../../common/siem_migrations/constants';
import { SeverityBadge } from '../../../../common/components/severity_badge';
import { COLUMN_EMPTY_VALUE, type TableColumn } from './constants';
@ -40,7 +40,7 @@ export const createSeverityColumn = (): TableColumn => {
}
/>
),
render: (value: Severity, rule: RuleMigration) =>
render: (value: Severity, rule: RuleMigrationRule) =>
rule.status === SiemMigrationStatus.FAILED ? (
<>{COLUMN_EMPTY_VALUE}</>
) : (

View file

@ -9,7 +9,7 @@ import React from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiHorizontalRule, EuiText } from '@elastic/eui';
import { RuleTranslationResult } from '../../../../../common/siem_migrations/constants';
import type { RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen';
import type { RuleMigrationRule } from '../../../../../common/siem_migrations/model/rule_migration.gen';
import * as i18n from './translations';
import type { TableColumn } from './constants';
import { StatusBadge } from '../status_badge';
@ -56,7 +56,12 @@ export const createStatusColumn = (): TableColumn => {
}
/>
),
render: (_, rule: RuleMigration) => <StatusBadge migrationRule={rule} />,
render: (_, rule: RuleMigrationRule) => (
<StatusBadge
data-test-subj={`translationStatus-${rule.translation_result ?? rule.status}`}
migrationRule={rule}
/>
),
sortable: true,
truncateText: true,
width: '15%',

View file

@ -7,7 +7,7 @@
import React from 'react';
import { FormattedRelativePreferenceDate } from '../../../../common/components/formatted_date';
import type { RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen';
import type { RuleMigrationRule } from '../../../../../common/siem_migrations/model/rule_migration.gen';
import * as i18n from './translations';
import type { TableColumn } from './constants';
@ -15,7 +15,7 @@ export const createUpdatedColumn = (): TableColumn => {
return {
field: 'updated_at',
name: i18n.COLUMN_UPDATED,
render: (value: RuleMigration['updated_at']) => (
render: (value: RuleMigrationRule['updated_at']) => (
<FormattedRelativePreferenceDate value={value} dateFormat="M/D/YY" />
),
sortable: true,

View file

@ -14,10 +14,8 @@ import {
convertTranslationResultIntoText,
useResultVisColors,
} from '../../utils/translation_results';
import {
RuleMigrationStatusEnum,
type RuleMigration,
} from '../../../../../common/siem_migrations/model/rule_migration.gen';
import type { RuleMigrationRule } from '../../../../../common/siem_migrations/model/rule_migration.gen';
import { RuleMigrationStatusEnum } from '../../../../../common/siem_migrations/model/rule_migration.gen';
import * as i18n from './translations';
const statusTextWrapperClassName = css`
@ -26,7 +24,7 @@ const statusTextWrapperClassName = css`
`;
interface StatusBadgeProps {
migrationRule: RuleMigration;
migrationRule: RuleMigrationRule;
'data-test-subj'?: string;
}
@ -41,7 +39,9 @@ export const StatusBadge: React.FC<StatusBadgeProps> = React.memo(
<EuiFlexItem grow={false}>
<EuiIcon type="check" color={colors[RuleTranslationResult.FULL]} />
</EuiFlexItem>
<EuiFlexItem grow={false}>{i18n.RULE_STATUS_INSTALLED}</EuiFlexItem>
<EuiFlexItem data-test-subj={dataTestSubj} grow={false}>
{i18n.RULE_STATUS_INSTALLED}
</EuiFlexItem>
</EuiFlexGroup>
</EuiToolTip>
);
@ -58,7 +58,9 @@ export const StatusBadge: React.FC<StatusBadgeProps> = React.memo(
<EuiFlexItem grow={false}>
<EuiIcon type="warningFilled" color="danger" />
</EuiFlexItem>
<EuiFlexItem grow={false}>{i18n.RULE_STATUS_FAILED}</EuiFlexItem>
<EuiFlexItem data-test-subj={dataTestSubj} grow={false}>
{i18n.RULE_STATUS_FAILED}
</EuiFlexItem>
</EuiFlexGroup>
</EuiToolTip>
);

View file

@ -9,24 +9,24 @@ import type { ReactNode } from 'react';
import React, { useCallback, useState, useMemo } from 'react';
import type { EuiTabbedContentTab } from '@elastic/eui';
import type { RuleResponse } from '../../../../common/api/detection_engine';
import type { RuleMigration } from '../../../../common/siem_migrations/model/rule_migration.gen';
import type { RuleMigrationRule } from '../../../../common/siem_migrations/model/rule_migration.gen';
import { MigrationRuleDetailsFlyout } from '../components/rule_details_flyout';
interface UseMigrationRuleDetailsFlyoutParams {
isLoading?: boolean;
getMigrationRuleData: (ruleId: string) =>
| {
ruleMigration?: RuleMigration;
migrationRule?: RuleMigrationRule;
matchedPrebuiltRule?: RuleResponse;
}
| undefined;
ruleActionsFactory: (ruleMigration: RuleMigration, closeRulePreview: () => void) => ReactNode;
extraTabsFactory?: (ruleMigration: RuleMigration) => EuiTabbedContentTab[];
ruleActionsFactory: (migrationRule: RuleMigrationRule, closeRulePreview: () => void) => ReactNode;
extraTabsFactory?: (migrationRule: RuleMigrationRule) => EuiTabbedContentTab[];
}
interface UseMigrationRuleDetailsFlyoutResult {
migrationRuleDetailsFlyout: ReactNode;
openMigrationRuleDetails: (rule: RuleMigration) => void;
openMigrationRuleDetails: (rule: RuleMigrationRule) => void;
closeMigrationRuleDetails: () => void;
}
@ -44,29 +44,29 @@ export function useMigrationRuleDetailsFlyout({
}
}, [getMigrationRuleData, migrationRuleId]);
const openMigrationRuleDetails = useCallback((rule: RuleMigration) => {
const openMigrationRuleDetails = useCallback((rule: RuleMigrationRule) => {
setMigrationRuleId(rule.id);
}, []);
const closeMigrationRuleDetails = useCallback(() => setMigrationRuleId(undefined), []);
const ruleActions = useMemo(
() =>
migrationRuleData?.ruleMigration &&
ruleActionsFactory(migrationRuleData.ruleMigration, closeMigrationRuleDetails),
[migrationRuleData?.ruleMigration, ruleActionsFactory, closeMigrationRuleDetails]
migrationRuleData?.migrationRule &&
ruleActionsFactory(migrationRuleData.migrationRule, closeMigrationRuleDetails),
[migrationRuleData?.migrationRule, ruleActionsFactory, closeMigrationRuleDetails]
);
const extraTabs = useMemo(
() =>
migrationRuleData?.ruleMigration && extraTabsFactory
? extraTabsFactory(migrationRuleData.ruleMigration)
migrationRuleData?.migrationRule && extraTabsFactory
? extraTabsFactory(migrationRuleData.migrationRule)
: [],
[extraTabsFactory, migrationRuleData?.ruleMigration]
[extraTabsFactory, migrationRuleData?.migrationRule]
);
return {
migrationRuleDetailsFlyout: migrationRuleData?.ruleMigration && (
migrationRuleDetailsFlyout: migrationRuleData?.migrationRule && (
<MigrationRuleDetailsFlyout
ruleMigration={migrationRuleData.ruleMigration}
migrationRule={migrationRuleData.migrationRule}
matchedPrebuiltRule={migrationRuleData.matchedPrebuiltRule}
size="l"
closeFlyout={closeMigrationRuleDetails}

View file

@ -7,7 +7,7 @@
import { useMemo } from 'react';
import type { RelatedIntegration } from '../../../../common/api/detection_engine';
import type { RuleMigration } from '../../../../common/siem_migrations/model/rule_migration.gen';
import type { RuleMigrationRule } from '../../../../common/siem_migrations/model/rule_migration.gen';
import type { TableColumn } from '../components/rules_table_columns';
import {
createActionsColumn,
@ -27,8 +27,8 @@ export const useMigrationRulesTableColumns = ({
getMigrationRuleData,
}: {
disableActions?: boolean;
openMigrationRuleDetails: (rule: RuleMigration) => void;
installMigrationRule: (migrationRule: RuleMigration, enable?: boolean) => void;
openMigrationRuleDetails: (rule: RuleMigrationRule) => void;
installMigrationRule: (migrationRule: RuleMigrationRule, enable?: boolean) => void;
getMigrationRuleData: (
ruleId: string
) => { relatedIntegrations?: RelatedIntegration[]; isIntegrationsLoading?: boolean } | undefined;

View file

@ -12,7 +12,7 @@ import type { RuleMigrationFilters } from '../../../../common/siem_migrations/ty
import { SIEM_RULE_MIGRATION_PATH } from '../../../../common/siem_migrations/constants';
import { useAppToasts } from '../../../common/hooks/use_app_toasts';
import * as i18n from './translations';
import { getRuleMigrations } from '../api';
import { getMigrationRules } from '../api';
import { DEFAULT_QUERY_OPTIONS } from './constants';
export const useGetMigrationRules = (params: {
@ -33,9 +33,9 @@ export const useGetMigrationRules = (params: {
return useQuery(
['GET', SPECIFIC_MIGRATION_PATH, params],
async ({ signal }) => {
const response = await getRuleMigrations({ signal, ...params });
const response = await getMigrationRules({ signal, ...params });
return { ruleMigrations: response.data, total: response.total };
return { migrationRules: response.data, total: response.total };
},
{
...DEFAULT_QUERY_OPTIONS,

View file

@ -7,8 +7,8 @@
import { useMutation } from '@tanstack/react-query';
import { useCallback } from 'react';
import type { RuleMigrationRule } from '../../../../common/siem_migrations/model/rule_migration.gen';
import { useKibana } from '../../../common/lib/kibana/kibana_react';
import type { RuleMigration } from '../../../../common/siem_migrations/model/rule_migration.gen';
import { SIEM_RULE_MIGRATION_INSTALL_PATH } from '../../../../common/siem_migrations/constants';
import type { InstallMigrationRulesResponse } from '../../../../common/siem_migrations/model/api/rules/rule_migration.gen';
import { useAppToasts } from '../../../common/hooks/use_app_toasts';
@ -20,7 +20,7 @@ import { installMigrationRules } from '../api';
export const INSTALL_MIGRATION_RULE_MUTATION_KEY = ['POST', SIEM_RULE_MIGRATION_INSTALL_PATH];
interface InstallMigrationRuleParams {
ruleMigration: RuleMigration;
migrationRule: RuleMigrationRule;
enabled?: boolean;
}
@ -29,8 +29,8 @@ export const useInstallMigrationRule = (migrationId: string) => {
const { telemetry } = useKibana().services.siemMigrations.rules;
const reportTelemetry = useCallback(
({ ruleMigration, enabled = false }: InstallMigrationRuleParams, error?: Error) => {
telemetry.reportTranslatedRuleInstall({ ruleMigration, enabled, error });
({ migrationRule, enabled = false }: InstallMigrationRuleParams, error?: Error) => {
telemetry.reportTranslatedRuleInstall({ migrationRule, enabled, error });
},
[telemetry]
);
@ -39,7 +39,7 @@ export const useInstallMigrationRule = (migrationId: string) => {
const invalidateGetMigrationTranslationStats = useInvalidateGetMigrationTranslationStats();
return useMutation<InstallMigrationRulesResponse, Error, InstallMigrationRuleParams>(
({ ruleMigration, enabled }) =>
({ migrationRule: ruleMigration, enabled }) =>
installMigrationRules({ migrationId, ids: [ruleMigration.id], enabled }),
{
mutationKey: INSTALL_MIGRATION_RULE_MUTATION_KEY,

View file

@ -8,10 +8,10 @@
import { useMutation } from '@tanstack/react-query';
import { useCallback } from 'react';
import type {
RuleMigration,
UpdateRuleMigrationData,
RuleMigrationRule,
UpdateRuleMigrationRule,
} from '../../../../common/siem_migrations/model/rule_migration.gen';
import { SIEM_RULE_MIGRATION_PATH } from '../../../../common/siem_migrations/constants';
import { SIEM_RULE_MIGRATION_RULES_PATH } from '../../../../common/siem_migrations/constants';
import type { UpdateRuleMigrationResponse } from '../../../../common/siem_migrations/model/api/rules/rule_migration.gen';
import { useAppToasts } from '../../../common/hooks/use_app_toasts';
import { useKibana } from '../../../common/lib/kibana/kibana_react';
@ -20,25 +20,25 @@ import { useInvalidateGetMigrationRules } from './use_get_migration_rules';
import { useInvalidateGetMigrationTranslationStats } from './use_get_migration_translation_stats';
import { updateMigrationRules } from '../api';
export const UPDATE_MIGRATION_RULE_MUTATION_KEY = ['PUT', SIEM_RULE_MIGRATION_PATH];
export const UPDATE_MIGRATION_RULE_MUTATION_KEY = ['PUT', SIEM_RULE_MIGRATION_RULES_PATH];
export const useUpdateMigrationRule = (ruleMigration: RuleMigration) => {
export const useUpdateMigrationRule = (migrationRule: RuleMigrationRule) => {
const { addError } = useAppToasts();
const { telemetry } = useKibana().services.siemMigrations.rules;
const migrationId = ruleMigration.migration_id;
const migrationId = migrationRule.migration_id;
const reportTelemetry = useCallback(
(error?: Error) => {
telemetry.reportTranslatedRuleUpdate({ ruleMigration, error });
telemetry.reportTranslatedRuleUpdate({ migrationRule, error });
},
[telemetry, ruleMigration]
[telemetry, migrationRule]
);
const invalidateGetRuleMigrations = useInvalidateGetMigrationRules();
const invalidateGetMigrationTranslationStats = useInvalidateGetMigrationTranslationStats();
return useMutation<UpdateRuleMigrationResponse, Error, UpdateRuleMigrationData>(
return useMutation<UpdateRuleMigrationResponse, Error, UpdateRuleMigrationRule>(
(ruleUpdateData) => updateMigrationRules({ migrationId, rulesToUpdate: [ruleUpdateData] }),
{
mutationKey: UPDATE_MIGRATION_RULE_MUTATION_KEY,

View file

@ -133,7 +133,7 @@ const mockUseGetMigrationRules: typeof useGetMigrationRulesModule.useGetMigratio
const { data, total } = mockedMigrationResultsObj[migrationId];
return {
data: {
ruleMigrations: data,
migrationRules: data,
total,
},
isLoading: false,

View file

@ -7,8 +7,8 @@
import { useCallback, useReducer } from 'react';
import { i18n } from '@kbn/i18n';
import type { CreateRuleMigrationRulesRequestBody } from '../../../../../common/siem_migrations/model/api/rules/rule_migration.gen';
import type { RuleMigrationTaskStats } from '../../../../../common/siem_migrations/model/rule_migration.gen';
import type { CreateRuleMigrationRequestBody } from '../../../../../common/siem_migrations/model/api/rules/rule_migration.gen';
import { useKibana } from '../../../../common/lib/kibana/kibana_react';
import { reducer, initialState } from './common/api_request_reducer';
@ -26,7 +26,7 @@ export const RULES_DATA_INPUT_CREATE_MIGRATION_ERROR = i18n.translate(
{ defaultMessage: 'Failed to upload rules file' }
);
export type CreateMigration = (data: CreateRuleMigrationRequestBody) => void;
export type CreateMigration = (data: CreateRuleMigrationRulesRequestBody) => void;
export type OnSuccess = (migrationStats: RuleMigrationTaskStats) => void;
export const useCreateMigration = (onSuccess: OnSuccess) => {

View file

@ -23,8 +23,8 @@ import {
getRuleMigrationsStatsAll,
getMissingResources,
getIntegrations,
addRulesToMigration,
} from '../api';
import type { CreateRuleMigrationRequestBody } from '../../../../common/siem_migrations/model/api/rules/rule_migration.gen';
import { createTelemetryServiceMock } from '../../../common/lib/telemetry/telemetry_service.mock';
import {
SiemMigrationRetryFilter,
@ -37,6 +37,7 @@ import {
REQUEST_POLLING_INTERVAL_SECONDS,
SiemRulesMigrationsService,
} from './rule_migrations_service';
import type { CreateRuleMigrationRulesRequestBody } from '../../../../common/siem_migrations/model/api/rules/rule_migration.gen';
// --- Mocks for external modules ---
@ -48,6 +49,7 @@ jest.mock('../api', () => ({
getRuleMigrationsStatsAll: jest.fn(),
getMissingResources: jest.fn(),
getIntegrations: jest.fn(),
addRulesToMigration: jest.fn(),
}));
jest.mock('./capabilities', () => ({
@ -132,37 +134,42 @@ describe('SiemRulesMigrationsService', () => {
});
it('should create migration with a single batch', async () => {
const body = [{ id: 'rule1' }] as CreateRuleMigrationRequestBody;
const body = [{ id: 'rule1' }] as CreateRuleMigrationRulesRequestBody;
(createRuleMigration as jest.Mock).mockResolvedValue({ migration_id: 'mig-1' });
(addRulesToMigration as jest.Mock).mockResolvedValue(undefined);
const migrationId = await service.createRuleMigration(body);
expect(createRuleMigration).toHaveBeenCalledTimes(1);
expect(createRuleMigration).toHaveBeenCalledWith({ migrationId: undefined, body });
expect(createRuleMigration).toHaveBeenCalledWith({});
expect(addRulesToMigration).toHaveBeenCalledWith({ migrationId: 'mig-1', body });
expect(migrationId).toBe('mig-1');
});
it('should create migration in batches if body length exceeds the batch size', async () => {
// Create an array of 51 items (the service batches in chunks of 50)
const body = new Array(51).fill({ rule: 'rule' });
(createRuleMigration as jest.Mock)
.mockResolvedValueOnce({ migration_id: 'mig-1' })
.mockResolvedValueOnce({ migration_id: 'mig-2' });
(createRuleMigration as jest.Mock).mockResolvedValueOnce({ migration_id: 'mig-1' });
(addRulesToMigration as jest.Mock).mockResolvedValue(undefined);
const migrationId = await service.createRuleMigration(body);
expect(createRuleMigration).toHaveBeenCalledTimes(2);
expect(createRuleMigration).toHaveBeenCalledTimes(1);
expect(addRulesToMigration).toHaveBeenCalledTimes(2);
// First call: first 50 items, migrationId undefined
expect((createRuleMigration as jest.Mock).mock.calls[0][0]).toEqual({
migrationId: undefined,
expect(createRuleMigration).toHaveBeenNthCalledWith(1, {});
expect(addRulesToMigration).toHaveBeenNthCalledWith(1, {
migrationId: 'mig-1',
body: body.slice(0, 50),
});
// Second call: remaining 1 item, migrationId passed from previous batch
expect((createRuleMigration as jest.Mock).mock.calls[1][0]).toEqual({
expect(addRulesToMigration).toHaveBeenNthCalledWith(2, {
migrationId: 'mig-1',
body: body.slice(50, 51),
});
expect(migrationId).toBe('mig-2');
expect(migrationId).toBe('mig-1');
});
});

View file

@ -19,7 +19,7 @@ import type {
RuleMigrationTaskStats,
} from '../../../../common/siem_migrations/model/rule_migration.gen';
import type {
CreateRuleMigrationRequestBody,
CreateRuleMigrationRulesRequestBody,
GetRuleMigrationStatsResponse,
StartRuleMigrationResponse,
UpsertRuleMigrationResourcesRequestBody,
@ -39,6 +39,7 @@ import {
getMissingResources,
upsertMigrationResources,
getIntegrations,
addRulesToMigration,
} from '../api';
import {
getMissingCapabilities,
@ -119,22 +120,36 @@ export class SiemRulesMigrationsService {
});
}
public async createRuleMigration(body: CreateRuleMigrationRequestBody): Promise<string> {
const rulesCount = body.length;
public async addRulesToMigration(
migrationId: string,
rules: CreateRuleMigrationRulesRequestBody
) {
const rulesCount = rules.length;
if (rulesCount === 0) {
throw new Error(i18n.EMPTY_RULES_ERROR);
}
// Batching creation to avoid hitting the max payload size limit of the API
for (let i = 0; i < rulesCount; i += CREATE_MIGRATION_BODY_BATCH_SIZE) {
const rulesBatch = rules.slice(i, i + CREATE_MIGRATION_BODY_BATCH_SIZE);
await addRulesToMigration({ migrationId, body: rulesBatch });
}
}
public async createRuleMigration(data: CreateRuleMigrationRulesRequestBody): Promise<string> {
const rulesCount = data.length;
if (rulesCount === 0) {
throw new Error(i18n.EMPTY_RULES_ERROR);
}
try {
let migrationId: string | undefined;
// Batching creation to avoid hitting the max payload size limit of the API
for (let i = 0; i < rulesCount; i += CREATE_MIGRATION_BODY_BATCH_SIZE) {
const bodyBatch = body.slice(i, i + CREATE_MIGRATION_BODY_BATCH_SIZE);
const response = await createRuleMigration({ migrationId, body: bodyBatch });
migrationId = response.migration_id;
}
// create the migration
const { migration_id: migrationId } = await createRuleMigration({});
await this.addRulesToMigration(migrationId, data);
this.telemetry.reportSetupMigrationCreated({ migrationId, rulesCount });
return migrationId as string;
return migrationId;
} catch (error) {
this.telemetry.reportSetupMigrationCreated({ rulesCount, error });
throw error;

View file

@ -9,8 +9,8 @@ import type { ActionConnector } from '@kbn/triggers-actions-ui-plugin/public';
import { siemMigrationEventNames } from '../../../common/lib/telemetry/events/siem_migrations';
import type { SiemMigrationRetryFilter } from '../../../../common/siem_migrations/constants';
import type {
RuleMigration,
RuleMigrationResourceType,
RuleMigrationRule,
} from '../../../../common/siem_migrations/model/rule_migration.gen';
import type { TelemetryServiceStart } from '../../../common/lib/telemetry';
import type {
@ -125,36 +125,36 @@ export class SiemRulesMigrationsTelemetry {
// Translated rule actions
reportTranslatedRuleUpdate = (params: { ruleMigration: RuleMigration; error?: Error }) => {
const { ruleMigration, error } = params;
reportTranslatedRuleUpdate = (params: { migrationRule: RuleMigrationRule; error?: Error }) => {
const { migrationRule, error } = params;
this.telemetryService.reportEvent(SiemMigrationsEventTypes.TranslatedRuleUpdate, {
eventName: siemMigrationEventNames[SiemMigrationsEventTypes.TranslatedRuleUpdate],
migrationId: ruleMigration.migration_id,
ruleMigrationId: ruleMigration.id,
migrationId: migrationRule.migration_id,
ruleMigrationId: migrationRule.id,
...this.getBaseResultParams(error),
});
};
reportTranslatedRuleInstall = (params: {
ruleMigration: RuleMigration;
migrationRule: RuleMigrationRule;
enabled: boolean;
error?: Error;
}) => {
const { ruleMigration, enabled, error } = params;
const { migrationRule, enabled, error } = params;
const eventParams: ReportTranslatedRuleInstallActionParams = {
eventName: siemMigrationEventNames[SiemMigrationsEventTypes.TranslatedRuleInstall],
migrationId: ruleMigration.migration_id,
ruleMigrationId: ruleMigration.id,
migrationId: migrationRule.migration_id,
ruleMigrationId: migrationRule.id,
author: 'custom',
enabled,
...this.getBaseResultParams(error),
};
if (ruleMigration.elastic_rule?.prebuilt_rule_id) {
if (migrationRule.elastic_rule?.prebuilt_rule_id) {
eventParams.author = 'elastic';
eventParams.prebuiltRule = {
id: ruleMigration.elastic_rule.prebuilt_rule_id,
title: ruleMigration.elastic_rule.title,
id: migrationRule.elastic_rule.prebuilt_rule_id,
title: migrationRule.elastic_rule.title,
};
}

View file

@ -6,16 +6,9 @@
*/
import type { IKibanaResponse, Logger } from '@kbn/core/server';
import { buildRouteValidationWithZod } from '@kbn/zod-helpers';
import { SIEM_RULE_MIGRATION_CREATE_PATH } from '../../../../../common/siem_migrations/constants';
import {
CreateRuleMigrationRequestBody,
CreateRuleMigrationRequestParams,
type CreateRuleMigrationResponse,
} from '../../../../../common/siem_migrations/model/api/rules/rule_migration.gen';
import { ResourceIdentifier } from '../../../../../common/siem_migrations/rules/resources';
import { SIEM_RULE_MIGRATIONS_PATH } from '../../../../../common/siem_migrations/constants';
import { type CreateRuleMigrationResponse } from '../../../../../common/siem_migrations/model/api/rules/rule_migration.gen';
import type { SecuritySolutionPluginRouter } from '../../../../types';
import type { CreateRuleMigrationInput } from '../data/rule_migrations_data_rules_client';
import { SiemMigrationAuditLogger } from './util/audit';
import { authz } from './util/authz';
import { withLicense } from './util/with_license';
@ -25,67 +18,31 @@ export const registerSiemRuleMigrationsCreateRoute = (
logger: Logger
) => {
router.versioned
.post({
path: SIEM_RULE_MIGRATION_CREATE_PATH,
.put({
path: SIEM_RULE_MIGRATIONS_PATH,
access: 'internal',
security: { authz },
})
.addVersion(
{
version: '1',
validate: {
request: {
body: buildRouteValidationWithZod(CreateRuleMigrationRequestBody),
params: buildRouteValidationWithZod(CreateRuleMigrationRequestParams),
},
},
// no request body or params to validate
validate: false,
},
withLicense(
async (context, req, res): Promise<IKibanaResponse<CreateRuleMigrationResponse>> => {
const originalRules = req.body;
const siemMigrationAuditLogger = new SiemMigrationAuditLogger(context.securitySolution);
const providedMigrationId = req.params?.migration_id;
try {
const [firstOriginalRule] = originalRules;
if (!firstOriginalRule) {
return res.noContent();
}
const ctx = await context.resolve(['securitySolution']);
const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient();
await siemMigrationAuditLogger.logCreateMigration({ migrationId: providedMigrationId });
await siemMigrationAuditLogger.logCreateMigration();
let migrationId: string;
if (!providedMigrationId) {
/** if new migration */
migrationId = await ruleMigrationsClient.data.migrations.create();
} else {
/** if updating existing migration */
migrationId = providedMigrationId;
}
const ruleMigrations = originalRules.map<CreateRuleMigrationInput>((originalRule) => ({
migration_id: migrationId,
original_rule: originalRule,
}));
await ruleMigrationsClient.data.rules.create(ruleMigrations);
// Create identified resource documents without content to keep track of them
const resourceIdentifier = new ResourceIdentifier(firstOriginalRule.vendor);
const resources = resourceIdentifier
.fromOriginalRules(originalRules)
.map((resource) => ({ ...resource, migration_id: migrationId }));
if (resources.length > 0) {
await ruleMigrationsClient.data.resources.create(resources);
}
const migrationId = await ruleMigrationsClient.data.migrations.create();
return res.ok({ body: { migration_id: migrationId } });
} catch (error) {
logger.error(error);
await siemMigrationAuditLogger.logCreateMigration({
migrationId: providedMigrationId,
error,
});
return res.badRequest({ body: error.message });

View file

@ -5,26 +5,22 @@
* 2.0.
*/
import type { IKibanaResponse, Logger } from '@kbn/core/server';
import type { Logger } from '@kbn/core/server';
import { buildRouteValidationWithZod } from '@kbn/zod-helpers';
import { SIEM_RULE_MIGRATION_PATH } from '../../../../../common/siem_migrations/constants';
import {
UpdateRuleMigrationRequestBody,
UpdateRuleMigrationRequestParams,
type UpdateRuleMigrationResponse,
} from '../../../../../common/siem_migrations/model/api/rules/rule_migration.gen';
import { GetRuleMigrationRequestParams } from '../../../../../common/siem_migrations/model/api/rules/rule_migration.gen';
import type { SecuritySolutionPluginRouter } from '../../../../types';
import { authz } from './util/authz';
import { SiemMigrationAuditLogger } from './util/audit';
import { transformToInternalUpdateRuleMigrationData } from './util/update_rules';
import { authz } from './util/authz';
import { withLicense } from './util/with_license';
import { withExistingMigration } from './util/with_existing_migration_id';
export const registerSiemRuleMigrationsUpdateRoute = (
export const registerSiemRuleMigrationsDeleteRoute = (
router: SecuritySolutionPluginRouter,
logger: Logger
) => {
router.versioned
.put({
.delete({
path: SIEM_RULE_MIGRATION_PATH,
access: 'internal',
security: { authz },
@ -34,40 +30,40 @@ export const registerSiemRuleMigrationsUpdateRoute = (
version: '1',
validate: {
request: {
params: buildRouteValidationWithZod(UpdateRuleMigrationRequestParams),
body: buildRouteValidationWithZod(UpdateRuleMigrationRequestBody),
params: buildRouteValidationWithZod(GetRuleMigrationRequestParams),
},
},
},
withLicense(
async (context, req, res): Promise<IKibanaResponse<UpdateRuleMigrationResponse>> => {
const { migration_id: migrationId } = req.params;
const rulesToUpdate = req.body;
if (rulesToUpdate.length === 0) {
return res.noContent();
}
const ids = rulesToUpdate.map((rule) => rule.id);
withExistingMigration(async (context, req, res) => {
const siemMigrationAuditLogger = new SiemMigrationAuditLogger(context.securitySolution);
const { migration_id: migrationId } = req.params;
try {
const ctx = await context.resolve(['securitySolution']);
const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient();
await siemMigrationAuditLogger.logDeleteMigration({ migrationId });
await siemMigrationAuditLogger.logUpdateRules({ migrationId, ids });
if (ruleMigrationsClient.task.isMigrationRunning(migrationId)) {
return res.conflict({
body: 'A running migration cannot be deleted. Please stop the migration first and try again',
});
}
const transformedRuleToUpdate = rulesToUpdate.map(
transformToInternalUpdateRuleMigrationData
);
await ruleMigrationsClient.data.rules.update(transformedRuleToUpdate);
await ruleMigrationsClient.data.deleteMigration(migrationId);
return res.ok({ body: { updated: true } });
return res.ok();
} catch (error) {
logger.error(error);
await siemMigrationAuditLogger.logUpdateRules({ migrationId, ids, error });
return res.badRequest({ body: error.message });
}
await siemMigrationAuditLogger.logDeleteMigration({
migrationId,
error,
});
return res.badRequest({
body: error.message,
});
}
})
)
);
};

View file

@ -8,16 +8,13 @@
import type { IKibanaResponse, Logger } from '@kbn/core/server';
import { buildRouteValidationWithZod } from '@kbn/zod-helpers';
import { SIEM_RULE_MIGRATION_PATH } from '../../../../../common/siem_migrations/constants';
import {
GetRuleMigrationRequestParams,
GetRuleMigrationRequestQuery,
type GetRuleMigrationResponse,
} from '../../../../../common/siem_migrations/model/api/rules/rule_migration.gen';
import type { GetRuleMigrationResponse } from '../../../../../common/siem_migrations/model/api/rules/rule_migration.gen';
import { GetRuleMigrationRequestParams } from '../../../../../common/siem_migrations/model/api/rules/rule_migration.gen';
import type { SecuritySolutionPluginRouter } from '../../../../types';
import type { RuleMigrationGetOptions } from '../data/rule_migrations_data_rules_client';
import { SiemMigrationAuditLogger } from './util/audit';
import { authz } from './util/authz';
import { withLicense } from './util/with_license';
import { MIGRATION_ID_NOT_FOUND } from './util/with_existing_migration_id';
export const registerSiemRuleMigrationsGetRoute = (
router: SecuritySolutionPluginRouter,
@ -35,42 +32,37 @@ export const registerSiemRuleMigrationsGetRoute = (
validate: {
request: {
params: buildRouteValidationWithZod(GetRuleMigrationRequestParams),
query: buildRouteValidationWithZod(GetRuleMigrationRequestQuery),
},
},
},
withLicense(async (context, req, res): Promise<IKibanaResponse<GetRuleMigrationResponse>> => {
const { migration_id: migrationId } = req.params;
const siemMigrationAuditLogger = new SiemMigrationAuditLogger(context.securitySolution);
const { migration_id: migrationId } = req.params;
try {
const ctx = await context.resolve(['securitySolution']);
const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient();
const { page, per_page: size } = req.query;
const options: RuleMigrationGetOptions = {
filters: {
searchTerm: req.query.search_term,
ids: req.query.ids,
prebuilt: req.query.is_prebuilt,
installed: req.query.is_installed,
fullyTranslated: req.query.is_fully_translated,
partiallyTranslated: req.query.is_partially_translated,
untranslatable: req.query.is_untranslatable,
failed: req.query.is_failed,
},
sort: { sortField: req.query.sort_field, sortDirection: req.query.sort_direction },
size,
from: page && size ? page * size : 0,
};
const result = await ruleMigrationsClient.data.rules.get(migrationId, options);
await siemMigrationAuditLogger.logGetMigration({ migrationId });
return res.ok({ body: result });
const storedMigration = await ruleMigrationsClient.data.migrations.get({
id: migrationId,
});
if (!storedMigration) {
return res.notFound({
body: MIGRATION_ID_NOT_FOUND(migrationId),
});
}
return res.ok({
body: storedMigration,
});
} catch (error) {
logger.error(error);
await siemMigrationAuditLogger.logGetMigration({ migrationId, error });
await siemMigrationAuditLogger.logGetMigration({
migrationId,
error,
});
return res.badRequest({ body: error.message });
}
})

View file

@ -9,7 +9,7 @@ import type { Logger } from '@kbn/core/server';
import type { ConfigType } from '../../../../config';
import type { SecuritySolutionPluginRouter } from '../../../../types';
import { registerSiemRuleMigrationsCreateRoute } from './create';
import { registerSiemRuleMigrationsUpdateRoute } from './update';
import { registerSiemRuleMigrationsUpdateRulesRoute } from './rules/update';
import { registerSiemRuleMigrationsGetRoute } from './get';
import { registerSiemRuleMigrationsStartRoute } from './start';
import { registerSiemRuleMigrationsStatsRoute } from './stats';
@ -24,27 +24,45 @@ import { registerSiemRuleMigrationsPrebuiltRulesRoute } from './get_prebuilt_rul
import { registerSiemRuleMigrationsIntegrationsRoute } from './get_integrations';
import { registerSiemRuleMigrationsGetMissingPrivilegesRoute } from './privileges/get_missing_privileges';
import { registerSiemRuleMigrationsEvaluateRoute } from './evaluation/evaluate';
import { registerSiemRuleMigrationsCreateRulesRoute } from './rules/create';
import { registerSiemRuleMigrationsGetRulesRoute } from './rules/get';
import { registerSiemRuleMigrationsDeleteRoute } from './delete';
export const registerSiemRuleMigrationsRoutes = (
router: SecuritySolutionPluginRouter,
config: ConfigType,
logger: Logger
) => {
/** Rules Migrations */
registerSiemRuleMigrationsCreateRoute(router, logger);
registerSiemRuleMigrationsUpdateRoute(router, logger);
registerSiemRuleMigrationsGetRoute(router, logger);
registerSiemRuleMigrationsDeleteRoute(router, logger);
/** *******/
/** Rules */
registerSiemRuleMigrationsCreateRulesRoute(router, logger);
registerSiemRuleMigrationsGetRulesRoute(router, logger);
registerSiemRuleMigrationsUpdateRulesRoute(router, logger);
/** *******/
/** Tasks **/
registerSiemRuleMigrationsStatsAllRoute(router, logger);
registerSiemRuleMigrationsPrebuiltRulesRoute(router, logger);
registerSiemRuleMigrationsGetRoute(router, logger);
registerSiemRuleMigrationsStartRoute(router, logger);
registerSiemRuleMigrationsStatsRoute(router, logger);
registerSiemRuleMigrationsTranslationStatsRoute(router, logger);
registerSiemRuleMigrationsStopRoute(router, logger);
/** *******/
registerSiemRuleMigrationsInstallRoute(router, logger);
registerSiemRuleMigrationsIntegrationsRoute(router, logger);
/** Resources */
registerSiemRuleMigrationsResourceUpsertRoute(router, logger);
registerSiemRuleMigrationsResourceGetRoute(router, logger);
registerSiemRuleMigrationsResourceGetMissingRoute(router, logger);
/** *******/
registerSiemRuleMigrationsGetMissingPrivilegesRoute(router, logger);

View file

@ -0,0 +1,96 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { IKibanaResponse, Logger } from '@kbn/core/server';
import { buildRouteValidationWithZod } from '@kbn/zod-helpers';
import type { RuleMigrationRule } from '../../../../../../common/siem_migrations/model/rule_migration.gen';
import { SIEM_RULE_MIGRATION_RULES_PATH } from '../../../../../../common/siem_migrations/constants';
import {
CreateRuleMigrationRulesRequestBody,
CreateRuleMigrationRulesRequestParams,
} from '../../../../../../common/siem_migrations/model/api/rules/rule_migration.gen';
import { ResourceIdentifier } from '../../../../../../common/siem_migrations/rules/resources';
import type { SecuritySolutionPluginRouter } from '../../../../../types';
import type { AddRuleMigrationRulesInput } from '../../data/rule_migrations_data_rules_client';
import { SiemMigrationAuditLogger } from '../util/audit';
import { authz } from '../util/authz';
import { withExistingMigration } from '../util/with_existing_migration_id';
import { withLicense } from '../util/with_license';
export const registerSiemRuleMigrationsCreateRulesRoute = (
router: SecuritySolutionPluginRouter,
logger: Logger
) => {
router.versioned
.post({
path: SIEM_RULE_MIGRATION_RULES_PATH,
access: 'internal',
security: { authz },
})
.addVersion(
{
version: '1',
validate: {
request: {
body: buildRouteValidationWithZod(CreateRuleMigrationRulesRequestBody),
params: buildRouteValidationWithZod(CreateRuleMigrationRulesRequestParams),
},
},
},
withLicense(
withExistingMigration(
async (context, req, res): Promise<IKibanaResponse<RuleMigrationRule>> => {
const { migration_id: migrationId } = req.params;
const originalRules = req.body;
const rulesCount = originalRules.length;
const siemMigrationAuditLogger = new SiemMigrationAuditLogger(context.securitySolution);
try {
const [firstOriginalRule] = originalRules;
if (!firstOriginalRule) {
return res.noContent();
}
const ctx = await context.resolve(['securitySolution']);
const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient();
await siemMigrationAuditLogger.logAddRules({
migrationId,
count: rulesCount,
});
const ruleMigrations = originalRules.map<AddRuleMigrationRulesInput>(
(originalRule) => ({
migration_id: migrationId,
original_rule: originalRule,
})
);
await ruleMigrationsClient.data.rules.create(ruleMigrations);
// Create identified resource documents without content to keep track of them
const resourceIdentifier = new ResourceIdentifier(firstOriginalRule.vendor);
const resources = resourceIdentifier
.fromOriginalRules(originalRules)
.map((resource) => ({ ...resource, migration_id: migrationId }));
if (resources.length > 0) {
await ruleMigrationsClient.data.resources.create(resources);
}
return res.ok();
} catch (error) {
logger.error(error);
await siemMigrationAuditLogger.logAddRules({
migrationId,
count: rulesCount,
error,
});
return res.badRequest({ body: error.message });
}
}
)
)
);
};

View file

@ -0,0 +1,83 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { IKibanaResponse, Logger } from '@kbn/core/server';
import { buildRouteValidationWithZod } from '@kbn/zod-helpers';
import { SIEM_RULE_MIGRATION_RULES_PATH } from '../../../../../../common/siem_migrations/constants';
import type { GetRuleMigrationRulesResponse } from '../../../../../../common/siem_migrations/model/api/rules/rule_migration.gen';
import {
GetRuleMigrationRulesRequestParams,
GetRuleMigrationRulesRequestQuery,
} from '../../../../../../common/siem_migrations/model/api/rules/rule_migration.gen';
import type { SecuritySolutionPluginRouter } from '../../../../../types';
import type { RuleMigrationGetRulesOptions } from '../../data/rule_migrations_data_rules_client';
import { SiemMigrationAuditLogger } from '../util/audit';
import { authz } from '../util/authz';
import { withLicense } from '../util/with_license';
import { withExistingMigration } from '../util/with_existing_migration_id';
export const registerSiemRuleMigrationsGetRulesRoute = (
router: SecuritySolutionPluginRouter,
logger: Logger
) => {
router.versioned
.get({
path: SIEM_RULE_MIGRATION_RULES_PATH,
access: 'internal',
security: { authz },
})
.addVersion(
{
version: '1',
validate: {
request: {
params: buildRouteValidationWithZod(GetRuleMigrationRulesRequestParams),
query: buildRouteValidationWithZod(GetRuleMigrationRulesRequestQuery),
},
},
},
withLicense(
withExistingMigration(
async (context, req, res): Promise<IKibanaResponse<GetRuleMigrationRulesResponse>> => {
const { migration_id: migrationId } = req.params;
const siemMigrationAuditLogger = new SiemMigrationAuditLogger(context.securitySolution);
try {
const ctx = await context.resolve(['securitySolution']);
const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient();
const { page, per_page: size } = req.query;
const options: RuleMigrationGetRulesOptions = {
filters: {
searchTerm: req.query.search_term,
ids: req.query.ids,
prebuilt: req.query.is_prebuilt,
installed: req.query.is_installed,
fullyTranslated: req.query.is_fully_translated,
partiallyTranslated: req.query.is_partially_translated,
untranslatable: req.query.is_untranslatable,
failed: req.query.is_failed,
},
sort: { sortField: req.query.sort_field, sortDirection: req.query.sort_direction },
size,
from: page && size ? page * size : 0,
};
const result = await ruleMigrationsClient.data.rules.get(migrationId, options);
await siemMigrationAuditLogger.logGetMigrationRules({ migrationId });
return res.ok({ body: result });
} catch (error) {
logger.error(error);
await siemMigrationAuditLogger.logGetMigrationRules({ migrationId, error });
return res.badRequest({ body: error.message });
}
}
)
)
);
};

View file

@ -0,0 +1,76 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { IKibanaResponse, Logger } from '@kbn/core/server';
import { buildRouteValidationWithZod } from '@kbn/zod-helpers';
import { SIEM_RULE_MIGRATION_RULES_PATH } from '../../../../../../common/siem_migrations/constants';
import type { UpdateRuleMigrationRulesResponse } from '../../../../../../common/siem_migrations/model/api/rules/rule_migration.gen';
import {
UpdateRuleMigrationRulesRequestBody,
UpdateRuleMigrationRulesRequestParams,
} from '../../../../../../common/siem_migrations/model/api/rules/rule_migration.gen';
import type { SecuritySolutionPluginRouter } from '../../../../../types';
import { authz } from '../util/authz';
import { SiemMigrationAuditLogger } from '../util/audit';
import { transformToInternalUpdateRuleMigrationData } from '../util/update_rules';
import { withLicense } from '../util/with_license';
import { withExistingMigration } from '../util/with_existing_migration_id';
export const registerSiemRuleMigrationsUpdateRulesRoute = (
router: SecuritySolutionPluginRouter,
logger: Logger
) => {
router.versioned
.patch({
path: SIEM_RULE_MIGRATION_RULES_PATH,
access: 'internal',
security: { authz },
})
.addVersion(
{
version: '1',
validate: {
request: {
params: buildRouteValidationWithZod(UpdateRuleMigrationRulesRequestParams),
body: buildRouteValidationWithZod(UpdateRuleMigrationRulesRequestBody),
},
},
},
withLicense(
withExistingMigration(
async (context, req, res): Promise<IKibanaResponse<UpdateRuleMigrationRulesResponse>> => {
const { migration_id: migrationId } = req.params;
const rulesToUpdate = req.body;
if (rulesToUpdate.length === 0) {
return res.noContent();
}
const ids = rulesToUpdate.map((rule) => rule.id);
const siemMigrationAuditLogger = new SiemMigrationAuditLogger(context.securitySolution);
try {
const ctx = await context.resolve(['securitySolution']);
const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient();
await siemMigrationAuditLogger.logUpdateRules({ migrationId, ids });
const transformedRuleToUpdate = rulesToUpdate.map(
transformToInternalUpdateRuleMigrationData
);
await ruleMigrationsClient.data.rules.update(transformedRuleToUpdate);
return res.ok({ body: { updated: true } });
} catch (error) {
logger.error(error);
await siemMigrationAuditLogger.logUpdateRules({ migrationId, ids, error });
return res.badRequest({ body: error.message });
}
}
)
)
);
};

View file

@ -19,13 +19,14 @@ import { authz } from './util/authz';
import { getRetryFilter } from './util/retry';
import { withLicense } from './util/with_license';
import { createTracersCallbacks } from './util/tracing';
import { withExistingMigration } from './util/with_existing_migration_id';
export const registerSiemRuleMigrationsStartRoute = (
router: SecuritySolutionPluginRouter,
logger: Logger
) => {
router.versioned
.put({
.post({
path: SIEM_RULE_MIGRATION_START_PATH,
access: 'internal',
security: { authz },
@ -41,6 +42,7 @@ export const registerSiemRuleMigrationsStartRoute = (
},
},
withLicense(
withExistingMigration(
async (context, req, res): Promise<IKibanaResponse<StartRuleMigrationResponse>> => {
const migrationId = req.params.migration_id;
const {
@ -51,7 +53,12 @@ export const registerSiemRuleMigrationsStartRoute = (
const siemMigrationAuditLogger = new SiemMigrationAuditLogger(context.securitySolution);
try {
const ctx = await context.resolve(['core', 'actions', 'alerting', 'securitySolution']);
const ctx = await context.resolve([
'core',
'actions',
'alerting',
'securitySolution',
]);
// Check if the connector exists and user has permissions to read it
const connector = await ctx.actions.getActionsClient().get({ id: connectorId });
@ -92,5 +99,6 @@ export const registerSiemRuleMigrationsStartRoute = (
}
}
)
)
);
};

View file

@ -15,6 +15,7 @@ import { SIEM_RULE_MIGRATION_STATS_PATH } from '../../../../../common/siem_migra
import type { SecuritySolutionPluginRouter } from '../../../../types';
import { authz } from './util/authz';
import { withLicense } from './util/with_license';
import { withExistingMigration } from './util/with_existing_migration_id';
export const registerSiemRuleMigrationsStatsRoute = (
router: SecuritySolutionPluginRouter,
@ -34,6 +35,7 @@ export const registerSiemRuleMigrationsStatsRoute = (
},
},
withLicense(
withExistingMigration(
async (context, req, res): Promise<IKibanaResponse<GetRuleMigrationStatsResponse>> => {
const migrationId = req.params.migration_id;
try {
@ -52,5 +54,6 @@ export const registerSiemRuleMigrationsStatsRoute = (
}
}
)
)
);
};

View file

@ -16,13 +16,14 @@ import type { SecuritySolutionPluginRouter } from '../../../../types';
import { SiemMigrationAuditLogger } from './util/audit';
import { authz } from './util/authz';
import { withLicense } from './util/with_license';
import { withExistingMigration } from './util/with_existing_migration_id';
export const registerSiemRuleMigrationsStopRoute = (
router: SecuritySolutionPluginRouter,
logger: Logger
) => {
router.versioned
.put({
.post({
path: SIEM_RULE_MIGRATION_STOP_PATH,
access: 'internal',
security: { authz },
@ -35,6 +36,7 @@ export const registerSiemRuleMigrationsStopRoute = (
},
},
withLicense(
withExistingMigration(
async (context, req, res): Promise<IKibanaResponse<StopRuleMigrationResponse>> => {
const migrationId = req.params.migration_id;
const siemMigrationAuditLogger = new SiemMigrationAuditLogger(context.securitySolution);
@ -57,5 +59,6 @@ export const registerSiemRuleMigrationsStopRoute = (
}
}
)
)
);
};

View file

@ -13,6 +13,7 @@ import { SIEM_RULE_MIGRATION_TRANSLATION_STATS_PATH } from '../../../../../commo
import type { SecuritySolutionPluginRouter } from '../../../../types';
import { authz } from './util/authz';
import { withLicense } from './util/with_license';
import { withExistingMigration } from './util/with_existing_migration_id';
export const registerSiemRuleMigrationsTranslationStatsRoute = (
router: SecuritySolutionPluginRouter,
@ -34,6 +35,7 @@ export const registerSiemRuleMigrationsTranslationStatsRoute = (
},
},
withLicense(
withExistingMigration(
async (
context,
req,
@ -56,5 +58,6 @@ export const registerSiemRuleMigrationsTranslationStatsRoute = (
}
}
)
)
);
};

View file

@ -12,6 +12,9 @@ import type { SecuritySolutionApiRequestHandlerContext } from '../../../../..';
export enum SiemMigrationsAuditActions {
SIEM_MIGRATION_CREATED = 'siem_migration_created',
SIEM_MIGRATION_RETRIEVED = 'siem_migration_retrieved',
SIEM_MIGRATION_DELETED = 'siem_migration_deleted',
SIEM_MIGRATION_ADDED_RULES = 'siem_migration_added_rules',
SIEM_MIGRATION_RETRIEVED_RULES = 'siem_migration_retrieved_rules',
SIEM_MIGRATION_UPLOADED_RESOURCES = 'siem_migration_uploaded_resources',
SIEM_MIGRATION_RETRIEVED_RESOURCES = 'siem_migration_retrieved_resources',
SIEM_MIGRATION_STARTED = 'siem_migration_started',
@ -53,6 +56,9 @@ export const siemMigrationAuditEventType: Record<
[SiemMigrationsAuditActions.SIEM_MIGRATION_STOPPED]: AUDIT_TYPE.END,
[SiemMigrationsAuditActions.SIEM_MIGRATION_UPDATED_RULE]: AUDIT_TYPE.CHANGE,
[SiemMigrationsAuditActions.SIEM_MIGRATION_INSTALLED_RULES]: AUDIT_TYPE.CREATION,
[SiemMigrationsAuditActions.SIEM_MIGRATION_ADDED_RULES]: AUDIT_TYPE.CREATION,
[SiemMigrationsAuditActions.SIEM_MIGRATION_RETRIEVED_RULES]: AUDIT_TYPE.ACCESS,
[SiemMigrationsAuditActions.SIEM_MIGRATION_DELETED]: AUDIT_TYPE.CHANGE,
};
interface SiemMigrationAuditEvent {
@ -106,9 +112,9 @@ export class SiemMigrationAuditLogger {
}
}
public async logCreateMigration(params: { migrationId?: string; error?: Error }): Promise<void> {
const { migrationId, error } = params;
const message = `User created a new SIEM migration with [id=${migrationId}]`;
public async logCreateMigration(params: { error?: Error } = {}): Promise<void> {
const { error } = params;
const message = `User created a new SIEM migration`;
return this.log({
action: SiemMigrationsAuditActions.SIEM_MIGRATION_CREATED,
message,
@ -126,6 +132,42 @@ export class SiemMigrationAuditLogger {
});
}
public async logDeleteMigration(params: { migrationId: string; error?: Error }): Promise<void> {
const { migrationId, error } = params;
const message = `User deleted the SIEM migration with [id=${migrationId}]`;
return this.log({
action: SiemMigrationsAuditActions.SIEM_MIGRATION_DELETED,
message,
error,
});
}
public async logGetMigrationRules(params: { migrationId: string; error?: Error }): Promise<void> {
const { migrationId, error } = params;
const message = `User retrieved rules for SIEM migration with [id=${migrationId}]`;
return this.log({
action: SiemMigrationsAuditActions.SIEM_MIGRATION_RETRIEVED_RULES,
message,
error,
});
}
public async logAddRules(params: {
migrationId: string;
error?: Error;
count?: number;
}): Promise<void> {
const { migrationId, error, count } = params;
const message = `User added ${
count ?? ''
} rules to the SIEM migration with [id=${migrationId}]`;
return this.log({
action: SiemMigrationsAuditActions.SIEM_MIGRATION_ADDED_RULES,
message,
error,
});
}
public async logUploadResources(params: { migrationId: string; error?: Error }): Promise<void> {
const { migrationId, error } = params;
const message = `User uploaded resources to the SIEM migration with [id=${migrationId}]`;

View file

@ -8,7 +8,7 @@
import type { SavedObjectsClientContract } from '@kbn/core/server';
import type { RulesClient } from '@kbn/alerting-plugin/server';
import { getErrorMessage } from '../../../../../utils/error_helpers';
import type { UpdateRuleMigrationData } from '../../../../../../common/siem_migrations/model/rule_migration.gen';
import type { UpdateRuleMigrationRule } from '../../../../../../common/siem_migrations/model/rule_migration.gen';
import { initPromisePool } from '../../../../../utils/promise_pool';
import type { SecuritySolutionApiRequestHandlerContext } from '../../../../..';
import { performTimelinesInstallation } from '../../../../detection_engine/prebuilt_rules/logic/perform_timelines_installation';
@ -31,7 +31,7 @@ const installPrebuiltRules = async (
rulesClient: RulesClient,
savedObjectsClient: SavedObjectsClientContract,
detectionRulesClient: IDetectionRulesClient
): Promise<{ rulesToUpdate: UpdateRuleMigrationData[]; errors: Error[] }> => {
): Promise<{ rulesToUpdate: UpdateRuleMigrationRule[]; errors: Error[] }> => {
// Get required prebuilt rules
const prebuiltRulesIds = getUniquePrebuiltRuleIds(rulesToInstall);
const prebuiltRules = await getPrebuiltRules(rulesClient, savedObjectsClient, prebuiltRulesIds);
@ -68,7 +68,7 @@ const installPrebuiltRules = async (
];
// Create migration rules updates templates
const rulesToUpdate: UpdateRuleMigrationData[] = [];
const rulesToUpdate: UpdateRuleMigrationRule[] = [];
installedRules.forEach((installedRule) => {
const filteredRules = rulesToInstall.filter(
(rule) => rule.elastic_rule?.prebuilt_rule_id === installedRule.rule_id
@ -91,11 +91,11 @@ export const installCustomRules = async (
enabled: boolean,
detectionRulesClient: IDetectionRulesClient
): Promise<{
rulesToUpdate: UpdateRuleMigrationData[];
rulesToUpdate: UpdateRuleMigrationRule[];
errors: Error[];
}> => {
const errors: Error[] = [];
const rulesToUpdate: UpdateRuleMigrationData[] = [];
const rulesToUpdate: UpdateRuleMigrationRule[] = [];
const createCustomRulesOutcome = await initPromisePool({
concurrency: MAX_CUSTOM_RULES_TO_CREATE_IN_PARALLEL,
items: rulesToInstall,

View file

@ -7,15 +7,15 @@
import type { SavedObjectsClientContract } from '@kbn/core/server';
import type { RulesClient } from '@kbn/alerting-plugin/server';
import type { RuleMigrationRule } from '../../../../../../common/siem_migrations/model/rule_migration.gen';
import type { RuleResponse } from '../../../../../../common/api/detection_engine';
import { createPrebuiltRuleObjectsClient } from '../../../../detection_engine/prebuilt_rules/logic/rule_objects/prebuilt_rule_objects_client';
import { fetchRuleVersionsTriad } from '../../../../detection_engine/prebuilt_rules/logic/rule_versions/fetch_rule_versions_triad';
import { createPrebuiltRuleAssetsClient } from '../../../../detection_engine/prebuilt_rules/logic/rule_assets/prebuilt_rule_assets_client';
import { convertPrebuiltRuleAssetToRuleResponse } from '../../../../detection_engine/rule_management/logic/detection_rules_client/converters/convert_prebuilt_rule_asset_to_rule_response';
import type { RuleMigration } from '../../../../../../common/siem_migrations/model/rule_migration.gen';
import type { SiemRuleMigrationsClient } from '../../siem_rule_migrations_service';
export const getUniquePrebuiltRuleIds = (migrationRules: RuleMigration[]): string[] => {
export const getUniquePrebuiltRuleIds = (migrationRules: RuleMigrationRule[]): string[] => {
const rulesIds = new Set<string>();
migrationRules.forEach((rule) => {
if (rule.elastic_rule?.prebuilt_rule_id) {

View file

@ -6,12 +6,12 @@
*/
import { parseEsqlQuery } from '@kbn/securitysolution-utils';
import type { UpdateRuleMigrationData } from '../../../../../../common/siem_migrations/model/rule_migration.gen';
import type { UpdateRuleMigrationRule } from '../../../../../../common/siem_migrations/model/rule_migration.gen';
import {
RuleMigrationTranslationResultEnum,
type RuleMigrationTranslationResult,
} from '../../../../../../common/siem_migrations/model/rule_migration.gen';
import type { InternalUpdateRuleMigrationData } from '../../types';
import type { InternalUpdateRuleMigrationRule } from '../../types';
export const isValidEsqlQuery = (esqlQuery: string) => {
const { isEsqlQueryAggregating, hasMetadataOperator, errors } = parseEsqlQuery(esqlQuery);
@ -41,8 +41,8 @@ export const convertEsqlQueryToTranslationResult = (
};
export const transformToInternalUpdateRuleMigrationData = (
ruleMigration: UpdateRuleMigrationData
): InternalUpdateRuleMigrationData => {
ruleMigration: UpdateRuleMigrationRule
): InternalUpdateRuleMigrationRule => {
if (ruleMigration.elastic_rule?.query == null) {
return ruleMigration;
}

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 type { KibanaRequest, KibanaResponseFactory } from '@kbn/core/server';
import type { SecuritySolutionRequestHandlerContext } from '../../../../../types';
import { withExistingMigration } from './with_existing_migration_id';
const mockRuleMigrationsClient = {
data: {
migrations: {
get: jest.fn(),
},
},
};
const mockSecuritySolutionContext = {
securitySolution: {
getSiemRuleMigrationsClient: jest.fn().mockReturnValue(mockRuleMigrationsClient),
},
};
const mockContext = {
resolve: jest.fn().mockResolvedValue(mockSecuritySolutionContext),
} as unknown as SecuritySolutionRequestHandlerContext;
const mockMigration = {
id: 'test-migration-id',
created_at: '2023-10-01T00:00:00Z',
created_by: 'test-user',
};
const mockReq = {
params: {
migration_id: 'test-migration-id',
},
} as unknown as KibanaRequest<{ migration_id: string }, unknown, unknown, never>;
const mockRes = {
notFound: jest.fn(),
} as unknown as KibanaResponseFactory;
describe('withExistingMigrationId', () => {
describe('when migration exists', () => {
beforeEach(() => {
mockRuleMigrationsClient.data.migrations.get.mockResolvedValue(mockMigration);
});
it('should call the handler', async () => {
const handler = jest.fn();
const wrappedHandler = withExistingMigration(handler);
await wrappedHandler(mockContext, mockReq, mockRes);
expect(handler).toHaveBeenCalledWith(mockContext, mockReq, mockRes);
});
});
describe('when migration does not exist', () => {
beforeEach(() => {
mockRuleMigrationsClient.data.migrations.get.mockResolvedValue(undefined);
});
it('should return a 404 response', async () => {
const handler = jest.fn();
const wrappedHandler = withExistingMigration(handler);
await wrappedHandler(mockContext, mockReq, mockRes);
expect(mockRes.notFound).toHaveBeenCalledWith({
body: expect.stringContaining('No Migration found with id: test-migration-id'),
});
});
});
});

View file

@ -0,0 +1,51 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { RequestHandler, RouteMethod } from '@kbn/core/server';
import { i18n } from '@kbn/i18n';
import type { SecuritySolutionRequestHandlerContext } from '../../../../../types';
export const MIGRATION_ID_NOT_FOUND = (id: string) =>
i18n.translate('xpack.securitySolution.api.migrationIdNotFound', {
defaultMessage: `No Migration found with id: {id}`,
values: {
id,
},
});
/**
* Checks the existence of a valid migration before proceeding with the request.
*
* if not found, it returns a 404 error with a message.
* if found, it adds the migration to the context.
*
* */
export const withExistingMigration = <
P extends { migration_id: string },
Q = unknown,
B = unknown,
Method extends RouteMethod = never
>(
handler: RequestHandler<P, Q, B, SecuritySolutionRequestHandlerContext, Method>
): RequestHandler<P, Q, B, SecuritySolutionRequestHandlerContext, Method> => {
return async (context, req, res) => {
const { migration_id: migrationId } = req.params;
const ctx = await context.resolve(['securitySolution']);
const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient();
const storedMigration = await ruleMigrationsClient.data.migrations.get({
id: migrationId,
});
if (!storedMigration) {
return res.notFound({
body: MIGRATION_ID_NOT_FOUND(migrationId),
});
}
return handler(context, req, res);
};
};

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import type { RuleMigrationsDataClient } from '../rule_migrations_data_client';
import type { RuleMigrationsDataIntegrationsClient } from '../rule_migrations_data_integrations_client';
import type { RuleMigrationsDataLookupsClient } from '../rule_migrations_data_lookups_client';
import type { RuleMigrationsDataMigrationClient } from '../rule_migrations_data_migration_client';
@ -63,15 +64,19 @@ export const mockRuleMigrationsDataMigrationsClient = {
get: jest.fn().mockResolvedValue(undefined),
} as unknown as jest.Mocked<RuleMigrationsDataMigrationClient>;
export const mockDeleteMigration = jest.fn().mockResolvedValue(undefined);
// Rule migrations data client
export const createRuleMigrationsDataClientMock = () => ({
export const createRuleMigrationsDataClientMock = () =>
({
rules: mockRuleMigrationsDataRulesClient,
resources: mockRuleMigrationsDataResourcesClient,
integrations: mockRuleMigrationsDataIntegrationsClient,
prebuiltRules: mockRuleMigrationsDataPrebuiltRulesClient,
lookups: mockRuleMigrationsDataLookupsClient,
migrations: mockRuleMigrationsDataMigrationsClient,
});
deleteMigration: mockDeleteMigration,
} as unknown as jest.MockedObjectDeep<RuleMigrationsDataClient>);
export const MockRuleMigrationsDataClient = jest
.fn()
@ -81,10 +86,12 @@ export const MockRuleMigrationsDataClient = jest
export const mockIndexName = 'mocked_siem_rule_migrations_index_name';
export const mockInstall = jest.fn().mockResolvedValue(undefined);
export const mockCreateClient = jest.fn(() => createRuleMigrationsDataClientMock());
export const mockSetup = jest.fn().mockResolvedValue(undefined);
export const MockRuleMigrationsDataService = jest.fn().mockImplementation(() => ({
createAdapter: jest.fn(),
install: mockInstall,
createClient: mockCreateClient,
createIndexNameProvider: jest.fn().mockResolvedValue(mockIndexName),
setup: mockSetup,
}));

View file

@ -18,7 +18,8 @@ import type {
Logger,
} from '@kbn/core/server';
import assert from 'assert';
import type { IndexNameProvider, SiemRuleMigrationsClientDependencies, Stored } from '../types';
import type { IndexNameProvider, SiemRuleMigrationsClientDependencies } from '../types';
import type { Stored } from '../../types';
const DEFAULT_PIT_KEEP_ALIVE: Duration = '30s' as const;

View file

@ -0,0 +1,99 @@
/*
* 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 { AuthenticatedUser } from '@kbn/security-plugin-types-common';
import { RuleMigrationsDataClient } from './rule_migrations_data_client';
import { RuleMigrationsDataResourcesClient } from './rule_migrations_data_resources_client';
import { RuleMigrationsDataRulesClient } from './rule_migrations_data_rules_client';
import type { IScopedClusterClient, Logger } from '@kbn/core/server';
import type { SiemRuleMigrationsClientDependencies } from '../types';
jest.mock('./rule_migrations_data_rules_client');
jest.mock('./rule_migrations_data_resources_client');
const mockedRulesClient = {
prepareDelete: jest
.fn()
.mockReturnValue([
{ delete: { _id: 'rule1', _index: '.mocked-rule-index' } },
{ delete: { _id: 'rule2', _index: '.mocked-rule-index' } },
]),
} as unknown as jest.Mocked<RuleMigrationsDataRulesClient>;
const mockedResourcesClient = {
prepareDelete: jest
.fn()
.mockReturnValue([{ delete: { _id: 'resource1', _index: '.mocked-resource-index' } }]),
} as unknown as jest.Mocked<RuleMigrationsDataResourcesClient>;
const mockIndexNameProviders = {
migrations: jest.fn().mockReturnValue('.mocked-migration-index'),
rules: jest.fn().mockReturnValue('.mocked-rule-index'),
resources: jest.fn().mockReturnValue('.mocked-resource-index'),
prebuiltrules: jest.fn().mockReturnValue('.mocked-prebuilt-rules-index'),
integrations: jest.fn().mockReturnValue('.mocked-integrations-index'),
};
const mockCurrentUser = {
username: 'testUser',
profile_uid: 'testProfileUid',
} as unknown as AuthenticatedUser;
const mockEsClient = {
asInternalUser: {
bulk: jest.fn().mockResolvedValue({ errors: false }),
},
} as unknown as jest.Mocked<IScopedClusterClient>;
const mockLogger = {
error: jest.fn(),
info: jest.fn(),
} as unknown as jest.Mocked<Logger>;
const mockSpaceId = 'default';
const mockDependencies = {} as unknown as jest.Mocked<SiemRuleMigrationsClientDependencies>;
describe('RuleMigrationsDataClient', () => {
beforeEach(() => {
(RuleMigrationsDataRulesClient as unknown as jest.Mock).mockImplementation(
() => mockedRulesClient
);
(RuleMigrationsDataResourcesClient as unknown as jest.Mock).mockImplementation(
() => mockedResourcesClient
);
});
afterEach(() => {
jest.clearAllMocks();
});
describe('deleteMigration', () => {
it('should delete the migration and associated rules and resources', async () => {
const dataClient = new RuleMigrationsDataClient(
mockIndexNameProviders,
mockCurrentUser,
mockEsClient,
mockLogger,
mockSpaceId,
mockDependencies
);
const migrationId = 'testId';
await dataClient.deleteMigration(migrationId);
expect(mockEsClient.asInternalUser.bulk).toHaveBeenCalledWith({
refresh: 'wait_for',
operations: [
{ delete: { _id: migrationId, _index: '.mocked-migration-index' } },
{ delete: { _id: 'rule1', _index: '.mocked-rule-index' } },
{ delete: { _id: 'rule2', _index: '.mocked-rule-index' } },
{ delete: { _id: 'resource1', _index: '.mocked-resource-index' } },
],
});
});
});
});

View file

@ -15,6 +15,9 @@ import type { IndexNameProviders, SiemRuleMigrationsClientDependencies } from '.
import { RuleMigrationsDataMigrationClient } from './rule_migrations_data_migration_client';
export class RuleMigrationsDataClient {
protected logger: Logger;
protected esClient: IScopedClusterClient['asInternalUser'];
public readonly migrations: RuleMigrationsDataMigrationClient;
public readonly rules: RuleMigrationsDataRulesClient;
public readonly resources: RuleMigrationsDataResourcesClient;
@ -71,5 +74,40 @@ export class RuleMigrationsDataClient {
logger,
spaceId
);
this.logger = logger;
this.esClient = esScopedClient.asInternalUser;
}
/**
*
* Deletes a migration and all its associated rules and resources.
*
*/
async deleteMigration(migrationId: string) {
const migrationDeleteOperations = await this.migrations.prepareDelete({
id: migrationId,
});
const rulesByMigrationIdDeleteOperations = await this.rules.prepareDelete(migrationId);
const resourcesByMigrationIdDeleteOperations = await this.resources.prepareDelete(migrationId);
return this.esClient
.bulk({
refresh: 'wait_for',
operations: [
...migrationDeleteOperations,
...rulesByMigrationIdDeleteOperations,
...resourcesByMigrationIdDeleteOperations,
],
})
.then(() => {
this.logger.info(`Deleted migration ${migrationId}`);
})
.catch((error) => {
this.logger.error(`Error deleting migration ${migrationId}: ${error}`);
throw error;
});
}
}

View file

@ -95,6 +95,25 @@ describe('RuleMigrationsDataMigrationClient', () => {
id: response._id,
});
});
test('should return undefined if the migration is not found', async () => {
const id = 'testId';
const response = {
_index: '.kibana-siem-rule-migrations',
found: false,
};
(
esClient.asInternalUser.get as unknown as jest.MockedFn<typeof GetApi>
).mockRejectedValueOnce({
message: JSON.stringify(response),
});
const result = await ruleMigrationsDataMigrationClient.get({ id });
expect(result).toBeUndefined();
});
test('should throw an error if an error occurs', async () => {
const id = 'testId';
(
@ -107,4 +126,26 @@ describe('RuleMigrationsDataMigrationClient', () => {
expect(logger.error).toHaveBeenCalledWith(`Error getting migration ${id}: Error: Test error`);
});
});
describe('prepareDelete', () => {
beforeEach(() => jest.clearAllMocks());
it('should delete the migration and associated rules and resources', async () => {
const migrationId = 'testId';
const index = '.kibana-siem-rule-migrations';
const operations = await ruleMigrationsDataMigrationClient.prepareDelete({
id: migrationId,
});
expect(operations).toMatchObject([
{
delete: {
_index: index,
_id: migrationId,
},
},
]);
});
});
});

View file

@ -6,8 +6,10 @@
*/
import { v4 as uuidV4 } from 'uuid';
import type { BulkOperationContainer } from '@elastic/elasticsearch/lib/api/types';
import type { StoredSiemMigration } from '../types';
import { RuleMigrationsDataBaseClient } from './rule_migrations_data_base_client';
import { isNotFoundError } from './utils';
export class RuleMigrationsDataMigrationClient extends RuleMigrationsDataBaseClient {
async create(): Promise<string> {
@ -34,19 +36,42 @@ export class RuleMigrationsDataMigrationClient extends RuleMigrationsDataBaseCli
return migrationId;
}
async get({ id }: { id: string }): Promise<StoredSiemMigration> {
/**
*
* Gets the migration document by id or returns undefined if it does not exist.
*
* */
async get({ id }: { id: string }): Promise<StoredSiemMigration | undefined> {
const index = await this.getIndexName();
return this.esClient
.get<StoredSiemMigration>({
index,
id,
})
.then((document) => {
return this.processHit(document);
})
.then(this.processHit)
.catch((error) => {
if (isNotFoundError(error)) {
return undefined;
}
this.logger.error(`Error getting migration ${id}: ${error}`);
throw error;
});
}
/**
*
* Prepares bulk ES delete operation for a migration document based on its id.
*
*/
async prepareDelete({ id }: { id: string }): Promise<BulkOperationContainer[]> {
const index = await this.getIndexName();
const migrationDeleteOperation = {
delete: {
_index: index,
_id: id,
},
};
return [migrationDeleteOperation];
}
}

View file

@ -6,7 +6,11 @@
*/
import { sha256 } from 'js-sha256';
import type { QueryDslQueryContainer, Duration } from '@elastic/elasticsearch/lib/api/types';
import type {
QueryDslQueryContainer,
Duration,
BulkOperationContainer,
} from '@elastic/elasticsearch/lib/api/types';
import type {
RuleMigrationResource,
RuleMigrationResourceType,
@ -156,4 +160,21 @@ export class RuleMigrationsDataResourcesClient extends RuleMigrationsDataBaseCli
}
return { bool: { filter } };
}
/**
*
* Prepares bulk ES delete operations for the resources of a given migrationId.
*
*/
async prepareDelete(migrationId: string): Promise<BulkOperationContainer[]> {
const index = await this.getIndexName();
const resourcesToBeDeleted = await this.get(migrationId, { size: 10000 });
const resourcesToBeDeletedDocIds = resourcesToBeDeleted.map((resource) => resource.id);
return resourcesToBeDeletedDocIds.map((docId) => ({
delete: {
_id: docId,
_index: index,
},
}));
}
}

View file

@ -14,14 +14,15 @@ import type {
AggregationsStringTermsBucket,
QueryDslQueryContainer,
Duration,
BulkOperationContainer,
} from '@elastic/elasticsearch/lib/api/types';
import type { RuleMigrationFilters } from '../../../../../common/siem_migrations/types';
import type { InternalUpdateRuleMigrationData, StoredRuleMigration } from '../types';
import type { InternalUpdateRuleMigrationRule, StoredRuleMigration } from '../types';
import {
SiemMigrationStatus,
RuleTranslationResult,
} from '../../../../../common/siem_migrations/constants';
import type { RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen';
import type { RuleMigrationRule } from '../../../../../common/siem_migrations/model/rule_migration.gen';
import {
type RuleMigrationTaskStats,
type RuleMigrationTranslationStats,
@ -30,14 +31,14 @@ import { getSortingOptions, type RuleMigrationSort } from './sort';
import { conditions as searchConditions } from './search';
import { RuleMigrationsDataBaseClient } from './rule_migrations_data_base_client';
export type CreateRuleMigrationInput = Omit<
RuleMigration,
export type AddRuleMigrationRulesInput = Omit<
RuleMigrationRule,
'@timestamp' | 'id' | 'status' | 'created_by'
>;
export type RuleMigrationDataStats = Omit<RuleMigrationTaskStats, 'status'>;
export type RuleMigrationAllDataStats = RuleMigrationDataStats[];
export interface RuleMigrationGetOptions {
export interface RuleMigrationGetRulesOptions {
filters?: RuleMigrationFilters;
sort?: RuleMigrationSort;
from?: number;
@ -53,11 +54,11 @@ const DEFAULT_SEARCH_BATCH_SIZE = 500 as const;
export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient {
/** Indexes an array of rule migrations to be processed */
async create(ruleMigrations: CreateRuleMigrationInput[]): Promise<void> {
async create(ruleMigrations: AddRuleMigrationRulesInput[]): Promise<void> {
const index = await this.getIndexName();
const profileId = await this.getProfileUid();
let ruleMigrationsSlice: CreateRuleMigrationInput[];
let ruleMigrationsSlice: AddRuleMigrationRulesInput[];
const createdAt = new Date().toISOString();
while ((ruleMigrationsSlice = ruleMigrations.splice(0, BULK_MAX_SIZE)).length) {
await this.esClient
@ -83,11 +84,11 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient
}
/** Updates an array of rule migrations to be processed */
async update(ruleMigrations: InternalUpdateRuleMigrationData[]): Promise<void> {
async update(ruleMigrations: InternalUpdateRuleMigrationRule[]): Promise<void> {
const index = await this.getIndexName();
const profileId = await this.getProfileUid();
let ruleMigrationsSlice: InternalUpdateRuleMigrationData[];
let ruleMigrationsSlice: InternalUpdateRuleMigrationRule[];
const updatedAt = new Date().toISOString();
while ((ruleMigrationsSlice = ruleMigrations.splice(0, BULK_MAX_SIZE)).length) {
await this.esClient
@ -117,14 +118,14 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient
/** Retrieves an array of rule documents of a specific migrations */
async get(
migrationId: string,
{ filters = {}, sort: sortParam = {}, from, size }: RuleMigrationGetOptions = {}
{ filters = {}, sort: sortParam = {}, from, size }: RuleMigrationGetRulesOptions = {}
): Promise<{ total: number; data: StoredRuleMigration[] }> {
const index = await this.getIndexName();
const query = this.getFilterQuery(migrationId, filters);
const sort = sortParam.sortField ? getSortingOptions(sortParam) : undefined;
const result = await this.esClient
.search<RuleMigration>({ index, query, sort, from, size })
.search<RuleMigrationRule>({ index, query, sort, from, size })
.catch((error) => {
this.logger.error(`Error searching rule migrations: ${error.message}`);
throw error;
@ -144,7 +145,7 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient
const query = this.getFilterQuery(migrationId, filters);
const search = { query, sort: '_doc', scroll, size }; // sort by _doc to ensure consistent order
try {
return this.getSearchBatches<RuleMigration>(search);
return this.getSearchBatches<RuleMigrationRule>(search);
} catch (error) {
this.logger.error(`Error scrolling rule migrations: ${error.message}`);
throw error;
@ -415,4 +416,22 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient
}
return { bool: { filter } };
}
/**
*
* Prepares bulk ES delete operations for the rules based on migrationId.
*
* */
async prepareDelete(migrationId: string): Promise<BulkOperationContainer[]> {
const index = await this.getIndexName();
const rulesToBeDeleted = await this.get(migrationId, { size: 10000 });
const rulesToBeDeletedDocIds = rulesToBeDeleted.data.map((rule) => rule.id);
return rulesToBeDeletedDocIds.map((docId) => ({
delete: {
_id: docId,
_index: index,
},
}));
}
}

View file

@ -8,12 +8,14 @@
import { elasticsearchServiceMock } from '@kbn/core-elasticsearch-server-mocks';
import { loggingSystemMock } from '@kbn/core-logging-server-mocks';
import { securityServiceMock } from '@kbn/core-security-server-mocks';
import type { InstallParams } from '@kbn/index-adapter';
import { IndexPatternAdapter, IndexAdapter } from '@kbn/index-adapter';
import { loggerMock } from '@kbn/logging-mocks';
import { Subject } from 'rxjs';
import type { IndexNameProviders, SiemRuleMigrationsClientDependencies } from '../types';
import type { SetupParams } from './rule_migrations_data_service';
import { INDEX_PATTERN, RuleMigrationsDataService } from './rule_migrations_data_service';
import { RuleMigrationIndexMigrator } from '../index_migrators';
jest.mock('../index_migrators');
jest.mock('@kbn/index-adapter');
@ -86,18 +88,19 @@ describe('SiemRuleMigrationsDataService', () => {
});
describe('install', () => {
it('should install index pattern', async () => {
it('should install index pattern and run the migration', async () => {
const service = new RuleMigrationsDataService(logger, kibanaVersion);
const params: Omit<InstallParams, 'logger'> = {
const params: SetupParams = {
esClient,
pluginStop$: new Subject(),
};
await service.install(params);
await service.setup(params);
const [indexPatternAdapter] = MockedIndexPatternAdapter.mock.instances;
const [indexAdapter] = MockedIndexAdapter.mock.instances;
expect(indexPatternAdapter.install).toHaveBeenCalledWith(expect.objectContaining(params));
expect(indexAdapter.install).toHaveBeenCalledWith(expect.objectContaining(params));
expect(RuleMigrationIndexMigrator).toHaveBeenCalled();
});
});
@ -112,9 +115,8 @@ describe('SiemRuleMigrationsDataService', () => {
it('should install space index pattern', async () => {
const service = new RuleMigrationsDataService(logger, kibanaVersion);
const params: InstallParams = {
const params: SetupParams = {
esClient,
logger: loggerMock.create(),
pluginStop$: new Subject(),
};
@ -122,7 +124,7 @@ describe('SiemRuleMigrationsDataService', () => {
MockedIndexPatternAdapter.mock.instances;
(rulesIndexPatternAdapter.install as jest.Mock).mockResolvedValueOnce(undefined);
await service.install(params);
await service.setup(params);
service.createClient(createClientParams);
await mockIndexNameProviders.rules();

View file

@ -4,7 +4,12 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { AuthenticatedUser, IScopedClusterClient, Logger } from '@kbn/core/server';
import type {
AuthenticatedUser,
ElasticsearchClient,
IScopedClusterClient,
Logger,
} from '@kbn/core/server';
import {
IndexAdapter,
IndexPatternAdapter,
@ -27,6 +32,7 @@ import {
ruleMigrationResourcesFieldMap,
ruleMigrationsFieldMap,
} from './rule_migrations_field_maps';
import { RuleMigrationIndexMigrator } from '../index_migrators';
const TOTAL_FIELDS_LIMIT = 2500;
export const INDEX_PATTERN = '.kibana-siem-rule-migrations';
@ -42,6 +48,10 @@ interface CreateAdapterParams {
fieldMap: FieldMap;
}
export interface SetupParams extends Omit<InstallParams, 'logger'> {
esClient: ElasticsearchClient;
}
export class RuleMigrationsDataService {
private readonly adapters: Adapters;
@ -96,7 +106,12 @@ export class RuleMigrationsDataService {
return adapter;
}
public async install(params: Omit<InstallParams, 'logger'>): Promise<void> {
private async runIndexMigrations(esClient: SetupParams['esClient']) {
const indexMigrator = new RuleMigrationIndexMigrator(this.adapters, esClient, this.logger);
await indexMigrator.run();
}
private async install(params: SetupParams): Promise<void> {
await Promise.all([
this.adapters.rules.install({ ...params, logger: this.logger }),
this.adapters.resources.install({ ...params, logger: this.logger }),
@ -106,6 +121,11 @@ export class RuleMigrationsDataService {
]);
}
public async setup(params: SetupParams): Promise<void> {
await this.install(params);
await this.runIndexMigrations(params.esClient);
}
public createClient({ spaceId, currentUser, esScopedClient, dependencies }: CreateClientParams) {
const indexNameProviders: IndexNameProviders = {
rules: this.createIndexNameProvider(this.adapters.rules, spaceId),

View file

@ -9,10 +9,11 @@ import type { FieldMap, SchemaFieldMapKeys } from '@kbn/data-stream-adapter';
import type {
RuleMigration,
RuleMigrationResource,
RuleMigrationRule,
} from '../../../../../common/siem_migrations/model/rule_migration.gen';
import type { SiemMigration, RuleMigrationIntegration, RuleMigrationPrebuiltRule } from '../types';
import type { RuleMigrationIntegration, RuleMigrationPrebuiltRule } from '../types';
export const ruleMigrationsFieldMap: FieldMap<SchemaFieldMapKeys<Omit<RuleMigration, 'id'>>> = {
export const ruleMigrationsFieldMap: FieldMap<SchemaFieldMapKeys<Omit<RuleMigrationRule, 'id'>>> = {
'@timestamp': { type: 'date', required: false },
migration_id: { type: 'keyword', required: true },
created_by: { type: 'keyword', required: true },
@ -93,7 +94,7 @@ export const getPrebuiltRulesFieldMap: ({
mitre_attack_ids: { type: 'keyword', array: true, required: false },
});
export const migrationsFieldMaps: FieldMap<SchemaFieldMapKeys<SiemMigration>> = {
export const migrationsFieldMaps: FieldMap<SchemaFieldMapKeys<Omit<RuleMigration, 'id'>>> = {
created_at: { type: 'date', required: true },
created_by: { type: 'keyword', required: true },
};

View file

@ -0,0 +1,24 @@
/*
* 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 { isNotFoundError } from './utils';
describe('isNotFoundError', () => {
it('should return true if message has key found with value false', () => {
expect(isNotFoundError(new Error('{"key": "value", "found": false}'))).toBe(true);
});
it('should return false for invalid JSON strings', () => {
expect(isNotFoundError(new Error('{key: "value"}'))).toBe(false);
expect(isNotFoundError(new Error('Some Non JSON String'))).toBe(false);
});
it('should return false if message does not have key `found` or it is `true`', () => {
expect(isNotFoundError(new Error('{"message": {key: "value", "found": true}}'))).toBe(false);
expect(isNotFoundError(new Error('{"message": {key: "value"}}'))).toBe(false);
});
});

View file

@ -0,0 +1,24 @@
/*
* 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.
*/
/**
*
* Probes esClient error to see if it is a not found error
*
*/
export const isNotFoundError = (error: Error) => {
try {
const message = JSON.parse(error.message);
if (Object.hasOwn(message, 'found') && message.found === false) {
return true;
}
} catch (e) {
return false;
}
return false;
};

View file

@ -0,0 +1,12 @@
/*
* 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 const RuleMigrationIndexMigrator = jest.fn().mockImplementation(() => {
return {
run: jest.fn(),
};
});

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 './rule_migrations_index_migrator';

View file

@ -0,0 +1,94 @@
/*
* 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 { ElasticsearchClient, Logger } from '@kbn/core/server';
import { RuleMigrationIndexMigrator } from '.';
import * as RuleMigrationSpaceIndexMigratorModule from './rule_migrations_per_space_index_migrator';
import type { Adapters } from '../types';
import { IndexPatternAdapter } from '@kbn/index-adapter';
const rulesIndexName = '.kibana-siem-rule-migrations-rules';
const esClientMock = {
indices: {
get: jest.fn().mockResolvedValue({
'.kibana-siem-rule-migrations-rules-space1': {},
'.kibana-siem-rule-migrations-rules-space2': {},
'.kibana-siem-rule-migrations-rules-space3': {},
}),
},
} as unknown as ElasticsearchClient;
const ruleMigrationIndexAdapters = {
rules: new IndexPatternAdapter(rulesIndexName, {
kibanaVersion: '9.0.0',
}),
} as unknown as Adapters;
const loggerMock = {
info: jest.fn(),
} as unknown as Logger;
const mockPerSpaceIndexMigrator = jest.spyOn(
RuleMigrationSpaceIndexMigratorModule,
'RuleMigrationSpaceIndexMigrator'
);
describe('Index migrator', () => {
beforeEach(() => {
jest.clearAllMocks();
mockPerSpaceIndexMigrator.mockImplementation(
() =>
({
run: jest.fn(),
} as unknown as RuleMigrationSpaceIndexMigratorModule.RuleMigrationSpaceIndexMigrator)
);
});
describe('getSpaceListForMigrations', () => {
it('should return a list of spaces with indices', async () => {
const migrator = new RuleMigrationIndexMigrator(
ruleMigrationIndexAdapters,
esClientMock,
loggerMock
);
await migrator.run();
expect(mockPerSpaceIndexMigrator).toHaveBeenNthCalledWith(
1,
'space1',
esClientMock,
loggerMock,
ruleMigrationIndexAdapters
);
expect(mockPerSpaceIndexMigrator).toHaveBeenNthCalledWith(
2,
'space2',
esClientMock,
loggerMock,
ruleMigrationIndexAdapters
);
expect(mockPerSpaceIndexMigrator).toHaveBeenNthCalledWith(
3,
'space3',
esClientMock,
loggerMock,
ruleMigrationIndexAdapters
);
});
it('should return an empty list if no indices are found', async () => {
(esClientMock.indices.get as jest.Mock).mockResolvedValueOnce({});
const migrator = new RuleMigrationIndexMigrator(
ruleMigrationIndexAdapters,
esClientMock,
loggerMock
);
await migrator.run();
expect(loggerMock.info).toHaveBeenCalledWith('No spaces or index found for index migration');
expect(mockPerSpaceIndexMigrator).not.toHaveBeenCalled();
});
});
});

View file

@ -0,0 +1,53 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { ElasticsearchClient, Logger } from '@kbn/core/server';
import { RuleMigrationSpaceIndexMigrator } from './rule_migrations_per_space_index_migrator';
import type { Adapters } from '../types';
export class RuleMigrationIndexMigrator {
constructor(
private ruleMigrationIndexAdapters: Adapters,
private esClient: ElasticsearchClient,
private logger: Logger
) {}
private async getSpaceListForMigrations() {
const rulesIndicesAcrossSpaces = await this.esClient.indices.get({
index: this.ruleMigrationIndexAdapters.rules.getIndexName('*'),
allow_no_indices: true,
});
const rulesIndexPatternPrefix = this.ruleMigrationIndexAdapters.rules.getIndexName('');
const spaceList = Object.keys(rulesIndicesAcrossSpaces).map((index) => {
return index.replace(rulesIndexPatternPrefix, '');
});
return spaceList;
}
async run() {
const allSpaces = await this.getSpaceListForMigrations();
if (allSpaces.length === 0) {
this.logger.info('No spaces or index found for index migration');
return;
}
this.logger.info(
`Starting index migration for rule migrations for spaces :${allSpaces.join(', ')}`
);
for (const spaceId of allSpaces) {
const migrator = new RuleMigrationSpaceIndexMigrator(
spaceId,
this.esClient,
this.logger,
this.ruleMigrationIndexAdapters
);
await migrator.run();
}
this.logger.info('Finished index migration for rule migrations successfully');
}
}

View file

@ -0,0 +1,140 @@
/*
* 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 { ElasticsearchClient, Logger } from '@kbn/core/server';
import type { Adapters, StoredSiemMigration } from '../types';
import { RuleMigrationSpaceIndexMigrator } from './rule_migrations_per_space_index_migrator';
import type { SearchResponseBody } from 'elasticsearch-8.x/lib/api/types';
const mockRuleIndexAggregationsResult = {
aggregations: {
migrationIds: {
buckets: [
{
key: 'migration1',
createdAt: { value_as_string: '2023-01-01T00:00:00Z' },
createdBy: { buckets: [{ key: 'user1' }] },
},
{
key: 'migration2',
createdAt: { value_as_string: '2023-01-02T00:00:00Z' },
createdBy: { buckets: [{ key: 'user2' }, { key: 'user3' }] },
},
],
},
},
};
const mockMigrationsIndexResult = {
hits: {
hits: [],
},
} as unknown as SearchResponseBody<StoredSiemMigration>;
const getMockedESSearchFunction = (
rulesIndexAggResult: typeof mockRuleIndexAggregationsResult = mockRuleIndexAggregationsResult,
migrationIndexResult: typeof mockMigrationsIndexResult = mockMigrationsIndexResult
) =>
jest.fn((args) => {
if (args.index === '.kibana-siem-rule-migrations-rules-space1') {
return Promise.resolve(rulesIndexAggResult);
} else if (args.index === '.kibana-siem-rule-migrations-migrations-space1') {
return Promise.resolve(migrationIndexResult);
}
return Promise.resolve({ hits: { hits: [] } });
});
const esClientMock = {
search: jest.fn(),
bulk: jest.fn(),
} as unknown as jest.Mocked<ElasticsearchClient>;
const loggerMock = {
info: jest.fn(),
error: jest.fn(),
debug: jest.fn(),
warn: jest.fn(),
} as unknown as Logger;
const ruleMigrationIndexAdapters = {
rules: {
getIndexName: (spaceId: string) => `.kibana-siem-rule-migrations-rules-${spaceId}`,
},
migrations: {
getIndexName: (spaceId: string) => `.kibana-siem-rule-migrations-migrations-${spaceId}`,
getInstalledIndexName: (spaceId: string) =>
`.kibana-siem-rule-migrations-migrations-${spaceId}`,
createIndex: jest.fn(),
},
} as unknown as Adapters;
describe('RuleMigrationSpaceIndexMigrator', () => {
beforeEach(() => {
jest.clearAllMocks();
esClientMock.search.mockImplementation(
getMockedESSearchFunction() as unknown as ElasticsearchClient['search']
);
});
it('should create correct number of documents when nothing exists in Migration index', async () => {
const migrator = new RuleMigrationSpaceIndexMigrator(
'space1',
esClientMock,
loggerMock,
ruleMigrationIndexAdapters
);
await migrator.run();
expect(esClientMock.bulk).toHaveBeenNthCalledWith(1, {
refresh: 'wait_for',
operations: [
{ create: { _id: 'migration1', _index: '.kibana-siem-rule-migrations-migrations-space1' } },
{ id: 'migration1', created_at: '2023-01-01T00:00:00Z', created_by: 'user1' },
{ create: { _id: 'migration2', _index: '.kibana-siem-rule-migrations-migrations-space1' } },
{ id: 'migration2', created_at: '2023-01-02T00:00:00Z', created_by: 'user2' },
],
});
});
it('should create correct number of documents when some exist in Migration index', async () => {
const mockMigrationIndexResultWithOneDocument = {
hits: {
hits: [
{
_id: 'migration1',
_source: {
created_at: '2023-01-01T00:00:00Z',
created_by: 'user1',
},
},
],
},
} as unknown as SearchResponseBody<StoredSiemMigration>;
esClientMock.search.mockImplementation(
getMockedESSearchFunction(
mockRuleIndexAggregationsResult,
mockMigrationIndexResultWithOneDocument
) as unknown as ElasticsearchClient['search']
);
const migrator = new RuleMigrationSpaceIndexMigrator(
'space1',
esClientMock,
loggerMock,
ruleMigrationIndexAdapters
);
await migrator.run();
expect(esClientMock.bulk).toHaveBeenNthCalledWith(1, {
refresh: 'wait_for',
operations: [
{ create: { _id: 'migration2', _index: '.kibana-siem-rule-migrations-migrations-space1' } },
{ id: 'migration2', created_at: '2023-01-02T00:00:00Z', created_by: 'user2' },
],
});
});
});

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 type { ElasticsearchClient, Logger } from '@kbn/core/server';
import assert from 'assert';
import type {
AggregationsAggregationContainer,
AggregationsMinAggregate,
AggregationsStringTermsAggregate,
AggregationsStringTermsBucket,
} from '@elastic/elasticsearch/lib/api/types';
import type { Adapters, StoredSiemMigration } from '../types';
const MAX_ES_SIZE = 10000;
export class RuleMigrationSpaceIndexMigrator {
constructor(
private spaceId: string,
private esClient: ElasticsearchClient,
private logger: Logger,
private ruleMigrationIndexAdapters: Adapters
) {}
private async getExistingMigrationFromRulesIndex() {
const index = this.ruleMigrationIndexAdapters.rules.getIndexName(this.spaceId);
const aggregations: Record<string, AggregationsAggregationContainer> = {
migrationIds: {
terms: { field: 'migration_id', order: { createdAt: 'asc' }, size: MAX_ES_SIZE },
aggregations: {
createdAt: { min: { field: '@timestamp' } },
createdBy: { terms: { field: 'created_by' } },
},
},
};
const result = await this.esClient
.search({ index, aggregations, _source: false })
.catch((error) => {
this.logger.error(`Error getting all rule migrations stats: ${error.message}`);
throw error;
});
const migrationsAgg = result.aggregations?.migrationIds as AggregationsStringTermsAggregate;
const buckets = (migrationsAgg?.buckets as AggregationsStringTermsBucket[]) ?? [];
return buckets.map((bucket) => ({
id: `${bucket.key}`,
created_at: (bucket.createdAt as AggregationsMinAggregate | undefined)
?.value_as_string as string,
created_by: (
(bucket.createdBy as AggregationsStringTermsAggregate)
.buckets as AggregationsStringTermsBucket[]
)[0].key as string,
}));
}
private async getExistingMigrationFromMigrationsIndex() {
const index = this.ruleMigrationIndexAdapters.migrations.getIndexName(this.spaceId);
const result = await this.esClient.search<StoredSiemMigration>({
index,
size: MAX_ES_SIZE,
query: {
match_all: {},
},
_source: true,
});
return result.hits.hits.map(({ _id }) => {
assert(_id, 'document should have _id');
return _id;
});
}
private async indexMigrationDocs(docs: StoredSiemMigration[]) {
const indexName = this.ruleMigrationIndexAdapters.migrations.getIndexName(this.spaceId);
const createOperations = docs.flatMap((doc) => [
{
create: {
_id: doc.id,
_index: indexName,
},
},
{
...doc,
},
]);
return this.esClient.bulk({
refresh: 'wait_for',
operations: createOperations,
});
}
async run() {
await this.migrateRuleMigrationIndex();
}
/**
* Creates the rule migration index if it doesn't exist and indexes any missing migration documents
* from the rules index.
*
*/
private async migrateRuleMigrationIndex() {
const installedIndexName =
await this.ruleMigrationIndexAdapters.migrations.getInstalledIndexName(this.spaceId);
if (!installedIndexName) {
await this.ruleMigrationIndexAdapters.migrations.createIndex(this.spaceId);
}
const existingMigrationsFromRulesIndex = await this.getExistingMigrationFromRulesIndex();
const existingMigrationsFromMigrationsIndex =
await this.getExistingMigrationFromMigrationsIndex();
const migrationsToIndex = existingMigrationsFromRulesIndex.filter(
(migration) => !existingMigrationsFromMigrationsIndex.some((id) => id === migration.id)
);
if (migrationsToIndex.length > 0) {
this.logger.info(
`Found ${migrationsToIndex.length} rule migration documents from rules index with an absent migration doc. Creating corresponding migration documents.`
);
await this.indexMigrationDocs(migrationsToIndex);
this.logger.info(
`Created ${migrationsToIndex.length} rule migration documents from rules index with an absent migration doc.`
);
}
}
}

View file

@ -17,7 +17,7 @@ import {
import { Subject } from 'rxjs';
import {
MockRuleMigrationsDataService,
mockInstall,
mockSetup,
mockCreateClient as mockDataCreateClient,
} from './data/__mocks__/mocks';
import { mockCreateClient as mockTaskCreateClient, mockStopAll } from './task/__mocks__/mocks';
@ -52,7 +52,7 @@ describe('SiemRuleMigrationsService', () => {
it('should set esClusterClient and call dataStreamAdapter.install', () => {
ruleMigrationsService.setup({ esClusterClient, pluginStop$ });
expect(mockInstall).toHaveBeenCalledWith({
expect(mockSetup).toHaveBeenCalledWith({
esClient: esClusterClient.asInternalUser,
pluginStop$,
});
@ -60,7 +60,7 @@ describe('SiemRuleMigrationsService', () => {
it('should log error when data installation fails', async () => {
const error = 'Failed to install';
mockInstall.mockRejectedValueOnce(error);
mockSetup.mockRejectedValueOnce(error);
ruleMigrationsService.setup({ esClusterClient, pluginStop$ });
await waitFor(() => {

View file

@ -54,7 +54,7 @@ export class SiemRuleMigrationsService {
this.esClusterClient = esClusterClient;
const esClient = esClusterClient.asInternalUser;
this.dataService.install({ ...params, esClient }).catch((err) => {
this.dataService.setup({ ...params, esClient }).catch((err) => {
this.logger.error('Error installing data service.', err);
});
}

View file

@ -11,7 +11,7 @@ import type { RuleTranslationResult } from '../../../../../../common/siem_migrat
import type {
ElasticRulePartial,
OriginalRule,
RuleMigration,
RuleMigrationRule,
} from '../../../../../../common/siem_migrations/model/rule_migration.gen';
import type { RuleMigrationResources } from '../retrievers/rule_resource_retriever';
@ -30,7 +30,7 @@ export const migrateRuleState = Annotation.Root({
default: () => '',
}),
translation_result: Annotation<RuleTranslationResult>(),
comments: Annotation<RuleMigration['comments']>({
comments: Annotation<RuleMigrationRule['comments']>({
// Translation subgraph causes the original main graph comments to be concatenated again, we need to deduplicate them.
reducer: (current, value) => uniq(value ? (current ?? []).concat(value) : current),
default: () => [],

View file

@ -10,7 +10,7 @@ import { RuleTranslationResult } from '../../../../../../../../common/siem_migra
import type {
ElasticRulePartial,
OriginalRule,
RuleMigration,
RuleMigrationRule,
} from '../../../../../../../../common/siem_migrations/model/rule_migration.gen';
import type { RuleMigrationResources } from '../../../retrievers/rule_resource_retriever';
import type { RuleMigrationIntegration } from '../../../../types';
@ -47,7 +47,7 @@ export const translateRuleState = Annotation.Root({
reducer: (current, value) => value ?? current,
default: () => RuleTranslationResult.UNTRANSLATABLE,
}),
comments: Annotation<RuleMigration['comments']>({
comments: Annotation<RuleMigrationRule['comments']>({
reducer: (current, value) => (value ? (current ?? []).concat(value) : current),
default: () => [],
}),

View file

@ -6,16 +6,16 @@
*/
import { RuleResourceRetriever } from './rule_resource_retriever'; // Adjust path as needed
import type { RuleMigration } from '../../../../../../common/siem_migrations/model/rule_migration.gen';
import { ResourceIdentifier } from '../../../../../../common/siem_migrations/rules/resources';
import type { RuleMigrationsDataClient } from '../../data/rule_migrations_data_client';
import type { RuleMigrationRule } from '../../../../../../common/siem_migrations/model/rule_migration.gen';
jest.mock('../../data/rule_migrations_data_service');
jest.mock('../../../../../../common/siem_migrations/rules/resources');
const MockResourceIdentifier = ResourceIdentifier as jest.Mock;
const migration = { original_rule: { vendor: 'splunk' } } as unknown as RuleMigration;
const migration = { original_rule: { vendor: 'splunk' } } as unknown as RuleMigrationRule;
describe('RuleResourceRetriever', () => {
let retriever: RuleResourceRetriever;
@ -25,7 +25,7 @@ describe('RuleResourceRetriever', () => {
beforeEach(() => {
mockDataClient = {
resources: { searchBatches: jest.fn().mockReturnValue({ next: jest.fn(() => []) }) },
} as unknown as RuleMigrationsDataClient;
} as unknown as jest.Mocked<RuleMigrationsDataClient>;
retriever = new RuleResourceRetriever('mockMigrationId', mockDataClient);

View file

@ -7,9 +7,9 @@
import { ResourceIdentifier } from '../../../../../../common/siem_migrations/rules/resources';
import type {
RuleMigration,
RuleMigrationResource,
RuleMigrationResourceType,
RuleMigrationRule,
} from '../../../../../../common/siem_migrations/model/rule_migration.gen';
import type { RuleMigrationsDataClient } from '../../data/rule_migrations_data_client';
@ -54,8 +54,8 @@ export class RuleResourceRetriever {
this.existingResources = existingRuleResources;
}
public async getResources(ruleMigration: RuleMigration): Promise<RuleMigrationResources> {
const originalRule = ruleMigration.original_rule;
public async getResources(migrationRule: RuleMigrationRule): Promise<RuleMigrationResources> {
const originalRule = migrationRule.original_rule;
const existingResources = this.existingResources;
if (!existingResources) {
throw new Error('initialize must be called before calling getResources');

View file

@ -198,4 +198,9 @@ export class RuleMigrationsTaskClient {
invocationConfig,
});
}
/** Returns if a migration is running or not */
isMigrationRunning(migrationId: string): boolean {
return this.migrationsRunning.has(migrationId);
}
}

View file

@ -14,23 +14,16 @@ import type { InferenceClient } from '@kbn/inference-plugin/server';
import type { IndexAdapter, IndexPatternAdapter } from '@kbn/index-adapter';
import type {
RuleMigration,
RuleMigrationRule,
RuleMigrationTranslationResult,
UpdateRuleMigrationData,
UpdateRuleMigrationRule,
} from '../../../../common/siem_migrations/model/rule_migration.gen';
import { type RuleMigrationResource } from '../../../../common/siem_migrations/model/rule_migration.gen';
import type { RuleVersions } from './data/rule_migrations_data_prebuilt_rules_client';
import type { Stored } from '../types';
export type Stored<T extends object> = T & { id: string };
export interface SiemMigration {
/** The moment the migration was created */
created_at: string;
/** The profile id of the user who created the migration */
created_by: string;
}
export type StoredSiemMigration = Stored<SiemMigration>;
export type StoredRuleMigration = Stored<RuleMigration>;
export type StoredSiemMigration = Stored<RuleMigration>;
export type StoredRuleMigration = Stored<RuleMigrationRule>;
export type StoredRuleMigrationResource = Stored<RuleMigrationResource>;
export interface SiemRuleMigrationsClientDependencies {
@ -60,7 +53,7 @@ export interface RuleMigrationPrebuiltRule {
export type RuleSemanticSearchResult = RuleMigrationPrebuiltRule & RuleVersions;
export type InternalUpdateRuleMigrationData = UpdateRuleMigrationData & {
export type InternalUpdateRuleMigrationRule = UpdateRuleMigrationRule & {
translation_result?: RuleMigrationTranslationResult;
};

View file

@ -11,3 +11,5 @@ export interface SiemMigrationsSetupParams {
esClusterClient: IClusterClient;
tasksTimeoutMs?: number;
}
export type Stored<T extends object> = T & { id: string };

View file

@ -30,8 +30,8 @@ import { CreateAssetCriticalityRecordRequestBodyInput } from '@kbn/security-solu
import { CreatePrivMonUserRequestBodyInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/privilege_monitoring/users/create.gen';
import { CreateRuleRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_management/crud/create_rule/create_rule_route.gen';
import {
CreateRuleMigrationRequestParamsInput,
CreateRuleMigrationRequestBodyInput,
CreateRuleMigrationRulesRequestParamsInput,
CreateRuleMigrationRulesRequestBodyInput,
} from '@kbn/security-solution-plugin/common/siem_migrations/model/api/rules/rule_migration.gen';
import { CreateTimelinesRequestBodyInput } from '@kbn/security-solution-plugin/common/api/timeline/create_timelines/create_timelines_route.gen';
import {
@ -46,6 +46,7 @@ import {
import { DeleteNoteRequestBodyInput } from '@kbn/security-solution-plugin/common/api/timeline/delete_note/delete_note_route.gen';
import { DeletePrivMonUserRequestParamsInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/privilege_monitoring/users/delete.gen';
import { DeleteRuleRequestQueryInput } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_management/crud/delete_rule/delete_rule_route.gen';
import { DeleteRuleMigrationRequestParamsInput } from '@kbn/security-solution-plugin/common/siem_migrations/model/api/rules/rule_migration.gen';
import { DeleteTimelinesRequestBodyInput } from '@kbn/security-solution-plugin/common/api/timeline/delete_timelines/delete_timelines_route.gen';
import { DeprecatedTriggerRiskScoreCalculationRequestBodyInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/risk_engine/entity_calculation_route.gen';
import { EndpointExecuteActionRequestBodyInput } from '@kbn/security-solution-plugin/common/api/endpoint/actions/response_actions/execute/execute.gen';
@ -93,16 +94,17 @@ import {
GetRuleExecutionResultsRequestQueryInput,
GetRuleExecutionResultsRequestParamsInput,
} from '@kbn/security-solution-plugin/common/api/detection_engine/rule_monitoring/rule_execution_logs/get_rule_execution_results/get_rule_execution_results_route.gen';
import {
GetRuleMigrationRequestQueryInput,
GetRuleMigrationRequestParamsInput,
} from '@kbn/security-solution-plugin/common/siem_migrations/model/api/rules/rule_migration.gen';
import { GetRuleMigrationRequestParamsInput } from '@kbn/security-solution-plugin/common/siem_migrations/model/api/rules/rule_migration.gen';
import { GetRuleMigrationPrebuiltRulesRequestParamsInput } from '@kbn/security-solution-plugin/common/siem_migrations/model/api/rules/rule_migration.gen';
import {
GetRuleMigrationResourcesRequestQueryInput,
GetRuleMigrationResourcesRequestParamsInput,
} from '@kbn/security-solution-plugin/common/siem_migrations/model/api/rules/rule_migration.gen';
import { GetRuleMigrationResourcesMissingRequestParamsInput } from '@kbn/security-solution-plugin/common/siem_migrations/model/api/rules/rule_migration.gen';
import {
GetRuleMigrationRulesRequestQueryInput,
GetRuleMigrationRulesRequestParamsInput,
} from '@kbn/security-solution-plugin/common/siem_migrations/model/api/rules/rule_migration.gen';
import { GetRuleMigrationStatsRequestParamsInput } from '@kbn/security-solution-plugin/common/siem_migrations/model/api/rules/rule_migration.gen';
import { GetRuleMigrationTranslationStatsRequestParamsInput } from '@kbn/security-solution-plugin/common/siem_migrations/model/api/rules/rule_migration.gen';
import { GetTimelineRequestQueryInput } from '@kbn/security-solution-plugin/common/api/timeline/get_timeline/get_timeline_route.gen';
@ -159,9 +161,10 @@ import {
UpdatePrivMonUserRequestBodyInput,
} from '@kbn/security-solution-plugin/common/api/entity_analytics/privilege_monitoring/users/update.gen';
import { UpdateRuleRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_management/crud/update_rule/update_rule_route.gen';
import { UpdateRuleMigrationRequestParamsInput } from '@kbn/security-solution-plugin/common/siem_migrations/model/api/rules/rule_migration.gen';
import {
UpdateRuleMigrationRequestParamsInput,
UpdateRuleMigrationRequestBodyInput,
UpdateRuleMigrationRulesRequestParamsInput,
UpdateRuleMigrationRulesRequestBodyInput,
} from '@kbn/security-solution-plugin/common/siem_migrations/model/api/rules/rule_migration.gen';
import {
UpdateWorkflowInsightRequestParamsInput,
@ -414,13 +417,26 @@ For detailed information on Kibana actions and alerting, and additional API call
.send(props.body as object);
},
/**
* Creates a new SIEM rules migration using the original vendor rules provided
* Creates a new rule migration and returns the corresponding migration_id
*/
createRuleMigration(props: CreateRuleMigrationProps, kibanaSpace: string = 'default') {
createRuleMigration(kibanaSpace: string = 'default') {
return supertest
.put(routeWithNamespace('/internal/siem_migrations/rules', kibanaSpace))
.set('kbn-xsrf', 'true')
.set(ELASTIC_HTTP_VERSION_HEADER, '1')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana');
},
/**
* Adds original vendor rules to an already existing migration. Can be called multiple times to add more rules
*/
createRuleMigrationRules(
props: CreateRuleMigrationRulesProps,
kibanaSpace: string = 'default'
) {
return supertest
.post(
routeWithNamespace(
replaceParams('/internal/siem_migrations/rules/{migration_id}', props.params),
replaceParams('/internal/siem_migrations/rules/{migration_id}/rules', props.params),
kibanaSpace
)
)
@ -535,6 +551,21 @@ The difference between the `id` and `rule_id` is that the `id` is a unique rule
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.query(props.query);
},
/**
* Deletes a rule migration document stored in the system given the rule migration id
*/
deleteRuleMigration(props: DeleteRuleMigrationProps, kibanaSpace: string = 'default') {
return supertest
.delete(
routeWithNamespace(
replaceParams('/internal/siem_migrations/rules/{migration_id}', props.params),
kibanaSpace
)
)
.set('kbn-xsrf', 'true')
.set(ELASTIC_HTTP_VERSION_HEADER, '1')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana');
},
/**
* Delete one or more Timelines or Timeline templates.
*/
@ -1018,7 +1049,7 @@ finalize it.
.query(props.query);
},
/**
* Retrieves the rule documents stored in the system given the rule migration id
* Retrieves the rule migration document stored in the system given the rule migration id
*/
getRuleMigration(props: GetRuleMigrationProps, kibanaSpace: string = 'default') {
return supertest
@ -1030,8 +1061,7 @@ finalize it.
)
.set('kbn-xsrf', 'true')
.set(ELASTIC_HTTP_VERSION_HEADER, '1')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.query(props.query);
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana');
},
/**
* Retrieves all related integrations
@ -1114,6 +1144,22 @@ finalize it.
.set(ELASTIC_HTTP_VERSION_HEADER, '1')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana');
},
/**
* Retrieves the the list of rules included in a migration given the migration id
*/
getRuleMigrationRules(props: GetRuleMigrationRulesProps, kibanaSpace: string = 'default') {
return supertest
.get(
routeWithNamespace(
replaceParams('/internal/siem_migrations/rules/{migration_id}/rules', props.params),
kibanaSpace
)
)
.set('kbn-xsrf', 'true')
.set(ELASTIC_HTTP_VERSION_HEADER, '1')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.query(props.query);
},
/**
* Retrieves the stats of a SIEM rules migration using the migration id provided
*/
@ -1644,7 +1690,7 @@ The difference between the `id` and `rule_id` is that the `id` is a unique rule
*/
startRuleMigration(props: StartRuleMigrationProps, kibanaSpace: string = 'default') {
return supertest
.put(
.post(
routeWithNamespace(
replaceParams('/internal/siem_migrations/rules/{migration_id}/start', props.params),
kibanaSpace
@ -1672,7 +1718,7 @@ The difference between the `id` and `rule_id` is that the `id` is a unique rule
*/
stopRuleMigration(props: StopRuleMigrationProps, kibanaSpace: string = 'default') {
return supertest
.put(
.post(
routeWithNamespace(
replaceParams('/internal/siem_migrations/rules/{migration_id}/stop', props.params),
kibanaSpace
@ -1739,11 +1785,11 @@ The difference between the `id` and `rule_id` is that the `id` is a unique rule
.send(props.body as object);
},
/**
* Updates rules migrations attributes
* Updates rules migrations data
*/
updateRuleMigration(props: UpdateRuleMigrationProps, kibanaSpace: string = 'default') {
return supertest
.put(
.patch(
routeWithNamespace(
replaceParams('/internal/siem_migrations/rules/{migration_id}', props.params),
kibanaSpace
@ -1751,6 +1797,24 @@ The difference between the `id` and `rule_id` is that the `id` is a unique rule
)
.set('kbn-xsrf', 'true')
.set(ELASTIC_HTTP_VERSION_HEADER, '1')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana');
},
/**
* Updates rules migrations attributes
*/
updateRuleMigrationRules(
props: UpdateRuleMigrationRulesProps,
kibanaSpace: string = 'default'
) {
return supertest
.patch(
routeWithNamespace(
replaceParams('/internal/siem_migrations/rules/{migration_id}/rules', props.params),
kibanaSpace
)
)
.set('kbn-xsrf', 'true')
.set(ELASTIC_HTTP_VERSION_HEADER, '1')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send(props.body as object);
},
@ -1823,9 +1887,9 @@ export interface CreatePrivMonUserProps {
export interface CreateRuleProps {
body: CreateRuleRequestBodyInput;
}
export interface CreateRuleMigrationProps {
params: CreateRuleMigrationRequestParamsInput;
body: CreateRuleMigrationRequestBodyInput;
export interface CreateRuleMigrationRulesProps {
params: CreateRuleMigrationRulesRequestParamsInput;
body: CreateRuleMigrationRulesRequestBodyInput;
}
export interface CreateTimelinesProps {
body: CreateTimelinesRequestBodyInput;
@ -1850,6 +1914,9 @@ export interface DeletePrivMonUserProps {
export interface DeleteRuleProps {
query: DeleteRuleRequestQueryInput;
}
export interface DeleteRuleMigrationProps {
params: DeleteRuleMigrationRequestParamsInput;
}
export interface DeleteTimelinesProps {
body: DeleteTimelinesRequestBodyInput;
}
@ -1952,7 +2019,6 @@ export interface GetRuleExecutionResultsProps {
params: GetRuleExecutionResultsRequestParamsInput;
}
export interface GetRuleMigrationProps {
query: GetRuleMigrationRequestQueryInput;
params: GetRuleMigrationRequestParamsInput;
}
export interface GetRuleMigrationPrebuiltRulesProps {
@ -1965,6 +2031,10 @@ export interface GetRuleMigrationResourcesProps {
export interface GetRuleMigrationResourcesMissingProps {
params: GetRuleMigrationResourcesMissingRequestParamsInput;
}
export interface GetRuleMigrationRulesProps {
query: GetRuleMigrationRulesRequestQueryInput;
params: GetRuleMigrationRulesRequestParamsInput;
}
export interface GetRuleMigrationStatsProps {
params: GetRuleMigrationStatsRequestParamsInput;
}
@ -2087,7 +2157,10 @@ export interface UpdateRuleProps {
}
export interface UpdateRuleMigrationProps {
params: UpdateRuleMigrationRequestParamsInput;
body: UpdateRuleMigrationRequestBodyInput;
}
export interface UpdateRuleMigrationRulesProps {
params: UpdateRuleMigrationRulesRequestParamsInput;
body: UpdateRuleMigrationRulesRequestBodyInput;
}
export interface UpdateWorkflowInsightProps {
params: UpdateWorkflowInsightRequestParamsInput;

View file

@ -6,192 +6,35 @@
*/
import expect from 'expect';
import { v4 as uuidv4 } from 'uuid';
import { SiemMigrationStatus } from '@kbn/security-solution-plugin/common/siem_migrations/constants';
import {
defaultOriginalRule,
deleteAllMigrationRules,
migrationResourcesRouteHelpersFactory,
migrationRulesRouteHelpersFactory,
splunkRuleWithResources,
} from '../../utils';
import { deleteAllRuleMigrations, ruleMigrationRouteHelpersFactory } from '../../utils';
import { FtrProviderContext } from '../../../../ftr_provider_context';
export default ({ getService }: FtrProviderContext) => {
const es = getService('es');
const supertest = getService('supertest');
const migrationRulesRoutes = migrationRulesRouteHelpersFactory(supertest);
const migrationResourcesRoutes = migrationResourcesRouteHelpersFactory(supertest);
const ruleMigrationRoutes = ruleMigrationRouteHelpersFactory(supertest);
describe('@ess @serverless @serverlessQA Create API', () => {
beforeEach(async () => {
await deleteAllMigrationRules(es);
await deleteAllRuleMigrations(es);
});
describe('Happy path', () => {
it('should create migrations with provided id', async () => {
const migrationId = uuidv4();
await migrationRulesRoutes.create({ migrationId, payload: [defaultOriginalRule] });
// fetch migration rule
const response = await migrationRulesRoutes.get({ migrationId });
expect(response.body.total).toEqual(1);
const migrationRule = response.body.data[0];
expect(migrationRule).toEqual(
expect.objectContaining({
migration_id: migrationId,
original_rule: defaultOriginalRule,
status: SiemMigrationStatus.PENDING,
})
);
});
it('should create migrations without provided id', async () => {
it('should create migrations without any issues', async () => {
const {
body: { migration_id: migrationId },
} = await migrationRulesRoutes.create({ payload: [defaultOriginalRule] });
} = await ruleMigrationRoutes.create({});
// fetch migration rule
const response = await migrationRulesRoutes.get({ migrationId });
expect(response.body.total).toEqual(1);
expect(migrationId).not.toBeNull();
const migrationRule = response.body.data[0];
expect(migrationRule).toEqual(
expect.objectContaining({
migration_id: migrationId,
original_rule: defaultOriginalRule,
status: SiemMigrationStatus.PENDING,
})
);
});
it('should create migrations with the rules that have resources', async () => {
const migrationId = uuidv4();
await migrationRulesRoutes.create({ migrationId, payload: [splunkRuleWithResources] });
// fetch migration rule
const response = await migrationRulesRoutes.get({ migrationId });
expect(response.body.total).toEqual(1);
const migrationRule = response.body.data[0];
expect(migrationRule).toEqual(
expect.objectContaining({
migration_id: migrationId,
original_rule: splunkRuleWithResources,
status: SiemMigrationStatus.PENDING,
})
);
// fetch missing resources
const resourcesResponse = await migrationResourcesRoutes.getMissingResources({
const {
body: { id, created_by: createdBy },
} = await ruleMigrationRoutes.get({
migrationId,
});
expect(resourcesResponse.body).toEqual([
{ type: 'macro', name: 'summariesonly' },
{ type: 'macro', name: 'drop_dm_object_name(1)' },
{ type: 'lookup', name: 'malware_tracker' },
]);
});
});
describe('Error handling', () => {
it('should return no content error', async () => {
const migrationId = uuidv4();
await migrationRulesRoutes.create({ migrationId, payload: [], expectStatusCode: 204 });
// fetch migration rule
const response = await migrationRulesRoutes.get({ migrationId });
expect(response.body.total).toEqual(0);
});
it(`should return an error when undefined payload has been passed`, async () => {
const migrationId = uuidv4();
const response = await migrationRulesRoutes.create({ migrationId, expectStatusCode: 400 });
expect(response.body).toEqual({
error: 'Bad Request',
message: '[request body]: Expected array, received null',
statusCode: 400,
});
});
it('should return an error when original rule id is not specified', async () => {
const { id, ...restOfOriginalRule } = defaultOriginalRule;
const response = await migrationRulesRoutes.create({
payload: [restOfOriginalRule],
expectStatusCode: 400,
});
expect(response.body).toEqual({
statusCode: 400,
error: 'Bad Request',
message: '[request body]: 0.id: Required',
});
});
it('should return an error when original rule vendor is not specified', async () => {
const { vendor, ...restOfOriginalRule } = defaultOriginalRule;
const response = await migrationRulesRoutes.create({
payload: [restOfOriginalRule],
expectStatusCode: 400,
});
expect(response.body).toEqual({
statusCode: 400,
error: 'Bad Request',
message: '[request body]: 0.vendor: Invalid literal value, expected "splunk"',
});
});
it('should return an error when original rule title is not specified', async () => {
const { title, ...restOfOriginalRule } = defaultOriginalRule;
const response = await migrationRulesRoutes.create({
payload: [restOfOriginalRule],
expectStatusCode: 400,
});
expect(response.body).toEqual({
statusCode: 400,
error: 'Bad Request',
message: '[request body]: 0.title: Required',
});
});
it('should return an error when original rule description is not specified', async () => {
const { description, ...restOfOriginalRule } = defaultOriginalRule;
const response = await migrationRulesRoutes.create({
payload: [restOfOriginalRule],
expectStatusCode: 400,
});
expect(response.body).toEqual({
statusCode: 400,
error: 'Bad Request',
message: '[request body]: 0.description: Required',
});
});
it('should return an error when original rule query is not specified', async () => {
const { query, ...restOfOriginalRule } = defaultOriginalRule;
const response = await migrationRulesRoutes.create({
payload: [restOfOriginalRule],
expectStatusCode: 400,
});
expect(response.body).toEqual({
statusCode: 400,
error: 'Bad Request',
message: '[request body]: 0.query: Required',
});
});
it('should return an error when original rule query_language is not specified', async () => {
const { query_language: _, ...restOfOriginalRule } = defaultOriginalRule;
const response = await migrationRulesRoutes.create({
payload: [restOfOriginalRule],
expectStatusCode: 400,
});
expect(response.body).toEqual({
statusCode: 400,
error: 'Bad Request',
message: '[request body]: 0.query_language: Required',
});
expect(id).toBe(migrationId);
expect(createdBy).not.toBeNull();
});
});
});

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.
*/
import expect from 'expect';
import {
createLookupsForMigrationId,
createMacrosForMigrationId,
deleteAllRuleMigrations,
ruleMigrationRouteHelpersFactory,
splunkRuleWithResources,
} from '../../utils';
import { FtrProviderContext } from '../../../../ftr_provider_context';
import {
getResoucesPerMigrationFromES,
getRuleMigrationFromES,
getRulesPerMigrationFromES,
} from '../../utils/es_queries';
export default ({ getService }: FtrProviderContext) => {
const es = getService('es');
const supertest = getService('supertest');
const ruleMigrationRoutes = ruleMigrationRouteHelpersFactory(supertest);
describe('@ess @serverless @serverlessQA Delete API', () => {
let migrationId: string;
beforeEach(async () => {
await deleteAllRuleMigrations(es);
const response = await ruleMigrationRoutes.create({});
migrationId = response.body.migration_id;
});
describe('Happy path', () => {
it('should delete existing migration without any issues', async () => {
await ruleMigrationRoutes.delete({
migrationId,
expectStatusCode: 200,
});
await ruleMigrationRoutes.get({
migrationId,
expectStatusCode: 404,
});
});
it('should delete migrations, rules and resources associated with the migration', async () => {
await ruleMigrationRoutes.addRulesToMigration({
migrationId,
/** adding bulk rules so that deletion of all rules can be tested */
payload: Array.from({ length: 40 }, () => splunkRuleWithResources),
});
await createMacrosForMigrationId({
es,
migrationId,
count: 40,
});
await createLookupsForMigrationId({
es,
migrationId,
count: 40,
});
let migrationsFromES = await getRuleMigrationFromES({
es,
migrationId,
});
expect(migrationsFromES.hits.hits).toHaveLength(1);
let rulesFromES = await getRulesPerMigrationFromES({
es,
migrationId,
});
expect(rulesFromES.hits.hits).toHaveLength(40);
let resourcesFromES = await getResoucesPerMigrationFromES({
es,
migrationId,
});
expect(resourcesFromES.hits.hits).toHaveLength(83);
await ruleMigrationRoutes.delete({
migrationId,
expectStatusCode: 200,
});
rulesFromES = await getRulesPerMigrationFromES({
es,
migrationId,
});
expect(rulesFromES.hits.hits).toHaveLength(0);
migrationsFromES = await getRuleMigrationFromES({
es,
migrationId,
});
expect(migrationsFromES.hits.hits).toHaveLength(0);
resourcesFromES = await getResoucesPerMigrationFromES({
es,
migrationId,
});
expect(resourcesFromES.hits.hits).toHaveLength(0);
});
describe('Error handling', () => {
it('should return 409 if migration is already running', async () => {
// start a migration
await ruleMigrationRoutes.addRulesToMigration({
migrationId,
payload: [splunkRuleWithResources],
});
const response = await ruleMigrationRoutes.start({
migrationId,
payload: {
connector_id: 'preconfigured-bedrock',
},
});
expect(response.body).toMatchObject({ started: true });
const deleteResponse = await ruleMigrationRoutes.delete({
migrationId,
expectStatusCode: 409,
});
expect(deleteResponse.body).toMatchObject({
statusCode: 409,
error: 'Conflict',
message:
'A running migration cannot be deleted. Please stop the migration first and try again',
});
});
it('should return 404 if migration ID does not exist', async () => {
const { body } = await ruleMigrationRoutes.delete({
migrationId: 'non-existing-migration-id',
expectStatusCode: 404,
});
expect(body).toMatchObject({
statusCode: 404,
error: 'Not Found',
message: 'No Migration found with id: non-existing-migration-id',
});
});
});
});
});
};

View file

@ -6,665 +6,42 @@
*/
import expect from 'expect';
import { v4 as uuidv4 } from 'uuid';
import {
RuleTranslationResult,
SiemMigrationStatus,
} from '@kbn/security-solution-plugin/common/siem_migrations/constants';
import {
RuleMigrationDocument,
createMigrationRules,
defaultElasticRule,
defaultOriginalRule,
deleteAllMigrationRules,
getMigrationRuleDocument,
getMigrationRuleDocuments,
migrationRulesRouteHelpersFactory,
} from '../../utils';
import { deleteAllRuleMigrations, ruleMigrationRouteHelpersFactory } from '../../utils';
import { FtrProviderContext } from '../../../../ftr_provider_context';
export default ({ getService }: FtrProviderContext) => {
const es = getService('es');
const supertest = getService('supertest');
const migrationRulesRoutes = migrationRulesRouteHelpersFactory(supertest);
const ruleMigrationRoutes = ruleMigrationRouteHelpersFactory(supertest);
describe('@ess @serverless @serverlessQA Get API', () => {
let migrationId: string;
beforeEach(async () => {
await deleteAllMigrationRules(es);
await deleteAllRuleMigrations(es);
const creationResponse = await ruleMigrationRoutes.create({});
migrationId = creationResponse.body.migration_id;
});
describe('Basic', () => {
it('should fetch existing rules within specified migration', async () => {
// create a document
const migrationId = uuidv4();
const migrationRuleDocument = getMigrationRuleDocument({ migration_id: migrationId });
await createMigrationRules(es, [migrationRuleDocument]);
const { '@timestamp': timestamp, updated_at: updatedAt, ...rest } = migrationRuleDocument;
// fetch migration rule
const response = await migrationRulesRoutes.get({ migrationId });
expect(response.body.total).toEqual(1);
expect(response.body.data).toEqual(expect.arrayContaining([expect.objectContaining(rest)]));
});
});
describe('Filtering', () => {
it('should fetch rules filtered by `searchTerm`', async () => {
// create a document
const migrationId = uuidv4();
const overrideCallback = (index: number): Partial<RuleMigrationDocument> => {
const title = `${index < 5 ? 'Elastic' : 'Splunk'} rule - ${index}`;
const originalRule = { ...defaultOriginalRule, title };
const elasticRule = { ...defaultElasticRule, title };
return {
migration_id: migrationId,
original_rule: originalRule,
elastic_rule: elasticRule,
};
};
const migrationRuleDocuments = getMigrationRuleDocuments(10, overrideCallback);
await createMigrationRules(es, migrationRuleDocuments);
// Search by word `Elastic`
let expectedRuleDocuments = expect.arrayContaining(
migrationRuleDocuments
.slice(0, 5)
.map(({ '@timestamp': timestamp, updated_at: updatedAt, ...rest }) =>
expect.objectContaining(rest)
)
);
// fetch migration rules
let response = await migrationRulesRoutes.get({
it('should fetch existing migration', async () => {
const migrationResponse = await ruleMigrationRoutes.get({
migrationId,
queryParams: { search_term: 'Elastic' },
});
expect(response.body.total).toEqual(5);
expect(response.body.data).toEqual(expectedRuleDocuments);
// Search by word `Splunk`
expectedRuleDocuments = expect.arrayContaining(
migrationRuleDocuments
.slice(5)
.map(({ '@timestamp': timestamp, updated_at: updatedAt, ...rest }) =>
expect.objectContaining(rest)
)
);
// fetch migration rules
response = await migrationRulesRoutes.get({
migrationId,
queryParams: { search_term: 'Splunk' },
});
expect(response.body.total).toEqual(5);
expect(response.body.data).toEqual(expectedRuleDocuments);
});
it('should fetch rules filtered by `ids`', async () => {
// create a document
const migrationId = uuidv4();
const migrationRuleDocuments = getMigrationRuleDocuments(10, () => ({
migration_id: migrationId,
}));
const createdDocumentIds = await createMigrationRules(es, migrationRuleDocuments);
const expectedIds = createdDocumentIds.slice(0, 3).sort();
// fetch migration rules by existing ids
let response = await migrationRulesRoutes.get({
migrationId,
queryParams: { ids: expectedIds },
});
expect(response.body.total).toEqual(3);
expect(response.body.data.map(({ id }) => id).sort()).toEqual(expectedIds);
// fetch migration rules by non-existing id
response = await migrationRulesRoutes.get({
migrationId,
queryParams: { ids: [uuidv4()] },
});
expect(response.body.total).toEqual(0);
expect(migrationResponse.body.id).toBe(migrationId);
});
it('should fetch rules filtered by `prebuilt`', async () => {
// create a document
const migrationId = uuidv4();
const overrideCallback = (index: number): Partial<RuleMigrationDocument> => {
const prebuiltRuleId = index < 3 ? uuidv4() : undefined;
const elasticRule = { ...defaultElasticRule, prebuilt_rule_id: prebuiltRuleId };
return {
migration_id: migrationId,
elastic_rule: elasticRule,
};
};
const migrationRuleDocuments = getMigrationRuleDocuments(10, overrideCallback);
await createMigrationRules(es, migrationRuleDocuments);
// fetch migration rules matched Elastic prebuilt rules
let response = await migrationRulesRoutes.get({
migrationId,
queryParams: { is_prebuilt: true },
});
expect(response.body.total).toEqual(3);
// fetch custom translated migration rules
response = await migrationRulesRoutes.get({
migrationId,
queryParams: { is_prebuilt: false },
});
expect(response.body.total).toEqual(7);
describe('Error handling', () => {
it('should return 404 if migration ID does not exist', async () => {
const { body } = await ruleMigrationRoutes.get({
migrationId: 'non-existing-migration-id',
expectStatusCode: 404,
});
it('should fetch rules filtered by `installed`', async () => {
// create a document
const migrationId = uuidv4();
const overrideCallback = (index: number): Partial<RuleMigrationDocument> => {
const installedRuleId = index < 2 ? uuidv4() : undefined;
const elasticRule = { ...defaultElasticRule, id: installedRuleId };
return {
migration_id: migrationId,
elastic_rule: elasticRule,
};
};
const migrationRuleDocuments = getMigrationRuleDocuments(10, overrideCallback);
await createMigrationRules(es, migrationRuleDocuments);
// fetch installed migration rules
let response = await migrationRulesRoutes.get({
migrationId,
queryParams: { is_installed: true },
expect(body).toMatchObject({
statusCode: 404,
error: 'Not Found',
message: 'No Migration found with id: non-existing-migration-id',
});
expect(response.body.total).toEqual(2);
// fetch non-installed migration rules
response = await migrationRulesRoutes.get({
migrationId,
queryParams: { is_installed: false },
});
expect(response.body.total).toEqual(8);
});
it('should fetch rules filtered by `failed`', async () => {
// create a document
const migrationId = uuidv4();
const overrideCallback = (index: number): Partial<RuleMigrationDocument> => {
const status = index < 4 ? SiemMigrationStatus.FAILED : SiemMigrationStatus.COMPLETED;
return {
migration_id: migrationId,
status,
};
};
const migrationRuleDocuments = getMigrationRuleDocuments(10, overrideCallback);
await createMigrationRules(es, migrationRuleDocuments);
// fetch failed migration rules
let response = await migrationRulesRoutes.get({
migrationId,
queryParams: { is_failed: true },
});
expect(response.body.total).toEqual(4);
// fetch non-failed migration rules
response = await migrationRulesRoutes.get({
migrationId,
queryParams: { is_failed: false },
});
expect(response.body.total).toEqual(6);
});
it('should fetch rules filtered by `fullyTranslated`', async () => {
// create a document
const migrationId = uuidv4();
const overrideCallback = (index: number): Partial<RuleMigrationDocument> => {
const translationResult =
index < 6
? RuleTranslationResult.FULL
: index < 8
? RuleTranslationResult.PARTIAL
: RuleTranslationResult.UNTRANSLATABLE;
return {
migration_id: migrationId,
translation_result: translationResult,
};
};
const migrationRuleDocuments = getMigrationRuleDocuments(10, overrideCallback);
await createMigrationRules(es, migrationRuleDocuments);
// fetch failed migration rules
let response = await migrationRulesRoutes.get({
migrationId,
queryParams: { is_fully_translated: true },
});
expect(response.body.total).toEqual(6);
// fetch non-failed migration rules
response = await migrationRulesRoutes.get({
migrationId,
queryParams: { is_fully_translated: false },
});
expect(response.body.total).toEqual(4);
});
it('should fetch rules filtered by `partiallyTranslated`', async () => {
// create a document
const migrationId = uuidv4();
const overrideCallback = (index: number): Partial<RuleMigrationDocument> => {
const translationResult =
index < 4
? RuleTranslationResult.FULL
: index < 8
? RuleTranslationResult.PARTIAL
: RuleTranslationResult.UNTRANSLATABLE;
return {
migration_id: migrationId,
translation_result: translationResult,
};
};
const migrationRuleDocuments = getMigrationRuleDocuments(10, overrideCallback);
await createMigrationRules(es, migrationRuleDocuments);
// fetch failed migration rules
let response = await migrationRulesRoutes.get({
migrationId,
queryParams: { is_partially_translated: true },
});
expect(response.body.total).toEqual(4);
// fetch non-failed migration rules
response = await migrationRulesRoutes.get({
migrationId,
queryParams: { is_partially_translated: false },
});
expect(response.body.total).toEqual(6);
});
it('should fetch rules filtered by `untranslatable`', async () => {
// create a document
const migrationId = uuidv4();
const overrideCallback = (index: number): Partial<RuleMigrationDocument> => {
const translationResult =
index < 3
? RuleTranslationResult.FULL
: index < 5
? RuleTranslationResult.PARTIAL
: RuleTranslationResult.UNTRANSLATABLE;
return {
migration_id: migrationId,
translation_result: translationResult,
};
};
const migrationRuleDocuments = getMigrationRuleDocuments(10, overrideCallback);
await createMigrationRules(es, migrationRuleDocuments);
// fetch failed migration rules
let response = await migrationRulesRoutes.get({
migrationId,
queryParams: { is_untranslatable: true },
});
expect(response.body.total).toEqual(5);
// fetch non-failed migration rules
response = await migrationRulesRoutes.get({
migrationId,
queryParams: { is_untranslatable: false },
});
expect(response.body.total).toEqual(5);
});
});
describe('Sorting', () => {
it('should fetch rules sorted by `title`', async () => {
const titles = ['Elastic 1', 'Windows', 'Linux', 'Elastic 2'];
// create a document
const migrationId = uuidv4();
const overrideCallback = (index: number): Partial<RuleMigrationDocument> => {
const title = titles[index];
const originalRule = { ...defaultOriginalRule, title };
const elasticRule = { ...defaultElasticRule, title };
return {
migration_id: migrationId,
original_rule: originalRule,
elastic_rule: elasticRule,
};
};
const migrationRuleDocuments = getMigrationRuleDocuments(titles.length, overrideCallback);
await createMigrationRules(es, migrationRuleDocuments);
// fetch migration rules
let response = await migrationRulesRoutes.get({
migrationId,
queryParams: { sort_field: 'elastic_rule.title', sort_direction: 'asc' },
});
expect(response.body.data.map((rule) => rule.elastic_rule?.title)).toEqual(titles.sort());
// fetch migration rules
response = await migrationRulesRoutes.get({
migrationId,
queryParams: { sort_field: 'elastic_rule.title', sort_direction: 'desc' },
});
expect(response.body.data.map((rule) => rule.elastic_rule?.title)).toEqual(
titles.sort().reverse()
);
});
it('should fetch rules sorted by `severity`', async () => {
const severities = ['critical', 'low', 'medium', 'low', 'critical'];
// create a document
const migrationId = uuidv4();
const overrideCallback = (index: number): Partial<RuleMigrationDocument> => {
const severity = severities[index];
const elasticRule = { ...defaultElasticRule, severity };
return {
migration_id: migrationId,
elastic_rule: elasticRule,
};
};
const migrationRuleDocuments = getMigrationRuleDocuments(
severities.length,
overrideCallback
);
await createMigrationRules(es, migrationRuleDocuments);
// fetch migration rules
let response = await migrationRulesRoutes.get({
migrationId,
queryParams: { sort_field: 'elastic_rule.severity', sort_direction: 'asc' },
});
expect(response.body.data.map((rule) => rule.elastic_rule?.severity)).toEqual([
'low',
'low',
'medium',
'critical',
'critical',
]);
// fetch migration rules
response = await migrationRulesRoutes.get({
migrationId,
queryParams: { sort_field: 'elastic_rule.severity', sort_direction: 'desc' },
});
expect(response.body.data.map((rule) => rule.elastic_rule?.severity)).toEqual([
'critical',
'critical',
'medium',
'low',
'low',
]);
});
it('should fetch rules sorted by `risk_score`', async () => {
const riskScores = [55, 0, 100, 23];
// create a document
const migrationId = uuidv4();
const overrideCallback = (index: number): Partial<RuleMigrationDocument> => {
const riskScore = riskScores[index];
const elasticRule = { ...defaultElasticRule, risk_score: riskScore };
return {
migration_id: migrationId,
elastic_rule: elasticRule,
};
};
const migrationRuleDocuments = getMigrationRuleDocuments(
riskScores.length,
overrideCallback
);
await createMigrationRules(es, migrationRuleDocuments);
// fetch migration rules
let response = await migrationRulesRoutes.get({
migrationId,
queryParams: { sort_field: 'elastic_rule.risk_score', sort_direction: 'asc' },
});
expect(response.body.data.map((rule) => rule.elastic_rule?.risk_score)).toEqual(
riskScores.sort((a, b) => {
return a - b;
})
);
// fetch migration rules
response = await migrationRulesRoutes.get({
migrationId,
queryParams: { sort_field: 'elastic_rule.risk_score', sort_direction: 'desc' },
});
expect(response.body.data.map((rule) => rule.elastic_rule?.risk_score)).toEqual(
riskScores
.sort((a, b) => {
return a - b;
})
.reverse()
);
});
it('should fetch rules sorted by `prebuilt_rule_id`', async () => {
const prebuiltRuleIds = ['rule-1', undefined, undefined, 'rule-2', undefined];
// create a document
const migrationId = uuidv4();
const overrideCallback = (index: number): Partial<RuleMigrationDocument> => {
const prebuiltRuleId = prebuiltRuleIds[index];
const elasticRule = { ...defaultElasticRule, prebuilt_rule_id: prebuiltRuleId };
return {
migration_id: migrationId,
elastic_rule: elasticRule,
};
};
const migrationRuleDocuments = getMigrationRuleDocuments(
prebuiltRuleIds.length,
overrideCallback
);
await createMigrationRules(es, migrationRuleDocuments);
// fetch migration rules
let response = await migrationRulesRoutes.get({
migrationId,
queryParams: { sort_field: 'elastic_rule.prebuilt_rule_id', sort_direction: 'asc' },
});
expect(response.body.data.map((rule) => rule.elastic_rule?.prebuilt_rule_id)).toEqual([
undefined,
undefined,
undefined,
'rule-1',
'rule-2',
]);
// fetch migration rules
response = await migrationRulesRoutes.get({
migrationId,
queryParams: { sort_field: 'elastic_rule.prebuilt_rule_id', sort_direction: 'desc' },
});
expect(response.body.data.map((rule) => rule.elastic_rule?.prebuilt_rule_id)).toEqual([
'rule-2',
'rule-1',
undefined,
undefined,
undefined,
]);
});
it('should fetch rules sorted by `translation_result`', async () => {
const translationResults = [
RuleTranslationResult.UNTRANSLATABLE,
RuleTranslationResult.FULL,
RuleTranslationResult.PARTIAL,
];
// create a document
const migrationId = uuidv4();
const overrideCallback = (index: number): Partial<RuleMigrationDocument> => {
return {
migration_id: migrationId,
translation_result: translationResults[index],
};
};
const migrationRuleDocuments = getMigrationRuleDocuments(
translationResults.length,
overrideCallback
);
await createMigrationRules(es, migrationRuleDocuments);
// fetch migration rules
let response = await migrationRulesRoutes.get({
migrationId,
queryParams: { sort_field: 'translation_result', sort_direction: 'asc' },
});
expect(response.body.data.map((rule) => rule.translation_result)).toEqual([
RuleTranslationResult.UNTRANSLATABLE,
RuleTranslationResult.PARTIAL,
RuleTranslationResult.FULL,
]);
// fetch migration rules
response = await migrationRulesRoutes.get({
migrationId,
queryParams: { sort_field: 'translation_result', sort_direction: 'desc' },
});
expect(response.body.data.map((rule) => rule.translation_result)).toEqual([
RuleTranslationResult.FULL,
RuleTranslationResult.PARTIAL,
RuleTranslationResult.UNTRANSLATABLE,
]);
});
it('should fetch rules sorted by `updated_at`', async () => {
// create a document
const migrationId = uuidv4();
// Creating documents separately to have different `update_at` timestamps
await createMigrationRules(es, [getMigrationRuleDocument({ migration_id: migrationId })]);
await createMigrationRules(es, [getMigrationRuleDocument({ migration_id: migrationId })]);
await createMigrationRules(es, [getMigrationRuleDocument({ migration_id: migrationId })]);
await createMigrationRules(es, [getMigrationRuleDocument({ migration_id: migrationId })]);
await createMigrationRules(es, [getMigrationRuleDocument({ migration_id: migrationId })]);
// fetch migration rules
let response = await migrationRulesRoutes.get({
migrationId,
queryParams: { sort_field: 'updated_at', sort_direction: 'asc' },
});
const ascSorted = response.body.data.map((rule) => rule.updated_at);
// fetch migration rules
response = await migrationRulesRoutes.get({
migrationId,
queryParams: { sort_field: 'updated_at', sort_direction: 'desc' },
});
const descSorted = response.body.data.map((rule) => rule.updated_at);
expect(ascSorted).toEqual(descSorted.reverse());
});
});
describe('Pagination', () => {
it('should fetch rules within specific page', async () => {
const titles = Array.from({ length: 50 }, (_, index) => `Migration rule - ${index}`);
// create a document
const migrationId = uuidv4();
const overrideCallback = (index: number): Partial<RuleMigrationDocument> => {
const title = titles[index];
const originalRule = { ...defaultOriginalRule, title };
const elasticRule = { ...defaultElasticRule, title };
return {
migration_id: migrationId,
original_rule: originalRule,
elastic_rule: elasticRule,
};
};
const migrationRuleDocuments = getMigrationRuleDocuments(titles.length, overrideCallback);
await createMigrationRules(es, migrationRuleDocuments);
// fetch migration rules
const response = await migrationRulesRoutes.get({
migrationId,
queryParams: { page: 3, per_page: 7 },
});
const start = 3 * 7;
expect(response.body.data.map((rule) => rule.elastic_rule?.title)).toEqual(
titles.slice(start, start + 7)
);
});
it('should fetch rules within very first page if `perPage` is not specified', async () => {
const titles = Array.from({ length: 50 }, (_, index) => `Migration rule - ${index}`);
// create a document
const migrationId = uuidv4();
const overrideCallback = (index: number): Partial<RuleMigrationDocument> => {
const title = titles[index];
const originalRule = { ...defaultOriginalRule, title };
const elasticRule = { ...defaultElasticRule, title };
return {
migration_id: migrationId,
original_rule: originalRule,
elastic_rule: elasticRule,
};
};
const migrationRuleDocuments = getMigrationRuleDocuments(titles.length, overrideCallback);
await createMigrationRules(es, migrationRuleDocuments);
// fetch migration rules
const response = await migrationRulesRoutes.get({
migrationId,
queryParams: { page: 3 },
});
const defaultSize = 10;
expect(response.body.data.map((rule) => rule.elastic_rule?.title)).toEqual(
titles.slice(0, defaultSize)
);
});
it('should fetch rules within very first page of a specified size if `perPage` is specified', async () => {
const titles = Array.from({ length: 50 }, (_, index) => `Migration rule - ${index}`);
// create a document
const migrationId = uuidv4();
const overrideCallback = (index: number): Partial<RuleMigrationDocument> => {
const title = titles[index];
const originalRule = { ...defaultOriginalRule, title };
const elasticRule = { ...defaultElasticRule, title };
return {
migration_id: migrationId,
original_rule: originalRule,
elastic_rule: elasticRule,
};
};
const migrationRuleDocuments = getMigrationRuleDocuments(titles.length, overrideCallback);
await createMigrationRules(es, migrationRuleDocuments);
// fetch migration rules
const response = await migrationRulesRoutes.get({
migrationId,
queryParams: { per_page: 18 },
});
expect(response.body.data.map((rule) => rule.elastic_rule?.title)).toEqual(
titles.slice(0, 18)
);
});
});
});

View file

@ -7,11 +7,11 @@
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../../../ftr_provider_context';
import { migrationRulesRouteHelpersFactory } from '../../utils';
import { ruleMigrationRouteHelpersFactory } from '../../utils';
export default ({ getService }: FtrProviderContext) => {
const supertest = getService('supertest');
const migrationRulesRoutes = migrationRulesRouteHelpersFactory(supertest);
const migrationRulesRoutes = ruleMigrationRouteHelpersFactory(supertest);
describe('Get Integrations', () => {
it('should return all integrations successfully', async () => {

View file

@ -8,14 +8,14 @@
import expect from 'expect';
import { v4 as uuidv4 } from 'uuid';
import { RuleTranslationResult } from '@kbn/security-solution-plugin/common/siem_migrations/constants';
import { RuleMigrationRuleData } from '@kbn/security-solution-plugin/common/siem_migrations/model/rule_migration.gen';
import { deleteAllRules } from '../../../../../common/utils/security_solution';
import {
RuleMigrationDocument,
createMigrationRules,
defaultElasticRule,
deleteAllMigrationRules,
deleteAllRuleMigrations,
getMigrationRuleDocuments,
migrationRulesRouteHelpersFactory,
ruleMigrationRouteHelpersFactory,
} from '../../utils';
import { FtrProviderContext } from '../../../../ftr_provider_context';
import {
@ -29,14 +29,14 @@ export default ({ getService }: FtrProviderContext) => {
const es = getService('es');
const log = getService('log');
const supertest = getService('supertest');
const migrationRulesRoutes = migrationRulesRouteHelpersFactory(supertest);
const migrationRulesRoutes = ruleMigrationRouteHelpersFactory(supertest);
describe('@ess @serverless @serverlessQA Get Prebuilt Rules API', () => {
beforeEach(async () => {
await deleteAllRules(supertest, log);
await deleteAllTimelines(es, log);
await deleteAllPrebuiltRuleAssets(es, log);
await deleteAllMigrationRules(es);
await deleteAllRuleMigrations(es);
// Add some prebuilt rules
const ruleAssetSavedObjects = [
@ -52,7 +52,7 @@ export default ({ getService }: FtrProviderContext) => {
it('should return all prebuilt rules matched by migration rules', async () => {
const migrationId = uuidv4();
const overrideCallback = (index: number): Partial<RuleMigrationDocument> => {
const overrideCallback = (index: number): Partial<RuleMigrationRuleData> => {
const { query_language: queryLanguage, query, ...rest } = defaultElasticRule;
return {
migration_id: migrationId,

View file

@ -9,11 +9,14 @@ import { FtrProviderContext } from '../../../../ftr_provider_context';
export default function ({ loadTestFile }: FtrProviderContext) {
describe('@ess @serverless SecuritySolution SIEM Migrations', () => {
loadTestFile(require.resolve('./create'));
loadTestFile(require.resolve('./get_prebuilt_rules'));
loadTestFile(require.resolve('./get'));
loadTestFile(require.resolve('./delete'));
loadTestFile(require.resolve('./rules/create'));
loadTestFile(require.resolve('./rules/get'));
loadTestFile(require.resolve('./rules/update'));
loadTestFile(require.resolve('./get_prebuilt_rules'));
loadTestFile(require.resolve('./install'));
loadTestFile(require.resolve('./stats'));
loadTestFile(require.resolve('./update'));
loadTestFile(require.resolve('./start'));
loadTestFile(require.resolve('./stop'));
loadTestFile(require.resolve('./get_integrations'));

View file

@ -7,17 +7,19 @@
import expect from 'expect';
import { v4 as uuidv4 } from 'uuid';
import { ElasticRule } from '@kbn/security-solution-plugin/common/siem_migrations/model/rule_migration.gen';
import {
ElasticRule,
RuleMigrationRuleData,
} from '@kbn/security-solution-plugin/common/siem_migrations/model/rule_migration.gen';
import { RuleTranslationResult } from '@kbn/security-solution-plugin/common/siem_migrations/constants';
import { RuleResponse } from '@kbn/security-solution-plugin/common/api/detection_engine';
import { deleteAllRules } from '../../../../../common/utils/security_solution';
import {
RuleMigrationDocument,
createMigrationRules,
defaultElasticRule,
deleteAllMigrationRules,
deleteAllRuleMigrations,
getMigrationRuleDocuments,
migrationRulesRouteHelpersFactory,
ruleMigrationRouteHelpersFactory,
statsOverrideCallbackFactory,
} from '../../utils';
import { FtrProviderContext } from '../../../../ftr_provider_context';
@ -33,20 +35,20 @@ export default ({ getService }: FtrProviderContext) => {
const log = getService('log');
const supertest = getService('supertest');
const securitySolutionApi = getService('securitySolutionApi');
const migrationRulesRoutes = migrationRulesRouteHelpersFactory(supertest);
const migrationRulesRoutes = ruleMigrationRouteHelpersFactory(supertest);
describe('@ess @serverless @serverlessQA Install API', () => {
beforeEach(async () => {
await deleteAllRules(supertest, log);
await deleteAllTimelines(es, log);
await deleteAllPrebuiltRuleAssets(es, log);
await deleteAllMigrationRules(es);
await deleteAllRuleMigrations(es);
});
it('should install all installable custom migration rules', async () => {
const migrationId = uuidv4();
const overrideCallback = (index: number): Partial<RuleMigrationDocument> => {
const overrideCallback = (index: number): Partial<RuleMigrationRuleData> => {
const title = `Rule - ${index}`;
const elasticRule = { ...defaultElasticRule, title };
return {
@ -63,7 +65,7 @@ export default ({ getService }: FtrProviderContext) => {
expect(installResponse.body).toEqual({ installed: 2 });
// fetch installed migration rules information
const response = await migrationRulesRoutes.get({ migrationId });
const response = await migrationRulesRoutes.getRules({ migrationId });
const installedMigrationRules = response.body.data.reduce((acc, item) => {
if (item.elastic_rule?.id) {
acc.push(item.elastic_rule);
@ -100,7 +102,7 @@ export default ({ getService }: FtrProviderContext) => {
const migrationId = uuidv4();
const overrideCallback = (index: number): Partial<RuleMigrationDocument> => {
const overrideCallback = (index: number): Partial<RuleMigrationRuleData> => {
const { query_language: queryLanguage, query, ...rest } = defaultElasticRule;
return {
migration_id: migrationId,

View file

@ -0,0 +1,210 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import expect from 'expect';
import { SiemMigrationStatus } from '@kbn/security-solution-plugin/common/siem_migrations/constants';
import {
defaultOriginalRule,
deleteAllRuleMigrations,
ruleMigrationResourcesRouteHelpersFactory,
ruleMigrationRouteHelpersFactory,
splunkRuleWithResources,
} from '../../../utils';
import { FtrProviderContext } from '../../../../../ftr_provider_context';
export default ({ getService }: FtrProviderContext) => {
const es = getService('es');
const supertest = getService('supertest');
const migrationRulesRoutes = ruleMigrationRouteHelpersFactory(supertest);
const migrationResourcesRoutes = ruleMigrationResourcesRouteHelpersFactory(supertest);
describe('@ess @serverless @serverlessQA Create Rules API', () => {
let migrationId: string;
beforeEach(async () => {
await deleteAllRuleMigrations(es);
const response = await migrationRulesRoutes.create({});
migrationId = response.body.migration_id;
});
describe('Happy path', () => {
it('should create migrations with provided id', async () => {
await migrationRulesRoutes.addRulesToMigration({
migrationId,
payload: [defaultOriginalRule],
});
// fetch migration rule
const response = await migrationRulesRoutes.getRules({ migrationId });
expect(response.body.total).toEqual(1);
const migrationRule = response.body.data[0];
expect(migrationRule).toEqual(
expect.objectContaining({
migration_id: migrationId,
original_rule: defaultOriginalRule,
status: SiemMigrationStatus.PENDING,
})
);
});
it('should create migrations with the rules that have resources', async () => {
await migrationRulesRoutes.addRulesToMigration({
migrationId,
payload: [splunkRuleWithResources],
});
// fetch migration rule
const response = await migrationRulesRoutes.getRules({ migrationId });
expect(response.body.total).toEqual(1);
const migrationRule = response.body.data[0];
expect(migrationRule).toEqual(
expect.objectContaining({
migration_id: migrationId,
original_rule: splunkRuleWithResources,
status: SiemMigrationStatus.PENDING,
})
);
// fetch missing resources
const resourcesResponse = await migrationResourcesRoutes.getMissingResources({
migrationId,
});
expect(resourcesResponse.body).toEqual([
{ type: 'macro', name: 'summariesonly' },
{ type: 'macro', name: 'drop_dm_object_name(1)' },
{ type: 'lookup', name: 'malware_tracker' },
]);
});
});
describe('Error handling', () => {
it('should return no content error', async () => {
await migrationRulesRoutes.addRulesToMigration({
migrationId,
payload: [],
expectStatusCode: 204,
});
// fetch migration rule
const response = await migrationRulesRoutes.getRules({ migrationId });
expect(response.body.total).toEqual(0);
});
it('should return 404 if invalid migration id is provided', async () => {
const { body } = await migrationRulesRoutes.addRulesToMigration({
migrationId: 'non-existing-migration-id',
payload: [defaultOriginalRule],
expectStatusCode: 404,
});
expect(body).toMatchObject({
statusCode: 404,
error: 'Not Found',
message: 'No Migration found with id: non-existing-migration-id',
});
});
it(`should return an error when undefined payload has been passed`, async () => {
const response = await migrationRulesRoutes.addRulesToMigration({
migrationId,
expectStatusCode: 400,
});
expect(response.body).toEqual({
error: 'Bad Request',
message: '[request body]: Expected array, received null',
statusCode: 400,
});
});
it('should return an error when original rule id is not specified', async () => {
const { id, ...restOfOriginalRule } = defaultOriginalRule;
const response = await migrationRulesRoutes.addRulesToMigration({
migrationId,
payload: [restOfOriginalRule],
expectStatusCode: 400,
});
expect(response.body).toEqual({
statusCode: 400,
error: 'Bad Request',
message: '[request body]: 0.id: Required',
});
});
it('should return an error when original rule vendor is not specified', async () => {
const { vendor, ...restOfOriginalRule } = defaultOriginalRule;
const response = await migrationRulesRoutes.addRulesToMigration({
migrationId,
payload: [restOfOriginalRule],
expectStatusCode: 400,
});
expect(response.body).toEqual({
statusCode: 400,
error: 'Bad Request',
message: '[request body]: 0.vendor: Invalid literal value, expected "splunk"',
});
});
it('should return an error when original rule title is not specified', async () => {
const { title, ...restOfOriginalRule } = defaultOriginalRule;
const response = await migrationRulesRoutes.addRulesToMigration({
migrationId,
payload: [restOfOriginalRule],
expectStatusCode: 400,
});
expect(response.body).toEqual({
statusCode: 400,
error: 'Bad Request',
message: '[request body]: 0.title: Required',
});
});
it('should return an error when original rule description is not specified', async () => {
const { description, ...restOfOriginalRule } = defaultOriginalRule;
const response = await migrationRulesRoutes.addRulesToMigration({
migrationId,
payload: [restOfOriginalRule],
expectStatusCode: 400,
});
expect(response.body).toEqual({
statusCode: 400,
error: 'Bad Request',
message: '[request body]: 0.description: Required',
});
});
it('should return an error when original rule query is not specified', async () => {
const { query, ...restOfOriginalRule } = defaultOriginalRule;
const response = await migrationRulesRoutes.addRulesToMigration({
migrationId,
payload: [restOfOriginalRule],
expectStatusCode: 400,
});
expect(response.body).toEqual({
statusCode: 400,
error: 'Bad Request',
message: '[request body]: 0.query: Required',
});
});
it('should return an error when original rule query_language is not specified', async () => {
const { query_language: _, ...restOfOriginalRule } = defaultOriginalRule;
const response = await migrationRulesRoutes.addRulesToMigration({
migrationId,
payload: [restOfOriginalRule],
expectStatusCode: 400,
});
expect(response.body).toEqual({
statusCode: 400,
error: 'Bad Request',
message: '[request body]: 0.query_language: Required',
});
});
});
});
};

View file

@ -0,0 +1,635 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import expect from 'expect';
import { v4 as uuidv4 } from 'uuid';
import {
RuleTranslationResult,
SiemMigrationStatus,
} from '@kbn/security-solution-plugin/common/siem_migrations/constants';
import { RuleMigrationRuleData } from '@kbn/security-solution-plugin/common/siem_migrations/model/rule_migration.gen';
import {
createMigrationRules,
defaultElasticRule,
defaultOriginalRule,
deleteAllRuleMigrations,
getMigrationRuleDocument,
getMigrationRuleDocuments,
ruleMigrationRouteHelpersFactory,
} from '../../../utils';
import { FtrProviderContext } from '../../../../../ftr_provider_context';
export default ({ getService }: FtrProviderContext) => {
const es = getService('es');
const supertest = getService('supertest');
const migrationRulesRoutes = ruleMigrationRouteHelpersFactory(supertest);
describe('@ess @serverless @serverlessQA Get Rules API', () => {
beforeEach(async () => {
await deleteAllRuleMigrations(es);
});
describe('Basic', () => {
it('should fetch existing rules within specified migration', async () => {
const migrationId = uuidv4();
const migrationRuleDocument = getMigrationRuleDocument({ migration_id: migrationId });
await createMigrationRules(es, [migrationRuleDocument]);
const { '@timestamp': timestamp, updated_at: updatedAt, ...rest } = migrationRuleDocument;
// fetch migration rule
const response = await migrationRulesRoutes.getRules({ migrationId });
expect(response.body.total).toEqual(1);
expect(response.body.data).toEqual(expect.arrayContaining([expect.objectContaining(rest)]));
});
});
describe('Filtering', () => {
it('should fetch rules filtered by `searchTerm`', async () => {
const migrationId = uuidv4();
const overrideCallback = (index: number): Partial<RuleMigrationRuleData> => {
const title = `${index < 5 ? 'Elastic' : 'Splunk'} rule - ${index}`;
const originalRule = { ...defaultOriginalRule, title };
const elasticRule = { ...defaultElasticRule, title };
return {
migration_id: migrationId,
original_rule: originalRule,
elastic_rule: elasticRule,
};
};
const migrationRuleDocuments = getMigrationRuleDocuments(10, overrideCallback);
await createMigrationRules(es, migrationRuleDocuments);
// Search by word `Elastic`
let expectedRuleDocuments = expect.arrayContaining(
migrationRuleDocuments
.slice(0, 5)
.map(({ '@timestamp': timestamp, updated_at: updatedAt, ...rest }) =>
expect.objectContaining(rest)
)
);
// fetch migration rules
let response = await migrationRulesRoutes.getRules({
migrationId,
queryParams: { search_term: 'Elastic' },
});
expect(response.body.total).toEqual(5);
expect(response.body.data).toEqual(expectedRuleDocuments);
// Search by word `Splunk`
expectedRuleDocuments = expect.arrayContaining(
migrationRuleDocuments
.slice(5)
.map(({ '@timestamp': timestamp, updated_at: updatedAt, ...rest }) =>
expect.objectContaining(rest)
)
);
// fetch migration rules
response = await migrationRulesRoutes.getRules({
migrationId,
queryParams: { search_term: 'Splunk' },
});
expect(response.body.total).toEqual(5);
expect(response.body.data).toEqual(expectedRuleDocuments);
});
it('should fetch rules filtered by `ids`', async () => {
const migrationId = uuidv4();
const migrationRuleDocuments = getMigrationRuleDocuments(10, () => ({
migration_id: migrationId,
}));
const createdDocumentIds = await createMigrationRules(es, migrationRuleDocuments);
const expectedIds = createdDocumentIds.slice(0, 3).sort();
// fetch migration rules by existing ids
let response = await migrationRulesRoutes.getRules({
migrationId,
queryParams: { ids: expectedIds },
});
expect(response.body.total).toEqual(3);
expect(response.body.data.map(({ id }) => id).sort()).toEqual(expectedIds);
// fetch migration rules by non-existing id
response = await migrationRulesRoutes.getRules({
migrationId,
queryParams: { ids: [uuidv4()] },
});
expect(response.body.total).toEqual(0);
});
it('should fetch rules filtered by `prebuilt`', async () => {
const migrationId = uuidv4();
const overrideCallback = (index: number): Partial<RuleMigrationRuleData> => {
const prebuiltRuleId = index < 3 ? uuidv4() : undefined;
const elasticRule = { ...defaultElasticRule, prebuilt_rule_id: prebuiltRuleId };
return {
migration_id: migrationId,
elastic_rule: elasticRule,
};
};
const migrationRuleDocuments = getMigrationRuleDocuments(10, overrideCallback);
await createMigrationRules(es, migrationRuleDocuments);
// fetch migration rules matched Elastic prebuilt rules
let response = await migrationRulesRoutes.getRules({
migrationId,
queryParams: { is_prebuilt: true },
});
expect(response.body.total).toEqual(3);
// fetch custom translated migration rules
response = await migrationRulesRoutes.getRules({
migrationId,
queryParams: { is_prebuilt: false },
});
expect(response.body.total).toEqual(7);
});
it('should fetch rules filtered by `installed`', async () => {
const migrationId = uuidv4();
const overrideCallback = (index: number): Partial<RuleMigrationRuleData> => {
const installedRuleId = index < 2 ? uuidv4() : undefined;
const elasticRule = { ...defaultElasticRule, id: installedRuleId };
return {
migration_id: migrationId,
elastic_rule: elasticRule,
};
};
const migrationRuleDocuments = getMigrationRuleDocuments(10, overrideCallback);
await createMigrationRules(es, migrationRuleDocuments);
// fetch installed migration rules
let response = await migrationRulesRoutes.getRules({
migrationId,
queryParams: { is_installed: true },
});
expect(response.body.total).toEqual(2);
// fetch non-installed migration rules
response = await migrationRulesRoutes.getRules({
migrationId,
queryParams: { is_installed: false },
});
expect(response.body.total).toEqual(8);
});
it('should fetch rules filtered by `failed`', async () => {
const migrationId = uuidv4();
const overrideCallback = (index: number): Partial<RuleMigrationRuleData> => {
const status = index < 4 ? SiemMigrationStatus.FAILED : SiemMigrationStatus.COMPLETED;
return {
migration_id: migrationId,
status,
};
};
const migrationRuleDocuments = getMigrationRuleDocuments(10, overrideCallback);
await createMigrationRules(es, migrationRuleDocuments);
// fetch failed migration rules
let response = await migrationRulesRoutes.getRules({
migrationId,
queryParams: { is_failed: true },
});
expect(response.body.total).toEqual(4);
// fetch non-failed migration rules
response = await migrationRulesRoutes.getRules({
migrationId,
queryParams: { is_failed: false },
});
expect(response.body.total).toEqual(6);
});
it('should fetch rules filtered by `fullyTranslated`', async () => {
const migrationId = uuidv4();
const overrideCallback = (index: number): Partial<RuleMigrationRuleData> => {
const translationResult =
index < 6
? RuleTranslationResult.FULL
: index < 8
? RuleTranslationResult.PARTIAL
: RuleTranslationResult.UNTRANSLATABLE;
return {
migration_id: migrationId,
translation_result: translationResult,
};
};
const migrationRuleDocuments = getMigrationRuleDocuments(10, overrideCallback);
await createMigrationRules(es, migrationRuleDocuments);
// fetch failed migration rules
let response = await migrationRulesRoutes.getRules({
migrationId,
queryParams: { is_fully_translated: true },
});
expect(response.body.total).toEqual(6);
// fetch non-failed migration rules
response = await migrationRulesRoutes.getRules({
migrationId,
queryParams: { is_fully_translated: false },
});
expect(response.body.total).toEqual(4);
});
it('should fetch rules filtered by `partiallyTranslated`', async () => {
const migrationId = uuidv4();
const overrideCallback = (index: number): Partial<RuleMigrationRuleData> => {
const translationResult =
index < 4
? RuleTranslationResult.FULL
: index < 8
? RuleTranslationResult.PARTIAL
: RuleTranslationResult.UNTRANSLATABLE;
return {
migration_id: migrationId,
translation_result: translationResult,
};
};
const migrationRuleDocuments = getMigrationRuleDocuments(10, overrideCallback);
await createMigrationRules(es, migrationRuleDocuments);
// fetch failed migration rules
let response = await migrationRulesRoutes.getRules({
migrationId,
queryParams: { is_partially_translated: true },
});
expect(response.body.total).toEqual(4);
// fetch non-failed migration rules
response = await migrationRulesRoutes.getRules({
migrationId,
queryParams: { is_partially_translated: false },
});
expect(response.body.total).toEqual(6);
});
it('should fetch rules filtered by `untranslatable`', async () => {
const migrationId = uuidv4();
const overrideCallback = (index: number): Partial<RuleMigrationRuleData> => {
const translationResult =
index < 3
? RuleTranslationResult.FULL
: index < 5
? RuleTranslationResult.PARTIAL
: RuleTranslationResult.UNTRANSLATABLE;
return {
migration_id: migrationId,
translation_result: translationResult,
};
};
const migrationRuleDocuments = getMigrationRuleDocuments(10, overrideCallback);
await createMigrationRules(es, migrationRuleDocuments);
// fetch failed migration rules
let response = await migrationRulesRoutes.getRules({
migrationId,
queryParams: { is_untranslatable: true },
});
expect(response.body.total).toEqual(5);
// fetch non-failed migration rules
response = await migrationRulesRoutes.getRules({
migrationId,
queryParams: { is_untranslatable: false },
});
expect(response.body.total).toEqual(5);
});
});
describe('Sorting', () => {
it('should fetch rules sorted by `title`', async () => {
const migrationId = uuidv4();
const titles = ['Elastic 1', 'Windows', 'Linux', 'Elastic 2'];
const overrideCallback = (index: number): Partial<RuleMigrationRuleData> => {
const title = titles[index];
const originalRule = { ...defaultOriginalRule, title };
const elasticRule = { ...defaultElasticRule, title };
return {
migration_id: migrationId,
original_rule: originalRule,
elastic_rule: elasticRule,
};
};
const migrationRuleDocuments = getMigrationRuleDocuments(titles.length, overrideCallback);
await createMigrationRules(es, migrationRuleDocuments);
// fetch migration rules
let response = await migrationRulesRoutes.getRules({
migrationId,
queryParams: { sort_field: 'elastic_rule.title', sort_direction: 'asc' },
});
expect(response.body.data.map((rule) => rule.elastic_rule?.title)).toEqual(titles.sort());
// fetch migration rules
response = await migrationRulesRoutes.getRules({
migrationId,
queryParams: { sort_field: 'elastic_rule.title', sort_direction: 'desc' },
});
expect(response.body.data.map((rule) => rule.elastic_rule?.title)).toEqual(
titles.sort().reverse()
);
});
it('should fetch rules sorted by `severity`', async () => {
const migrationId = uuidv4();
const severities = ['critical', 'low', 'medium', 'low', 'critical'];
const overrideCallback = (index: number): Partial<RuleMigrationRuleData> => {
const severity = severities[index];
const elasticRule = { ...defaultElasticRule, severity };
return {
migration_id: migrationId,
elastic_rule: elasticRule,
};
};
const migrationRuleDocuments = getMigrationRuleDocuments(
severities.length,
overrideCallback
);
await createMigrationRules(es, migrationRuleDocuments);
// fetch migration rules
let response = await migrationRulesRoutes.getRules({
migrationId,
queryParams: { sort_field: 'elastic_rule.severity', sort_direction: 'asc' },
});
expect(response.body.data.map((rule) => rule.elastic_rule?.severity)).toEqual([
'low',
'low',
'medium',
'critical',
'critical',
]);
// fetch migration rules
response = await migrationRulesRoutes.getRules({
migrationId,
queryParams: { sort_field: 'elastic_rule.severity', sort_direction: 'desc' },
});
expect(response.body.data.map((rule) => rule.elastic_rule?.severity)).toEqual([
'critical',
'critical',
'medium',
'low',
'low',
]);
});
it('should fetch rules sorted by `risk_score`', async () => {
const migrationId = uuidv4();
const riskScores = [55, 0, 100, 23];
const overrideCallback = (index: number): Partial<RuleMigrationRuleData> => {
const riskScore = riskScores[index];
const elasticRule = { ...defaultElasticRule, risk_score: riskScore };
return {
migration_id: migrationId,
elastic_rule: elasticRule,
};
};
const migrationRuleDocuments = getMigrationRuleDocuments(
riskScores.length,
overrideCallback
);
await createMigrationRules(es, migrationRuleDocuments);
// fetch migration rules
let response = await migrationRulesRoutes.getRules({
migrationId,
queryParams: { sort_field: 'elastic_rule.risk_score', sort_direction: 'asc' },
});
expect(response.body.data.map((rule) => rule.elastic_rule?.risk_score)).toEqual(
riskScores.sort((a, b) => {
return a - b;
})
);
// fetch migration rules
response = await migrationRulesRoutes.getRules({
migrationId,
queryParams: { sort_field: 'elastic_rule.risk_score', sort_direction: 'desc' },
});
expect(response.body.data.map((rule) => rule.elastic_rule?.risk_score)).toEqual(
riskScores
.sort((a, b) => {
return a - b;
})
.reverse()
);
});
it('should fetch rules sorted by `prebuilt_rule_id`', async () => {
const migrationId = uuidv4();
const prebuiltRuleIds = ['rule-1', undefined, undefined, 'rule-2', undefined];
const overrideCallback = (index: number): Partial<RuleMigrationRuleData> => {
const prebuiltRuleId = prebuiltRuleIds[index];
const elasticRule = { ...defaultElasticRule, prebuilt_rule_id: prebuiltRuleId };
return {
migration_id: migrationId,
elastic_rule: elasticRule,
};
};
const migrationRuleDocuments = getMigrationRuleDocuments(
prebuiltRuleIds.length,
overrideCallback
);
await createMigrationRules(es, migrationRuleDocuments);
// fetch migration rules
let response = await migrationRulesRoutes.getRules({
migrationId,
queryParams: { sort_field: 'elastic_rule.prebuilt_rule_id', sort_direction: 'asc' },
});
expect(response.body.data.map((rule) => rule.elastic_rule?.prebuilt_rule_id)).toEqual([
undefined,
undefined,
undefined,
'rule-1',
'rule-2',
]);
// fetch migration rules
response = await migrationRulesRoutes.getRules({
migrationId,
queryParams: { sort_field: 'elastic_rule.prebuilt_rule_id', sort_direction: 'desc' },
});
expect(response.body.data.map((rule) => rule.elastic_rule?.prebuilt_rule_id)).toEqual([
'rule-2',
'rule-1',
undefined,
undefined,
undefined,
]);
});
it('should fetch rules sorted by `translation_result`', async () => {
const migrationId = uuidv4();
const translationResults = [
RuleTranslationResult.UNTRANSLATABLE,
RuleTranslationResult.FULL,
RuleTranslationResult.PARTIAL,
];
const overrideCallback = (index: number): Partial<RuleMigrationRuleData> => {
return {
migration_id: migrationId,
translation_result: translationResults[index],
};
};
const migrationRuleDocuments = getMigrationRuleDocuments(
translationResults.length,
overrideCallback
);
await createMigrationRules(es, migrationRuleDocuments);
// fetch migration rules
let response = await migrationRulesRoutes.getRules({
migrationId,
queryParams: { sort_field: 'translation_result', sort_direction: 'asc' },
});
expect(response.body.data.map((rule) => rule.translation_result)).toEqual([
RuleTranslationResult.UNTRANSLATABLE,
RuleTranslationResult.PARTIAL,
RuleTranslationResult.FULL,
]);
// fetch migration rules
response = await migrationRulesRoutes.getRules({
migrationId,
queryParams: { sort_field: 'translation_result', sort_direction: 'desc' },
});
expect(response.body.data.map((rule) => rule.translation_result)).toEqual([
RuleTranslationResult.FULL,
RuleTranslationResult.PARTIAL,
RuleTranslationResult.UNTRANSLATABLE,
]);
});
it('should fetch rules sorted by `updated_at`', async () => {
// Creating documents separately to have different `update_at` timestamps
const migrationId = uuidv4();
await createMigrationRules(es, [getMigrationRuleDocument({ migration_id: migrationId })]);
await createMigrationRules(es, [getMigrationRuleDocument({ migration_id: migrationId })]);
await createMigrationRules(es, [getMigrationRuleDocument({ migration_id: migrationId })]);
await createMigrationRules(es, [getMigrationRuleDocument({ migration_id: migrationId })]);
await createMigrationRules(es, [getMigrationRuleDocument({ migration_id: migrationId })]);
// fetch migration rules
let response = await migrationRulesRoutes.getRules({
migrationId,
queryParams: { sort_field: 'updated_at', sort_direction: 'asc' },
});
const ascSorted = response.body.data.map((rule) => rule.updated_at);
// fetch migration rules
response = await migrationRulesRoutes.getRules({
migrationId,
queryParams: { sort_field: 'updated_at', sort_direction: 'desc' },
});
const descSorted = response.body.data.map((rule) => rule.updated_at);
expect(ascSorted).toEqual(descSorted.reverse());
});
});
describe('Pagination', () => {
it('should fetch rules within specific page', async () => {
const migrationId = uuidv4();
const titles = Array.from({ length: 50 }, (_, index) => `Migration rule - ${index}`);
const overrideCallback = (index: number): Partial<RuleMigrationRuleData> => {
const title = titles[index];
const originalRule = { ...defaultOriginalRule, title };
const elasticRule = { ...defaultElasticRule, title };
return {
migration_id: migrationId,
original_rule: originalRule,
elastic_rule: elasticRule,
};
};
const migrationRuleDocuments = getMigrationRuleDocuments(titles.length, overrideCallback);
await createMigrationRules(es, migrationRuleDocuments);
// fetch migration rules
const response = await migrationRulesRoutes.getRules({
migrationId,
queryParams: { page: 3, per_page: 7 },
});
const start = 3 * 7;
expect(response.body.data.map((rule) => rule.elastic_rule?.title)).toEqual(
titles.slice(start, start + 7)
);
});
it('should fetch rules within very first page if `perPage` is not specified', async () => {
const migrationId = uuidv4();
const titles = Array.from({ length: 50 }, (_, index) => `Migration rule - ${index}`);
const overrideCallback = (index: number): Partial<RuleMigrationRuleData> => {
const title = titles[index];
const originalRule = { ...defaultOriginalRule, title };
const elasticRule = { ...defaultElasticRule, title };
return {
migration_id: migrationId,
original_rule: originalRule,
elastic_rule: elasticRule,
};
};
const migrationRuleDocuments = getMigrationRuleDocuments(titles.length, overrideCallback);
await createMigrationRules(es, migrationRuleDocuments);
// fetch migration rules
const response = await migrationRulesRoutes.getRules({
migrationId,
queryParams: { page: 3 },
});
const defaultSize = 10;
expect(response.body.data.map((rule) => rule.elastic_rule?.title)).toEqual(
titles.slice(0, defaultSize)
);
});
it('should fetch rules within very first page of a specified size if `perPage` is specified', async () => {
const migrationId = uuidv4();
const titles = Array.from({ length: 50 }, (_, index) => `Migration rule - ${index}`);
const overrideCallback = (index: number): Partial<RuleMigrationRuleData> => {
const title = titles[index];
const originalRule = { ...defaultOriginalRule, title };
const elasticRule = { ...defaultElasticRule, title };
return {
migration_id: migrationId,
original_rule: originalRule,
elastic_rule: elasticRule,
};
};
const migrationRuleDocuments = getMigrationRuleDocuments(titles.length, overrideCallback);
await createMigrationRules(es, migrationRuleDocuments);
// fetch migration rules
const response = await migrationRulesRoutes.getRules({
migrationId,
queryParams: { per_page: 18 },
});
expect(response.body.data.map((rule) => rule.elastic_rule?.title)).toEqual(
titles.slice(0, 18)
);
});
});
});
};

View file

@ -9,20 +9,20 @@ import expect from 'expect';
import { v4 as uuidv4 } from 'uuid';
import {
createMigrationRules,
deleteAllMigrationRules,
deleteAllRuleMigrations,
getMigrationRuleDocument,
migrationRulesRouteHelpersFactory,
} from '../../utils';
import { FtrProviderContext } from '../../../../ftr_provider_context';
ruleMigrationRouteHelpersFactory,
} from '../../../utils';
import { FtrProviderContext } from '../../../../../ftr_provider_context';
export default ({ getService }: FtrProviderContext) => {
const es = getService('es');
const supertest = getService('supertest');
const migrationRulesRoutes = migrationRulesRouteHelpersFactory(supertest);
const ruleMigrationRoutes = ruleMigrationRouteHelpersFactory(supertest);
describe('@ess @serverless @serverlessQA Update API', () => {
describe('@ess @serverless @serverlessQA Update Rules API', () => {
beforeEach(async () => {
await deleteAllMigrationRules(es);
await deleteAllRuleMigrations(es);
});
describe('Happy path', () => {
@ -32,7 +32,10 @@ export default ({ getService }: FtrProviderContext) => {
const [createdDocumentId] = await createMigrationRules(es, [migrationRuleDocument]);
const now = new Date().toISOString();
await migrationRulesRoutes.update({
const {
body: { updated },
} = await ruleMigrationRoutes.updateRules({
migrationId,
payload: [
{
@ -43,8 +46,10 @@ export default ({ getService }: FtrProviderContext) => {
],
});
expect(updated).toBe(true);
// fetch migration rule
const response = await migrationRulesRoutes.get({ migrationId });
const response = await ruleMigrationRoutes.getRules({ migrationId });
expect(response.body.total).toEqual(1);
const {
@ -71,7 +76,7 @@ export default ({ getService }: FtrProviderContext) => {
const [createdDocumentId] = await createMigrationRules(es, [migrationRuleDocument]);
const now = new Date().toISOString();
await migrationRulesRoutes.update({
await ruleMigrationRoutes.updateRules({
migrationId,
payload: [
{
@ -101,7 +106,7 @@ export default ({ getService }: FtrProviderContext) => {
});
// fetch migration rule
const response = await migrationRulesRoutes.get({ migrationId });
const response = await ruleMigrationRoutes.getRules({ migrationId });
expect(response.body.total).toEqual(1);
const migrationRule = response.body.data[0];
@ -112,7 +117,9 @@ export default ({ getService }: FtrProviderContext) => {
describe('Error handling', () => {
it('should return empty content response when no rules passed', async () => {
const migrationId = uuidv4();
await migrationRulesRoutes.update({
const migrationRuleDocument = getMigrationRuleDocument({ migration_id: migrationId });
await createMigrationRules(es, [migrationRuleDocument]);
await ruleMigrationRoutes.updateRules({
migrationId,
payload: [],
expectStatusCode: 204,
@ -123,8 +130,7 @@ export default ({ getService }: FtrProviderContext) => {
const migrationId = uuidv4();
const migrationRuleDocument = getMigrationRuleDocument({ migration_id: migrationId });
await createMigrationRules(es, [migrationRuleDocument]);
const response = await migrationRulesRoutes.update({
const response = await ruleMigrationRoutes.updateRules({
migrationId,
payload: [{ elastic_rule: { title: 'Updated title' } }],
expectStatusCode: 400,
@ -140,8 +146,7 @@ export default ({ getService }: FtrProviderContext) => {
const migrationId = uuidv4();
const migrationRuleDocument = getMigrationRuleDocument({ migration_id: migrationId });
await createMigrationRules(es, [migrationRuleDocument]);
const response = await migrationRulesRoutes.update({
const response = await ruleMigrationRoutes.updateRules({
migrationId,
expectStatusCode: 400,
});

View file

@ -4,24 +4,24 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { v4 as uuidv4 } from 'uuid';
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../../../ftr_provider_context';
import {
SiemMigrationsAPIErrorResponse,
defaultOriginalRule,
migrationRulesRouteHelpersFactory,
ruleMigrationRouteHelpersFactory,
} from '../../utils';
export default ({ getService }: FtrProviderContext) => {
const supertest = getService('supertest');
const migrationRulesRoutes = migrationRulesRouteHelpersFactory(supertest);
const migrationRulesRoutes = ruleMigrationRouteHelpersFactory(supertest);
describe('Start Migration', () => {
let migrationId: string;
beforeEach(async () => {
migrationId = uuidv4();
await migrationRulesRoutes.create({
const createMigrationRespose = await migrationRulesRoutes.create({});
migrationId = createMigrationRespose.body.migration_id;
await migrationRulesRoutes.addRulesToMigration({
migrationId,
payload: [defaultOriginalRule],
});
@ -87,7 +87,7 @@ export default ({ getService }: FtrProviderContext) => {
describe('error scenarios', () => {
it('should reject if connector_id is incorrect', async () => {
const response = await migrationRulesRoutes.start({
migrationId: 'invalid_migration_id',
migrationId,
expectStatusCode: 400,
payload: {
connector_id: 'preconfigured_bedrock',
@ -122,6 +122,20 @@ export default ({ getService }: FtrProviderContext) => {
},
});
});
it('should reject with 404 if migrationId is not provided', async () => {
// @ts-expect-error
const response = await migrationRulesRoutes.start({
expectStatusCode: 404,
payload: {
connector_id: 'preconfigured-bedrock',
},
});
expect((response.body as unknown as SiemMigrationsAPIErrorResponse).message).to.eql(
'No Migration found with id: undefined'
);
});
});
});
};

View file

@ -9,9 +9,9 @@ import expect from 'expect';
import { v4 as uuidv4 } from 'uuid';
import {
createMigrationRules,
deleteAllMigrationRules,
deleteAllRuleMigrations,
getMigrationRuleDocuments,
migrationRulesRouteHelpersFactory,
ruleMigrationRouteHelpersFactory,
statsOverrideCallbackFactory,
} from '../../utils';
import { FtrProviderContext } from '../../../../ftr_provider_context';
@ -19,11 +19,11 @@ import { FtrProviderContext } from '../../../../ftr_provider_context';
export default ({ getService }: FtrProviderContext) => {
const es = getService('es');
const supertest = getService('supertest');
const migrationRulesRoutes = migrationRulesRouteHelpersFactory(supertest);
const migrationRulesRoutes = ruleMigrationRouteHelpersFactory(supertest);
describe('@ess @serverless @serverlessQA Stats API', () => {
beforeEach(async () => {
await deleteAllMigrationRules(es);
await deleteAllRuleMigrations(es);
});
it('should return stats for the specific migration', async () => {
@ -62,7 +62,7 @@ export default ({ getService }: FtrProviderContext) => {
);
});
it('should return stats for the existing migrations', async () => {
it('should return stats for all existing migrations', async () => {
const migrationId1 = uuidv4();
const migrationId2 = uuidv4();
@ -141,5 +141,20 @@ export default ({ getService }: FtrProviderContext) => {
})
);
});
describe('Error handling', () => {
it('should return 404 if migration ID does not exist', async () => {
const { body } = await migrationRulesRoutes.stats({
migrationId: 'non-existing-migration-id',
expectStatusCode: 404,
});
expect(body).toMatchObject({
statusCode: 404,
error: 'Not Found',
message: 'No Migration found with id: non-existing-migration-id',
});
});
});
});
};

Some files were not shown because too many files have changed in this diff Show more