[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, ResolveTimelineResponse,
} from './timeline/resolve_timeline/resolve_timeline_route.gen'; } from './timeline/resolve_timeline/resolve_timeline_route.gen';
import type { import type {
CreateRuleMigrationRequestParamsInput,
CreateRuleMigrationRequestBodyInput,
CreateRuleMigrationResponse, CreateRuleMigrationResponse,
CreateRuleMigrationRulesRequestParamsInput,
CreateRuleMigrationRulesRequestBodyInput,
DeleteRuleMigrationRequestParamsInput,
GetAllStatsRuleMigrationResponse, GetAllStatsRuleMigrationResponse,
GetRuleMigrationRequestQueryInput,
GetRuleMigrationRequestParamsInput, GetRuleMigrationRequestParamsInput,
GetRuleMigrationResponse, GetRuleMigrationResponse,
GetRuleMigrationIntegrationsResponse, GetRuleMigrationIntegrationsResponse,
@ -388,6 +388,9 @@ import type {
GetRuleMigrationResourcesResponse, GetRuleMigrationResourcesResponse,
GetRuleMigrationResourcesMissingRequestParamsInput, GetRuleMigrationResourcesMissingRequestParamsInput,
GetRuleMigrationResourcesMissingResponse, GetRuleMigrationResourcesMissingResponse,
GetRuleMigrationRulesRequestQueryInput,
GetRuleMigrationRulesRequestParamsInput,
GetRuleMigrationRulesResponse,
GetRuleMigrationStatsRequestParamsInput, GetRuleMigrationStatsRequestParamsInput,
GetRuleMigrationStatsResponse, GetRuleMigrationStatsResponse,
GetRuleMigrationTranslationStatsRequestParamsInput, GetRuleMigrationTranslationStatsRequestParamsInput,
@ -401,8 +404,10 @@ import type {
StopRuleMigrationRequestParamsInput, StopRuleMigrationRequestParamsInput,
StopRuleMigrationResponse, StopRuleMigrationResponse,
UpdateRuleMigrationRequestParamsInput, UpdateRuleMigrationRequestParamsInput,
UpdateRuleMigrationRequestBodyInput,
UpdateRuleMigrationResponse, UpdateRuleMigrationResponse,
UpdateRuleMigrationRulesRequestParamsInput,
UpdateRuleMigrationRulesRequestBodyInput,
UpdateRuleMigrationRulesResponse,
UpsertRuleMigrationResourcesRequestParamsInput, UpsertRuleMigrationResourcesRequestParamsInput,
UpsertRuleMigrationResourcesRequestBodyInput, UpsertRuleMigrationResourcesRequestBodyInput,
UpsertRuleMigrationResourcesResponse, UpsertRuleMigrationResourcesResponse,
@ -726,13 +731,28 @@ For detailed information on Kibana actions and alerting, and additional API call
.catch(catchAxiosErrorFormatAndThrow); .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`); this.log.info(`${new Date().toISOString()} Calling API CreateRuleMigration`);
return this.kbnClient return this.kbnClient
.request<CreateRuleMigrationResponse>({ .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: { headers: {
[ELASTIC_HTTP_VERSION_HEADER]: '1', [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); .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. * Delete one or more Timelines or Timeline templates.
*/ */
@ -1496,7 +1531,7 @@ finalize it.
.catch(catchAxiosErrorFormatAndThrow); .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) { async getRuleMigration(props: GetRuleMigrationProps) {
this.log.info(`${new Date().toISOString()} Calling API GetRuleMigration`); this.log.info(`${new Date().toISOString()} Calling API GetRuleMigration`);
@ -1507,8 +1542,6 @@ finalize it.
[ELASTIC_HTTP_VERSION_HEADER]: '1', [ELASTIC_HTTP_VERSION_HEADER]: '1',
}, },
method: 'GET', method: 'GET',
query: props.query,
}) })
.catch(catchAxiosErrorFormatAndThrow); .catch(catchAxiosErrorFormatAndThrow);
} }
@ -1598,6 +1631,23 @@ finalize it.
}) })
.catch(catchAxiosErrorFormatAndThrow); .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 * 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: { headers: {
[ELASTIC_HTTP_VERSION_HEADER]: '1', [ELASTIC_HTTP_VERSION_HEADER]: '1',
}, },
method: 'PUT', method: 'POST',
body: props.body, body: props.body,
}) })
.catch(catchAxiosErrorFormatAndThrow); .catch(catchAxiosErrorFormatAndThrow);
@ -2359,7 +2409,7 @@ The difference between the `id` and `rule_id` is that the `id` is a unique rule
headers: { headers: {
[ELASTIC_HTTP_VERSION_HEADER]: '1', [ELASTIC_HTTP_VERSION_HEADER]: '1',
}, },
method: 'PUT', method: 'POST',
}) })
.catch(catchAxiosErrorFormatAndThrow); .catch(catchAxiosErrorFormatAndThrow);
} }
@ -2433,7 +2483,7 @@ The difference between the `id` and `rule_id` is that the `id` is a unique rule
.catch(catchAxiosErrorFormatAndThrow); .catch(catchAxiosErrorFormatAndThrow);
} }
/** /**
* Updates rules migrations attributes * Updates rules migrations data
*/ */
async updateRuleMigration(props: UpdateRuleMigrationProps) { async updateRuleMigration(props: UpdateRuleMigrationProps) {
this.log.info(`${new Date().toISOString()} Calling API UpdateRuleMigration`); 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: { headers: {
[ELASTIC_HTTP_VERSION_HEADER]: '1', [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, body: props.body,
}) })
.catch(catchAxiosErrorFormatAndThrow); .catch(catchAxiosErrorFormatAndThrow);
@ -2522,9 +2587,9 @@ export interface CreatePrivMonUserProps {
export interface CreateRuleProps { export interface CreateRuleProps {
body: CreateRuleRequestBodyInput; body: CreateRuleRequestBodyInput;
} }
export interface CreateRuleMigrationProps { export interface CreateRuleMigrationRulesProps {
params: CreateRuleMigrationRequestParamsInput; params: CreateRuleMigrationRulesRequestParamsInput;
body: CreateRuleMigrationRequestBodyInput; body: CreateRuleMigrationRulesRequestBodyInput;
} }
export interface CreateTimelinesProps { export interface CreateTimelinesProps {
body: CreateTimelinesRequestBodyInput; body: CreateTimelinesRequestBodyInput;
@ -2549,6 +2614,9 @@ export interface DeletePrivMonUserProps {
export interface DeleteRuleProps { export interface DeleteRuleProps {
query: DeleteRuleRequestQueryInput; query: DeleteRuleRequestQueryInput;
} }
export interface DeleteRuleMigrationProps {
params: DeleteRuleMigrationRequestParamsInput;
}
export interface DeleteTimelinesProps { export interface DeleteTimelinesProps {
body: DeleteTimelinesRequestBodyInput; body: DeleteTimelinesRequestBodyInput;
} }
@ -2654,7 +2722,6 @@ export interface GetRuleExecutionResultsProps {
params: GetRuleExecutionResultsRequestParamsInput; params: GetRuleExecutionResultsRequestParamsInput;
} }
export interface GetRuleMigrationProps { export interface GetRuleMigrationProps {
query: GetRuleMigrationRequestQueryInput;
params: GetRuleMigrationRequestParamsInput; params: GetRuleMigrationRequestParamsInput;
} }
export interface GetRuleMigrationPrebuiltRulesProps { export interface GetRuleMigrationPrebuiltRulesProps {
@ -2667,6 +2734,10 @@ export interface GetRuleMigrationResourcesProps {
export interface GetRuleMigrationResourcesMissingProps { export interface GetRuleMigrationResourcesMissingProps {
params: GetRuleMigrationResourcesMissingRequestParamsInput; params: GetRuleMigrationResourcesMissingRequestParamsInput;
} }
export interface GetRuleMigrationRulesProps {
query: GetRuleMigrationRulesRequestQueryInput;
params: GetRuleMigrationRulesRequestParamsInput;
}
export interface GetRuleMigrationStatsProps { export interface GetRuleMigrationStatsProps {
params: GetRuleMigrationStatsRequestParamsInput; params: GetRuleMigrationStatsRequestParamsInput;
} }
@ -2793,7 +2864,10 @@ export interface UpdateRuleProps {
} }
export interface UpdateRuleMigrationProps { export interface UpdateRuleMigrationProps {
params: UpdateRuleMigrationRequestParamsInput; params: UpdateRuleMigrationRequestParamsInput;
body: UpdateRuleMigrationRequestBodyInput; }
export interface UpdateRuleMigrationRulesProps {
params: UpdateRuleMigrationRulesRequestParamsInput;
body: UpdateRuleMigrationRulesRequestBodyInput;
} }
export interface UpdateWorkflowInsightProps { export interface UpdateWorkflowInsightProps {
params: UpdateWorkflowInsightRequestParamsInput; 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_ALL_STATS_PATH = `${SIEM_RULE_MIGRATIONS_PATH}/stats` as const;
export const SIEM_RULE_MIGRATIONS_INTEGRATIONS_PATH = export const SIEM_RULE_MIGRATIONS_INTEGRATIONS_PATH =
`${SIEM_RULE_MIGRATIONS_PATH}/integrations` as const; `${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_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_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_STATS_PATH = `${SIEM_RULE_MIGRATION_PATH}/stats` as const;
export const SIEM_RULE_MIGRATION_TRANSLATION_STATS_PATH = export const SIEM_RULE_MIGRATION_TRANSLATION_STATS_PATH =

View file

@ -19,9 +19,10 @@ import { ArrayFromString, BooleanFromString } from '@kbn/zod-helpers';
import { import {
RuleMigrationTaskStats, RuleMigrationTaskStats,
OriginalRule,
UpdateRuleMigrationData,
RuleMigration, RuleMigration,
OriginalRule,
RuleMigrationRule,
UpdateRuleMigrationRule,
RuleMigrationRetryFilter, RuleMigrationRetryFilter,
RuleMigrationTranslationStats, RuleMigrationTranslationStats,
PrebuiltRuleVersion, PrebuiltRuleVersion,
@ -34,18 +35,6 @@ import { RelatedIntegration } from '../../../../api/detection_engine/model/rule_
import { NonEmptyString } from '../../../../api/model/primitives.gen'; import { NonEmptyString } from '../../../../api/model/primitives.gen';
import { ConnectorId, LangSmithOptions } from '../../common.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 type CreateRuleMigrationResponse = z.infer<typeof CreateRuleMigrationResponse>;
export const CreateRuleMigrationResponse = z.object({ export const CreateRuleMigrationResponse = z.object({
/** /**
@ -54,24 +43,34 @@ export const CreateRuleMigrationResponse = z.object({
migration_id: NonEmptyString, 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 type GetAllStatsRuleMigrationResponse = z.infer<typeof GetAllStatsRuleMigrationResponse>;
export const GetAllStatsRuleMigrationResponse = z.array(RuleMigrationTaskStats); 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 type GetRuleMigrationRequestParams = z.infer<typeof GetRuleMigrationRequestParams>;
export const GetRuleMigrationRequestParams = z.object({ export const GetRuleMigrationRequestParams = z.object({
@ -80,13 +79,7 @@ export const GetRuleMigrationRequestParams = z.object({
export type GetRuleMigrationRequestParamsInput = z.input<typeof GetRuleMigrationRequestParams>; export type GetRuleMigrationRequestParamsInput = z.input<typeof GetRuleMigrationRequestParams>;
export type GetRuleMigrationResponse = z.infer<typeof GetRuleMigrationResponse>; export type GetRuleMigrationResponse = z.infer<typeof GetRuleMigrationResponse>;
export const GetRuleMigrationResponse = z.object({ export const GetRuleMigrationResponse = RuleMigration;
/**
* The total number of rules in migration.
*/
total: z.number(),
data: z.array(RuleMigration),
});
/** /**
* The map of related integrations, with the integration id as a key * The map of related integrations, with the integration id as a key
@ -173,6 +166,41 @@ export type GetRuleMigrationResourcesMissingResponse = z.infer<
typeof GetRuleMigrationResourcesMissingResponse typeof GetRuleMigrationResourcesMissingResponse
>; >;
export const GetRuleMigrationResourcesMissingResponse = z.array(RuleMigrationResourceBase); 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 type GetRuleMigrationStatsRequestParams = z.infer<typeof GetRuleMigrationStatsRequestParams>;
export const GetRuleMigrationStatsRequestParams = z.object({ export const GetRuleMigrationStatsRequestParams = z.object({
@ -275,12 +303,29 @@ export type UpdateRuleMigrationRequestParamsInput = z.input<
typeof UpdateRuleMigrationRequestParams 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 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. * Indicates rules migrations have been updated.
*/ */

View file

@ -44,21 +44,113 @@ paths:
additionalProperties: additionalProperties:
$ref: '../../../../../common/api/detection_engine/model/rule_schema/common_attributes.schema.yaml#/components/schemas/RelatedIntegration' $ref: '../../../../../common/api/detection_engine/model/rule_schema/common_attributes.schema.yaml#/components/schemas/RelatedIntegration'
## Specific rule migration APIs /internal/siem_migrations/rules:
put:
/internal/siem_migrations/rules/{migration_id}:
post:
summary: Creates a new rule migration summary: Creates a new rule migration
operationId: CreateRuleMigration operationId: "CreateRuleMigration"
x-codegen-enabled: true x-codegen-enabled: true
x-internal: 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: tags:
- SIEM Rule Migrations - SIEM Rule Migrations
parameters: parameters:
- name: migration_id - name: migration_id
in: path 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: schema:
description: The migration id to create rules for description: The migration id to create rules for
$ref: '../../../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' $ref: '../../../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString'
@ -72,61 +164,13 @@ paths:
$ref: '../../rule_migration.schema.yaml#/components/schemas/OriginalRule' $ref: '../../rule_migration.schema.yaml#/components/schemas/OriginalRule'
responses: responses:
200: 200:
description: Indicates migration have been created correctly. description: Indicates rules have been added to the migration successfully.
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.
get: get:
summary: Retrieves all the rules of a migration summary: Retrieves all the rules of a migration
operationId: GetRuleMigration operationId: GetRuleMigrationRules
x-codegen-enabled: true x-codegen-enabled: true
x-internal: 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: tags:
- SIEM Rule Migrations - SIEM Rule Migrations
parameters: parameters:
@ -202,7 +246,6 @@ paths:
required: false required: false
schema: schema:
type: boolean type: boolean
responses: responses:
200: 200:
description: Indicates rule migration have been retrieved correctly. description: Indicates rule migration have been retrieved correctly.
@ -220,9 +263,45 @@ paths:
data: data:
type: array type: array
items: items:
$ref: '../../rule_migration.schema.yaml#/components/schemas/RuleMigration' $ref: '../../rule_migration.schema.yaml#/components/schemas/RuleMigrationRule'
204: 404:
description: Indicates the migration id was not found. 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: /internal/siem_migrations/rules/{migration_id}/install:
post: post:
@ -269,7 +348,7 @@ paths:
description: Indicates the number of successfully installed migration rules. description: Indicates the number of successfully installed migration rules.
/internal/siem_migrations/rules/{migration_id}/start: /internal/siem_migrations/rules/{migration_id}/start:
put: post:
summary: Starts a rule migration summary: Starts a rule migration
operationId: StartRuleMigration operationId: StartRuleMigration
x-codegen-enabled: true x-codegen-enabled: true
@ -368,7 +447,7 @@ paths:
description: Indicates the migration id was not found. description: Indicates the migration id was not found.
/internal/siem_migrations/rules/{migration_id}/stop: /internal/siem_migrations/rules/{migration_id}/stop:
put: post:
summary: Stops an existing rule migration summary: Stops an existing rule migration
operationId: StopRuleMigration operationId: StopRuleMigration
x-codegen-enabled: true x-codegen-enabled: true
@ -427,7 +506,6 @@ paths:
$ref: '../../rule_migration.schema.yaml#/components/schemas/PrebuiltRuleVersion' $ref: '../../rule_migration.schema.yaml#/components/schemas/PrebuiltRuleVersion'
# Rule migration resources APIs # Rule migration resources APIs
/internal/siem_migrations/rules/{migration_id}/resources: /internal/siem_migrations/rules/{migration_id}/resources:
post: post:
summary: Creates or updates rule migration resources for a migration summary: Creates or updates rule migration resources for a migration

View file

@ -141,6 +141,34 @@ export const PrebuiltRuleVersion = z.object({
current: RuleResponse.optional(), 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. * The rule translation result.
*/ */
@ -185,8 +213,8 @@ export const RuleMigrationComments = z.array(RuleMigrationComment);
/** /**
* The rule migration document object. * The rule migration document object.
*/ */
export type RuleMigrationData = z.infer<typeof RuleMigrationData>; export type RuleMigrationRuleData = z.infer<typeof RuleMigrationRuleData>;
export const RuleMigrationData = z.object({ export const RuleMigrationRuleData = z.object({
/** /**
* The moment of creation * The moment of creation
*/ */
@ -232,15 +260,15 @@ export const RuleMigrationData = z.object({
/** /**
* The rule migration document object. * The rule migration document object.
*/ */
export type RuleMigration = z.infer<typeof RuleMigration>; export type RuleMigrationRule = z.infer<typeof RuleMigrationRule>;
export const RuleMigration = z export const RuleMigrationRule = z
.object({ .object({
/** /**
* The rule migration id * The rule migration id
*/ */
id: NonEmptyString, id: NonEmptyString,
}) })
.merge(RuleMigrationData); .merge(RuleMigrationRuleData);
/** /**
* The status of the migration task. * The status of the migration task.
@ -363,8 +391,8 @@ export const RuleMigrationTranslationStats = z.object({
/** /**
* The rule migration data object for rule update operation * The rule migration data object for rule update operation
*/ */
export type UpdateRuleMigrationData = z.infer<typeof UpdateRuleMigrationData>; export type UpdateRuleMigrationRule = z.infer<typeof UpdateRuleMigrationRule>;
export const UpdateRuleMigrationData = z.object({ export const UpdateRuleMigrationRule = z.object({
/** /**
* The rule migration id * 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' $ref: '../../../common/api/detection_engine/model/rule_schema/rule_schemas.schema.yaml#/components/schemas/RuleResponse'
RuleMigration: RuleMigration:
description: The rule migration document object. description: The rule migration object with its settings.
allOf: allOf:
- type: object - type: object
required: required:
@ -129,6 +129,33 @@ components:
- $ref: '#/components/schemas/RuleMigrationData' - $ref: '#/components/schemas/RuleMigrationData'
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 type: object
description: The rule migration document object. description: The rule migration document object.
required: required:
@ -331,7 +358,7 @@ components:
description: The comments for the migration description: The comments for the migration
$ref: '#/components/schemas/RuleMigrationComment' $ref: '#/components/schemas/RuleMigrationComment'
UpdateRuleMigrationData: UpdateRuleMigrationRule:
type: object type: object
description: The rule migration data object for rule update operation description: The rule migration data object for rule update operation
required: required:

View file

@ -68,7 +68,11 @@ export const RuleMigrationsPanels = React.memo<RuleMigrationsPanelsProps>(
</EuiFlexItem> </EuiFlexItem>
{latestMigrationsStats.map((migrationStats) => ( {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.READY ||
migrationStats.status === SiemMigrationTaskStatus.STOPPED) && ( migrationStats.status === SiemMigrationTaskStatus.STOPPED) && (
<MigrationReadyPanel migrationStats={migrationStats} /> <MigrationReadyPanel migrationStats={migrationStats} />

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -28,7 +28,7 @@ import {
import type { EuiTabbedContentTab, EuiTabbedContentProps, EuiFlyoutProps } from '@elastic/eui'; import type { EuiTabbedContentTab, EuiTabbedContentProps, EuiFlyoutProps } from '@elastic/eui';
import { RuleTranslationResult } from '../../../../../common/siem_migrations/constants'; 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 { useAppToasts } from '../../../../common/hooks/use_app_toasts';
import { import {
RuleOverviewTab, RuleOverviewTab,
@ -70,7 +70,7 @@ export const TabContentPadding: FC<PropsWithChildren<unknown>> = ({ children })
); );
interface MigrationRuleDetailsFlyoutProps { interface MigrationRuleDetailsFlyoutProps {
ruleMigration: RuleMigration; migrationRule: RuleMigrationRule;
ruleActions?: React.ReactNode; ruleActions?: React.ReactNode;
matchedPrebuiltRule?: RuleResponse; matchedPrebuiltRule?: RuleResponse;
size?: EuiFlyoutProps['size']; size?: EuiFlyoutProps['size'];
@ -82,7 +82,7 @@ interface MigrationRuleDetailsFlyoutProps {
export const MigrationRuleDetailsFlyout: React.FC<MigrationRuleDetailsFlyoutProps> = React.memo( export const MigrationRuleDetailsFlyout: React.FC<MigrationRuleDetailsFlyoutProps> = React.memo(
({ ({
ruleActions, ruleActions,
ruleMigration, migrationRule,
matchedPrebuiltRule, matchedPrebuiltRule,
size = 'm', size = 'm',
extraTabs = [], extraTabs = [],
@ -93,7 +93,7 @@ export const MigrationRuleDetailsFlyout: React.FC<MigrationRuleDetailsFlyoutProp
const { expandedOverviewSections, toggleOverviewSection } = useOverviewTabSections(); const { expandedOverviewSections, toggleOverviewSection } = useOverviewTabSections();
const { mutateAsync: updateMigrationRule } = useUpdateMigrationRule(ruleMigration); const { mutateAsync: updateMigrationRule } = useUpdateMigrationRule(migrationRule);
const [isUpdating, setIsUpdating] = useState(false); const [isUpdating, setIsUpdating] = useState(false);
const isLoading = isDataLoading || isUpdating; const isLoading = isDataLoading || isUpdating;
@ -106,7 +106,7 @@ export const MigrationRuleDetailsFlyout: React.FC<MigrationRuleDetailsFlyoutProp
setIsUpdating(true); setIsUpdating(true);
try { try {
await updateMigrationRule({ await updateMigrationRule({
id: ruleMigration.id, id: migrationRule.id,
elastic_rule: { elastic_rule: {
title: ruleName, title: ruleName,
query: ruleQuery, query: ruleQuery,
@ -119,16 +119,16 @@ export const MigrationRuleDetailsFlyout: React.FC<MigrationRuleDetailsFlyoutProp
setIsUpdating(false); setIsUpdating(false);
} }
}, },
[isLoading, updateMigrationRule, ruleMigration, addError] [isLoading, updateMigrationRule, migrationRule, addError]
); );
const ruleDetailsToOverview = useMemo(() => { const ruleDetailsToOverview = useMemo(() => {
const elasticRule = ruleMigration?.elastic_rule; const elasticRule = migrationRule?.elastic_rule;
if (isMigrationCustomRule(elasticRule)) { if (isMigrationCustomRule(elasticRule)) {
return convertMigrationCustomRuleToSecurityRulePayload(elasticRule, false); return convertMigrationCustomRuleToSecurityRulePayload(elasticRule, false);
} }
return matchedPrebuiltRule; return matchedPrebuiltRule;
}, [ruleMigration, matchedPrebuiltRule]); }, [migrationRule, matchedPrebuiltRule]);
const translationTab: EuiTabbedContentTab = useMemo( const translationTab: EuiTabbedContentTab = useMemo(
() => ({ () => ({
@ -137,14 +137,14 @@ export const MigrationRuleDetailsFlyout: React.FC<MigrationRuleDetailsFlyoutProp
content: ( content: (
<TabContentPadding> <TabContentPadding>
<TranslationTab <TranslationTab
ruleMigration={ruleMigration} migrationRule={migrationRule}
matchedPrebuiltRule={matchedPrebuiltRule} matchedPrebuiltRule={matchedPrebuiltRule}
onTranslationUpdate={handleTranslationUpdate} onTranslationUpdate={handleTranslationUpdate}
/> />
</TabContentPadding> </TabContentPadding>
), ),
}), }),
[ruleMigration, handleTranslationUpdate, matchedPrebuiltRule] [migrationRule, handleTranslationUpdate, matchedPrebuiltRule]
); );
const overviewTab: EuiTabbedContentTab = useMemo( const overviewTab: EuiTabbedContentTab = useMemo(
@ -167,14 +167,14 @@ export const MigrationRuleDetailsFlyout: React.FC<MigrationRuleDetailsFlyoutProp
)} )}
</TabContentPadding> </TabContentPadding>
), ),
disabled: ruleMigration.translation_result === RuleTranslationResult.UNTRANSLATABLE, disabled: migrationRule.translation_result === RuleTranslationResult.UNTRANSLATABLE,
}), }),
[ [
ruleDetailsToOverview, ruleDetailsToOverview,
size, size,
expandedOverviewSections, expandedOverviewSections,
toggleOverviewSection, toggleOverviewSection,
ruleMigration.translation_result, migrationRule.translation_result,
] ]
); );
@ -184,11 +184,11 @@ export const MigrationRuleDetailsFlyout: React.FC<MigrationRuleDetailsFlyoutProp
name: i18n.SUMMARY_TAB_LABEL, name: i18n.SUMMARY_TAB_LABEL,
content: ( content: (
<TabContentPadding> <TabContentPadding>
<SummaryTab ruleMigration={ruleMigration} /> <SummaryTab migrationRule={migrationRule} />
</TabContentPadding> </TabContentPadding>
), ),
}), }),
[ruleMigration] [migrationRule]
); );
const tabs = useMemo(() => { const tabs = useMemo(() => {
@ -237,12 +237,12 @@ export const MigrationRuleDetailsFlyout: React.FC<MigrationRuleDetailsFlyoutProp
<EuiTitle size="m"> <EuiTitle size="m">
<h2 id={migrationsRulesFlyoutTitleId}> <h2 id={migrationsRulesFlyoutTitleId}>
{ruleDetailsToOverview?.name ?? {ruleDetailsToOverview?.name ??
ruleMigration.original_rule.title ?? migrationRule.original_rule.title ??
i18n.UNKNOWN_MIGRATION_RULE_TITLE} i18n.UNKNOWN_MIGRATION_RULE_TITLE}
</h2> </h2>
</EuiTitle> </EuiTitle>
<EuiSpacer size="s" /> <EuiSpacer size="s" />
<UpdatedByLabel ruleMigration={ruleMigration} /> <UpdatedByLabel migrationRule={migrationRule} />
</EuiFlyoutHeader> </EuiFlyoutHeader>
<EuiFlyoutBody <EuiFlyoutBody
// EUI TODO: We need to set transform to 'none' to avoid drag/drop issues in the flyout caused by the // 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 { UserAvatar } from '@kbn/user-profile-components';
import { USER_AVATAR_ITEM_TEST_ID } from '../../../../../../common/components/user_profiles/test_ids'; 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 { 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 { import {
RuleTranslationResult, RuleTranslationResult,
SIEM_MIGRATIONS_ASSISTANT_USER, SIEM_MIGRATIONS_ASSISTANT_USER,
@ -21,19 +21,19 @@ import {
import * as i18n from './translations'; import * as i18n from './translations';
interface SummaryTabProps { 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>>(() => { const userProfileIds = useMemo<Set<string>>(() => {
if (!ruleMigration.comments) { if (!migrationRule.comments) {
return new Set(); 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); if (createdBy !== SIEM_MIGRATIONS_ASSISTANT_USER) acc.add(createdBy);
return acc; return acc;
}, new Set<string>()); }, new Set<string>());
}, [ruleMigration.comments]); }, [migrationRule.comments]);
const { isLoading: isLoadingUserProfiles, data: userProfiles } = useBulkGetUserProfiles({ const { isLoading: isLoadingUserProfiles, data: userProfiles } = useBulkGetUserProfiles({
uids: userProfileIds, uids: userProfileIds,
}); });
@ -42,7 +42,7 @@ export const SummaryTab: React.FC<SummaryTabProps> = React.memo(({ ruleMigration
if (isLoadingUserProfiles) { if (isLoadingUserProfiles) {
return undefined; return undefined;
} }
return ruleMigration.comments?.map( return migrationRule.comments?.map(
({ message, created_at: createdAt, created_by: createdBy }) => { ({ message, created_at: createdAt, created_by: createdBy }) => {
const profile = userProfiles?.find(({ uid }) => uid === createdBy); const profile = userProfiles?.find(({ uid }) => uid === createdBy);
const isCreatedByAssistant = createdBy === SIEM_MIGRATIONS_ASSISTANT_USER || !profile; const isCreatedByAssistant = createdBy === SIEM_MIGRATIONS_ASSISTANT_USER || !profile;
@ -63,7 +63,7 @@ export const SummaryTab: React.FC<SummaryTabProps> = React.memo(({ ruleMigration
/> />
), ),
event: event:
ruleMigration.translation_result === RuleTranslationResult.UNTRANSLATABLE migrationRule.translation_result === RuleTranslationResult.UNTRANSLATABLE
? i18n.COMMENT_EVENT_UNTRANSLATABLE ? i18n.COMMENT_EVENT_UNTRANSLATABLE
: i18n.COMMENT_EVENT_TRANSLATED, : i18n.COMMENT_EVENT_TRANSLATED,
timestamp: moment(createdAt).format('ll'), // Date formats https://momentjs.com/docs/#/displaying/format/ 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, isLoadingUserProfiles,
ruleMigration.comments, migrationRule.comments,
ruleMigration.translation_result, migrationRule.translation_result,
userProfiles, userProfiles,
]); ]);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -9,7 +9,7 @@ import React from 'react';
import type { Severity } from '@kbn/securitysolution-io-ts-alerting-types'; import type { Severity } from '@kbn/securitysolution-io-ts-alerting-types';
import { EuiHorizontalRule, EuiText } from '@elastic/eui'; import { EuiHorizontalRule, EuiText } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react'; 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 { SiemMigrationStatus } from '../../../../../common/siem_migrations/constants';
import { SeverityBadge } from '../../../../common/components/severity_badge'; import { SeverityBadge } from '../../../../common/components/severity_badge';
import { COLUMN_EMPTY_VALUE, type TableColumn } from './constants'; 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 ? ( rule.status === SiemMigrationStatus.FAILED ? (
<>{COLUMN_EMPTY_VALUE}</> <>{COLUMN_EMPTY_VALUE}</>
) : ( ) : (

View file

@ -9,7 +9,7 @@ import React from 'react';
import { FormattedMessage } from '@kbn/i18n-react'; import { FormattedMessage } from '@kbn/i18n-react';
import { EuiHorizontalRule, EuiText } from '@elastic/eui'; import { EuiHorizontalRule, EuiText } from '@elastic/eui';
import { RuleTranslationResult } from '../../../../../common/siem_migrations/constants'; 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 * as i18n from './translations';
import type { TableColumn } from './constants'; import type { TableColumn } from './constants';
import { StatusBadge } from '../status_badge'; 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, sortable: true,
truncateText: true, truncateText: true,
width: '15%', width: '15%',

View file

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

View file

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

View file

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

View file

@ -7,7 +7,7 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import type { RelatedIntegration } from '../../../../common/api/detection_engine'; 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 type { TableColumn } from '../components/rules_table_columns';
import { import {
createActionsColumn, createActionsColumn,
@ -27,8 +27,8 @@ export const useMigrationRulesTableColumns = ({
getMigrationRuleData, getMigrationRuleData,
}: { }: {
disableActions?: boolean; disableActions?: boolean;
openMigrationRuleDetails: (rule: RuleMigration) => void; openMigrationRuleDetails: (rule: RuleMigrationRule) => void;
installMigrationRule: (migrationRule: RuleMigration, enable?: boolean) => void; installMigrationRule: (migrationRule: RuleMigrationRule, enable?: boolean) => void;
getMigrationRuleData: ( getMigrationRuleData: (
ruleId: string ruleId: string
) => { relatedIntegrations?: RelatedIntegration[]; isIntegrationsLoading?: boolean } | undefined; ) => { 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 { SIEM_RULE_MIGRATION_PATH } from '../../../../common/siem_migrations/constants';
import { useAppToasts } from '../../../common/hooks/use_app_toasts'; import { useAppToasts } from '../../../common/hooks/use_app_toasts';
import * as i18n from './translations'; import * as i18n from './translations';
import { getRuleMigrations } from '../api'; import { getMigrationRules } from '../api';
import { DEFAULT_QUERY_OPTIONS } from './constants'; import { DEFAULT_QUERY_OPTIONS } from './constants';
export const useGetMigrationRules = (params: { export const useGetMigrationRules = (params: {
@ -33,9 +33,9 @@ export const useGetMigrationRules = (params: {
return useQuery( return useQuery(
['GET', SPECIFIC_MIGRATION_PATH, params], ['GET', SPECIFIC_MIGRATION_PATH, params],
async ({ signal }) => { 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, ...DEFAULT_QUERY_OPTIONS,

View file

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

View file

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

View file

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

View file

@ -7,8 +7,8 @@
import { useCallback, useReducer } from 'react'; import { useCallback, useReducer } from 'react';
import { i18n } from '@kbn/i18n'; 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 { 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 { useKibana } from '../../../../common/lib/kibana/kibana_react';
import { reducer, initialState } from './common/api_request_reducer'; 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' } { 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 type OnSuccess = (migrationStats: RuleMigrationTaskStats) => void;
export const useCreateMigration = (onSuccess: OnSuccess) => { export const useCreateMigration = (onSuccess: OnSuccess) => {

View file

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

View file

@ -19,7 +19,7 @@ import type {
RuleMigrationTaskStats, RuleMigrationTaskStats,
} from '../../../../common/siem_migrations/model/rule_migration.gen'; } from '../../../../common/siem_migrations/model/rule_migration.gen';
import type { import type {
CreateRuleMigrationRequestBody, CreateRuleMigrationRulesRequestBody,
GetRuleMigrationStatsResponse, GetRuleMigrationStatsResponse,
StartRuleMigrationResponse, StartRuleMigrationResponse,
UpsertRuleMigrationResourcesRequestBody, UpsertRuleMigrationResourcesRequestBody,
@ -39,6 +39,7 @@ import {
getMissingResources, getMissingResources,
upsertMigrationResources, upsertMigrationResources,
getIntegrations, getIntegrations,
addRulesToMigration,
} from '../api'; } from '../api';
import { import {
getMissingCapabilities, getMissingCapabilities,
@ -119,22 +120,36 @@ export class SiemRulesMigrationsService {
}); });
} }
public async createRuleMigration(body: CreateRuleMigrationRequestBody): Promise<string> { public async addRulesToMigration(
const rulesCount = body.length; 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) { if (rulesCount === 0) {
throw new Error(i18n.EMPTY_RULES_ERROR); throw new Error(i18n.EMPTY_RULES_ERROR);
} }
try { try {
let migrationId: string | undefined; // create the migration
// Batching creation to avoid hitting the max payload size limit of the API const { migration_id: migrationId } = await createRuleMigration({});
for (let i = 0; i < rulesCount; i += CREATE_MIGRATION_BODY_BATCH_SIZE) {
const bodyBatch = body.slice(i, i + CREATE_MIGRATION_BODY_BATCH_SIZE); await this.addRulesToMigration(migrationId, data);
const response = await createRuleMigration({ migrationId, body: bodyBatch });
migrationId = response.migration_id;
}
this.telemetry.reportSetupMigrationCreated({ migrationId, rulesCount }); this.telemetry.reportSetupMigrationCreated({ migrationId, rulesCount });
return migrationId as string; return migrationId;
} catch (error) { } catch (error) {
this.telemetry.reportSetupMigrationCreated({ rulesCount, error }); this.telemetry.reportSetupMigrationCreated({ rulesCount, error });
throw 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 { siemMigrationEventNames } from '../../../common/lib/telemetry/events/siem_migrations';
import type { SiemMigrationRetryFilter } from '../../../../common/siem_migrations/constants'; import type { SiemMigrationRetryFilter } from '../../../../common/siem_migrations/constants';
import type { import type {
RuleMigration,
RuleMigrationResourceType, RuleMigrationResourceType,
RuleMigrationRule,
} from '../../../../common/siem_migrations/model/rule_migration.gen'; } from '../../../../common/siem_migrations/model/rule_migration.gen';
import type { TelemetryServiceStart } from '../../../common/lib/telemetry'; import type { TelemetryServiceStart } from '../../../common/lib/telemetry';
import type { import type {
@ -125,36 +125,36 @@ export class SiemRulesMigrationsTelemetry {
// Translated rule actions // Translated rule actions
reportTranslatedRuleUpdate = (params: { ruleMigration: RuleMigration; error?: Error }) => { reportTranslatedRuleUpdate = (params: { migrationRule: RuleMigrationRule; error?: Error }) => {
const { ruleMigration, error } = params; const { migrationRule, error } = params;
this.telemetryService.reportEvent(SiemMigrationsEventTypes.TranslatedRuleUpdate, { this.telemetryService.reportEvent(SiemMigrationsEventTypes.TranslatedRuleUpdate, {
eventName: siemMigrationEventNames[SiemMigrationsEventTypes.TranslatedRuleUpdate], eventName: siemMigrationEventNames[SiemMigrationsEventTypes.TranslatedRuleUpdate],
migrationId: ruleMigration.migration_id, migrationId: migrationRule.migration_id,
ruleMigrationId: ruleMigration.id, ruleMigrationId: migrationRule.id,
...this.getBaseResultParams(error), ...this.getBaseResultParams(error),
}); });
}; };
reportTranslatedRuleInstall = (params: { reportTranslatedRuleInstall = (params: {
ruleMigration: RuleMigration; migrationRule: RuleMigrationRule;
enabled: boolean; enabled: boolean;
error?: Error; error?: Error;
}) => { }) => {
const { ruleMigration, enabled, error } = params; const { migrationRule, enabled, error } = params;
const eventParams: ReportTranslatedRuleInstallActionParams = { const eventParams: ReportTranslatedRuleInstallActionParams = {
eventName: siemMigrationEventNames[SiemMigrationsEventTypes.TranslatedRuleInstall], eventName: siemMigrationEventNames[SiemMigrationsEventTypes.TranslatedRuleInstall],
migrationId: ruleMigration.migration_id, migrationId: migrationRule.migration_id,
ruleMigrationId: ruleMigration.id, ruleMigrationId: migrationRule.id,
author: 'custom', author: 'custom',
enabled, enabled,
...this.getBaseResultParams(error), ...this.getBaseResultParams(error),
}; };
if (ruleMigration.elastic_rule?.prebuilt_rule_id) { if (migrationRule.elastic_rule?.prebuilt_rule_id) {
eventParams.author = 'elastic'; eventParams.author = 'elastic';
eventParams.prebuiltRule = { eventParams.prebuiltRule = {
id: ruleMigration.elastic_rule.prebuilt_rule_id, id: migrationRule.elastic_rule.prebuilt_rule_id,
title: ruleMigration.elastic_rule.title, title: migrationRule.elastic_rule.title,
}; };
} }

View file

@ -6,16 +6,9 @@
*/ */
import type { IKibanaResponse, Logger } from '@kbn/core/server'; import type { IKibanaResponse, Logger } from '@kbn/core/server';
import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; import { SIEM_RULE_MIGRATIONS_PATH } from '../../../../../common/siem_migrations/constants';
import { SIEM_RULE_MIGRATION_CREATE_PATH } from '../../../../../common/siem_migrations/constants'; import { type CreateRuleMigrationResponse } from '../../../../../common/siem_migrations/model/api/rules/rule_migration.gen';
import {
CreateRuleMigrationRequestBody,
CreateRuleMigrationRequestParams,
type CreateRuleMigrationResponse,
} 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 { SecuritySolutionPluginRouter } from '../../../../types';
import type { CreateRuleMigrationInput } from '../data/rule_migrations_data_rules_client';
import { SiemMigrationAuditLogger } from './util/audit'; import { SiemMigrationAuditLogger } from './util/audit';
import { authz } from './util/authz'; import { authz } from './util/authz';
import { withLicense } from './util/with_license'; import { withLicense } from './util/with_license';
@ -25,67 +18,31 @@ export const registerSiemRuleMigrationsCreateRoute = (
logger: Logger logger: Logger
) => { ) => {
router.versioned router.versioned
.post({ .put({
path: SIEM_RULE_MIGRATION_CREATE_PATH, path: SIEM_RULE_MIGRATIONS_PATH,
access: 'internal', access: 'internal',
security: { authz }, security: { authz },
}) })
.addVersion( .addVersion(
{ {
version: '1', version: '1',
validate: { // no request body or params to validate
request: { validate: false,
body: buildRouteValidationWithZod(CreateRuleMigrationRequestBody),
params: buildRouteValidationWithZod(CreateRuleMigrationRequestParams),
},
},
}, },
withLicense( withLicense(
async (context, req, res): Promise<IKibanaResponse<CreateRuleMigrationResponse>> => { async (context, req, res): Promise<IKibanaResponse<CreateRuleMigrationResponse>> => {
const originalRules = req.body;
const siemMigrationAuditLogger = new SiemMigrationAuditLogger(context.securitySolution); const siemMigrationAuditLogger = new SiemMigrationAuditLogger(context.securitySolution);
const providedMigrationId = req.params?.migration_id;
try { try {
const [firstOriginalRule] = originalRules;
if (!firstOriginalRule) {
return res.noContent();
}
const ctx = await context.resolve(['securitySolution']); const ctx = await context.resolve(['securitySolution']);
const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient(); const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient();
await siemMigrationAuditLogger.logCreateMigration({ migrationId: providedMigrationId }); await siemMigrationAuditLogger.logCreateMigration();
let migrationId: string; const migrationId = await ruleMigrationsClient.data.migrations.create();
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);
}
return res.ok({ body: { migration_id: migrationId } }); return res.ok({ body: { migration_id: migrationId } });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);
await siemMigrationAuditLogger.logCreateMigration({ await siemMigrationAuditLogger.logCreateMigration({
migrationId: providedMigrationId,
error, error,
}); });
return res.badRequest({ body: error.message }); return res.badRequest({ body: error.message });

View file

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

View file

@ -8,16 +8,13 @@
import type { IKibanaResponse, Logger } from '@kbn/core/server'; import type { IKibanaResponse, Logger } from '@kbn/core/server';
import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; import { buildRouteValidationWithZod } from '@kbn/zod-helpers';
import { SIEM_RULE_MIGRATION_PATH } from '../../../../../common/siem_migrations/constants'; import { SIEM_RULE_MIGRATION_PATH } from '../../../../../common/siem_migrations/constants';
import { import type { GetRuleMigrationResponse } from '../../../../../common/siem_migrations/model/api/rules/rule_migration.gen';
GetRuleMigrationRequestParams, import { GetRuleMigrationRequestParams } from '../../../../../common/siem_migrations/model/api/rules/rule_migration.gen';
GetRuleMigrationRequestQuery,
type GetRuleMigrationResponse,
} from '../../../../../common/siem_migrations/model/api/rules/rule_migration.gen';
import type { SecuritySolutionPluginRouter } from '../../../../types'; import type { SecuritySolutionPluginRouter } from '../../../../types';
import type { RuleMigrationGetOptions } from '../data/rule_migrations_data_rules_client';
import { SiemMigrationAuditLogger } from './util/audit'; import { SiemMigrationAuditLogger } from './util/audit';
import { authz } from './util/authz'; import { authz } from './util/authz';
import { withLicense } from './util/with_license'; import { withLicense } from './util/with_license';
import { MIGRATION_ID_NOT_FOUND } from './util/with_existing_migration_id';
export const registerSiemRuleMigrationsGetRoute = ( export const registerSiemRuleMigrationsGetRoute = (
router: SecuritySolutionPluginRouter, router: SecuritySolutionPluginRouter,
@ -35,42 +32,37 @@ export const registerSiemRuleMigrationsGetRoute = (
validate: { validate: {
request: { request: {
params: buildRouteValidationWithZod(GetRuleMigrationRequestParams), params: buildRouteValidationWithZod(GetRuleMigrationRequestParams),
query: buildRouteValidationWithZod(GetRuleMigrationRequestQuery),
}, },
}, },
}, },
withLicense(async (context, req, res): Promise<IKibanaResponse<GetRuleMigrationResponse>> => { withLicense(async (context, req, res): Promise<IKibanaResponse<GetRuleMigrationResponse>> => {
const { migration_id: migrationId } = req.params;
const siemMigrationAuditLogger = new SiemMigrationAuditLogger(context.securitySolution); const siemMigrationAuditLogger = new SiemMigrationAuditLogger(context.securitySolution);
const { migration_id: migrationId } = req.params;
try { try {
const ctx = await context.resolve(['securitySolution']); const ctx = await context.resolve(['securitySolution']);
const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient(); 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 }); 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) { } catch (error) {
logger.error(error); logger.error(error);
await siemMigrationAuditLogger.logGetMigration({ migrationId, error }); await siemMigrationAuditLogger.logGetMigration({
migrationId,
error,
});
return res.badRequest({ body: error.message }); 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 { ConfigType } from '../../../../config';
import type { SecuritySolutionPluginRouter } from '../../../../types'; import type { SecuritySolutionPluginRouter } from '../../../../types';
import { registerSiemRuleMigrationsCreateRoute } from './create'; import { registerSiemRuleMigrationsCreateRoute } from './create';
import { registerSiemRuleMigrationsUpdateRoute } from './update'; import { registerSiemRuleMigrationsUpdateRulesRoute } from './rules/update';
import { registerSiemRuleMigrationsGetRoute } from './get'; import { registerSiemRuleMigrationsGetRoute } from './get';
import { registerSiemRuleMigrationsStartRoute } from './start'; import { registerSiemRuleMigrationsStartRoute } from './start';
import { registerSiemRuleMigrationsStatsRoute } from './stats'; import { registerSiemRuleMigrationsStatsRoute } from './stats';
@ -24,27 +24,45 @@ import { registerSiemRuleMigrationsPrebuiltRulesRoute } from './get_prebuilt_rul
import { registerSiemRuleMigrationsIntegrationsRoute } from './get_integrations'; import { registerSiemRuleMigrationsIntegrationsRoute } from './get_integrations';
import { registerSiemRuleMigrationsGetMissingPrivilegesRoute } from './privileges/get_missing_privileges'; import { registerSiemRuleMigrationsGetMissingPrivilegesRoute } from './privileges/get_missing_privileges';
import { registerSiemRuleMigrationsEvaluateRoute } from './evaluation/evaluate'; import { registerSiemRuleMigrationsEvaluateRoute } from './evaluation/evaluate';
import { registerSiemRuleMigrationsCreateRulesRoute } from './rules/create';
import { registerSiemRuleMigrationsGetRulesRoute } from './rules/get';
import { registerSiemRuleMigrationsDeleteRoute } from './delete';
export const registerSiemRuleMigrationsRoutes = ( export const registerSiemRuleMigrationsRoutes = (
router: SecuritySolutionPluginRouter, router: SecuritySolutionPluginRouter,
config: ConfigType, config: ConfigType,
logger: Logger logger: Logger
) => { ) => {
/** Rules Migrations */
registerSiemRuleMigrationsCreateRoute(router, logger); 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); registerSiemRuleMigrationsStatsAllRoute(router, logger);
registerSiemRuleMigrationsPrebuiltRulesRoute(router, logger); registerSiemRuleMigrationsPrebuiltRulesRoute(router, logger);
registerSiemRuleMigrationsGetRoute(router, logger);
registerSiemRuleMigrationsStartRoute(router, logger); registerSiemRuleMigrationsStartRoute(router, logger);
registerSiemRuleMigrationsStatsRoute(router, logger); registerSiemRuleMigrationsStatsRoute(router, logger);
registerSiemRuleMigrationsTranslationStatsRoute(router, logger); registerSiemRuleMigrationsTranslationStatsRoute(router, logger);
registerSiemRuleMigrationsStopRoute(router, logger); registerSiemRuleMigrationsStopRoute(router, logger);
/** *******/
registerSiemRuleMigrationsInstallRoute(router, logger); registerSiemRuleMigrationsInstallRoute(router, logger);
registerSiemRuleMigrationsIntegrationsRoute(router, logger); registerSiemRuleMigrationsIntegrationsRoute(router, logger);
/** Resources */
registerSiemRuleMigrationsResourceUpsertRoute(router, logger); registerSiemRuleMigrationsResourceUpsertRoute(router, logger);
registerSiemRuleMigrationsResourceGetRoute(router, logger); registerSiemRuleMigrationsResourceGetRoute(router, logger);
registerSiemRuleMigrationsResourceGetMissingRoute(router, logger); registerSiemRuleMigrationsResourceGetMissingRoute(router, logger);
/** *******/
registerSiemRuleMigrationsGetMissingPrivilegesRoute(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 { getRetryFilter } from './util/retry';
import { withLicense } from './util/with_license'; import { withLicense } from './util/with_license';
import { createTracersCallbacks } from './util/tracing'; import { createTracersCallbacks } from './util/tracing';
import { withExistingMigration } from './util/with_existing_migration_id';
export const registerSiemRuleMigrationsStartRoute = ( export const registerSiemRuleMigrationsStartRoute = (
router: SecuritySolutionPluginRouter, router: SecuritySolutionPluginRouter,
logger: Logger logger: Logger
) => { ) => {
router.versioned router.versioned
.put({ .post({
path: SIEM_RULE_MIGRATION_START_PATH, path: SIEM_RULE_MIGRATION_START_PATH,
access: 'internal', access: 'internal',
security: { authz }, security: { authz },
@ -41,56 +42,63 @@ export const registerSiemRuleMigrationsStartRoute = (
}, },
}, },
withLicense( withLicense(
async (context, req, res): Promise<IKibanaResponse<StartRuleMigrationResponse>> => { withExistingMigration(
const migrationId = req.params.migration_id; async (context, req, res): Promise<IKibanaResponse<StartRuleMigrationResponse>> => {
const { const migrationId = req.params.migration_id;
langsmith_options: langsmithOptions, const {
connector_id: connectorId, langsmith_options: langsmithOptions,
retry, connector_id: connectorId,
} = req.body; retry,
} = req.body;
const siemMigrationAuditLogger = new SiemMigrationAuditLogger(context.securitySolution); const siemMigrationAuditLogger = new SiemMigrationAuditLogger(context.securitySolution);
try { 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 // Check if the connector exists and user has permissions to read it
const connector = await ctx.actions.getActionsClient().get({ id: connectorId }); const connector = await ctx.actions.getActionsClient().get({ id: connectorId });
if (!connector) { if (!connector) {
return res.badRequest({ body: `Connector with id ${connectorId} not found` }); return res.badRequest({ body: `Connector with id ${connectorId} not found` });
}
const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient();
if (retry) {
const { updated } = await ruleMigrationsClient.task.updateToRetry(
migrationId,
getRetryFilter(retry)
);
if (!updated) {
return res.ok({ body: { started: false } });
} }
const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient();
if (retry) {
const { updated } = await ruleMigrationsClient.task.updateToRetry(
migrationId,
getRetryFilter(retry)
);
if (!updated) {
return res.ok({ body: { started: false } });
}
}
const callbacks = createTracersCallbacks(langsmithOptions, logger);
const { exists, started } = await ruleMigrationsClient.task.start({
migrationId,
connectorId,
invocationConfig: { callbacks },
});
if (!exists) {
return res.notFound();
}
await siemMigrationAuditLogger.logStart({ migrationId });
return res.ok({ body: { started } });
} catch (error) {
logger.error(error);
await siemMigrationAuditLogger.logStart({ migrationId, error });
return res.badRequest({ body: error.message });
} }
const callbacks = createTracersCallbacks(langsmithOptions, logger);
const { exists, started } = await ruleMigrationsClient.task.start({
migrationId,
connectorId,
invocationConfig: { callbacks },
});
if (!exists) {
return res.notFound();
}
await siemMigrationAuditLogger.logStart({ migrationId });
return res.ok({ body: { started } });
} catch (error) {
logger.error(error);
await siemMigrationAuditLogger.logStart({ migrationId, error });
return res.badRequest({ body: error.message });
} }
} )
) )
); );
}; };

View file

@ -15,6 +15,7 @@ import { SIEM_RULE_MIGRATION_STATS_PATH } from '../../../../../common/siem_migra
import type { SecuritySolutionPluginRouter } from '../../../../types'; import type { SecuritySolutionPluginRouter } from '../../../../types';
import { authz } from './util/authz'; import { authz } from './util/authz';
import { withLicense } from './util/with_license'; import { withLicense } from './util/with_license';
import { withExistingMigration } from './util/with_existing_migration_id';
export const registerSiemRuleMigrationsStatsRoute = ( export const registerSiemRuleMigrationsStatsRoute = (
router: SecuritySolutionPluginRouter, router: SecuritySolutionPluginRouter,
@ -34,23 +35,25 @@ export const registerSiemRuleMigrationsStatsRoute = (
}, },
}, },
withLicense( withLicense(
async (context, req, res): Promise<IKibanaResponse<GetRuleMigrationStatsResponse>> => { withExistingMigration(
const migrationId = req.params.migration_id; async (context, req, res): Promise<IKibanaResponse<GetRuleMigrationStatsResponse>> => {
try { const migrationId = req.params.migration_id;
const ctx = await context.resolve(['securitySolution']); try {
const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient(); const ctx = await context.resolve(['securitySolution']);
const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient();
const stats = await ruleMigrationsClient.task.getStats(migrationId); const stats = await ruleMigrationsClient.task.getStats(migrationId);
if (stats.rules.total === 0) { if (stats.rules.total === 0) {
return res.noContent(); return res.noContent();
}
return res.ok({ body: stats });
} catch (err) {
logger.error(err);
return res.badRequest({ body: err.message });
} }
return res.ok({ body: stats });
} catch (err) {
logger.error(err);
return res.badRequest({ body: err.message });
} }
} )
) )
); );
}; };

View file

@ -16,13 +16,14 @@ import type { SecuritySolutionPluginRouter } from '../../../../types';
import { SiemMigrationAuditLogger } from './util/audit'; import { SiemMigrationAuditLogger } from './util/audit';
import { authz } from './util/authz'; import { authz } from './util/authz';
import { withLicense } from './util/with_license'; import { withLicense } from './util/with_license';
import { withExistingMigration } from './util/with_existing_migration_id';
export const registerSiemRuleMigrationsStopRoute = ( export const registerSiemRuleMigrationsStopRoute = (
router: SecuritySolutionPluginRouter, router: SecuritySolutionPluginRouter,
logger: Logger logger: Logger
) => { ) => {
router.versioned router.versioned
.put({ .post({
path: SIEM_RULE_MIGRATION_STOP_PATH, path: SIEM_RULE_MIGRATION_STOP_PATH,
access: 'internal', access: 'internal',
security: { authz }, security: { authz },
@ -35,27 +36,29 @@ export const registerSiemRuleMigrationsStopRoute = (
}, },
}, },
withLicense( withLicense(
async (context, req, res): Promise<IKibanaResponse<StopRuleMigrationResponse>> => { withExistingMigration(
const migrationId = req.params.migration_id; async (context, req, res): Promise<IKibanaResponse<StopRuleMigrationResponse>> => {
const siemMigrationAuditLogger = new SiemMigrationAuditLogger(context.securitySolution); const migrationId = req.params.migration_id;
try { const siemMigrationAuditLogger = new SiemMigrationAuditLogger(context.securitySolution);
const ctx = await context.resolve(['securitySolution']); try {
const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient(); const ctx = await context.resolve(['securitySolution']);
const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient();
const { exists, stopped } = await ruleMigrationsClient.task.stop(migrationId); const { exists, stopped } = await ruleMigrationsClient.task.stop(migrationId);
if (!exists) { if (!exists) {
return res.notFound(); return res.notFound();
}
await siemMigrationAuditLogger.logStop({ migrationId });
return res.ok({ body: { stopped } });
} catch (error) {
logger.error(error);
await siemMigrationAuditLogger.logStop({ migrationId, error });
return res.badRequest({ body: error.message });
} }
await siemMigrationAuditLogger.logStop({ migrationId });
return res.ok({ body: { stopped } });
} catch (error) {
logger.error(error);
await siemMigrationAuditLogger.logStop({ migrationId, error });
return res.badRequest({ body: error.message });
} }
} )
) )
); );
}; };

View file

@ -13,6 +13,7 @@ import { SIEM_RULE_MIGRATION_TRANSLATION_STATS_PATH } from '../../../../../commo
import type { SecuritySolutionPluginRouter } from '../../../../types'; import type { SecuritySolutionPluginRouter } from '../../../../types';
import { authz } from './util/authz'; import { authz } from './util/authz';
import { withLicense } from './util/with_license'; import { withLicense } from './util/with_license';
import { withExistingMigration } from './util/with_existing_migration_id';
export const registerSiemRuleMigrationsTranslationStatsRoute = ( export const registerSiemRuleMigrationsTranslationStatsRoute = (
router: SecuritySolutionPluginRouter, router: SecuritySolutionPluginRouter,
@ -34,27 +35,29 @@ export const registerSiemRuleMigrationsTranslationStatsRoute = (
}, },
}, },
withLicense( withLicense(
async ( withExistingMigration(
context, async (
req, context,
res req,
): Promise<IKibanaResponse<GetRuleMigrationTranslationStatsResponse>> => { res
const migrationId = req.params.migration_id; ): Promise<IKibanaResponse<GetRuleMigrationTranslationStatsResponse>> => {
try { const migrationId = req.params.migration_id;
const ctx = await context.resolve(['securitySolution']); try {
const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient(); const ctx = await context.resolve(['securitySolution']);
const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient();
const stats = await ruleMigrationsClient.data.rules.getTranslationStats(migrationId); const stats = await ruleMigrationsClient.data.rules.getTranslationStats(migrationId);
if (stats.rules.total === 0) { if (stats.rules.total === 0) {
return res.noContent(); return res.noContent();
}
return res.ok({ body: stats });
} catch (err) {
logger.error(err);
return res.badRequest({ body: err.message });
} }
return res.ok({ body: stats });
} catch (err) {
logger.error(err);
return res.badRequest({ body: err.message });
} }
} )
) )
); );
}; };

View file

@ -12,6 +12,9 @@ import type { SecuritySolutionApiRequestHandlerContext } from '../../../../..';
export enum SiemMigrationsAuditActions { export enum SiemMigrationsAuditActions {
SIEM_MIGRATION_CREATED = 'siem_migration_created', SIEM_MIGRATION_CREATED = 'siem_migration_created',
SIEM_MIGRATION_RETRIEVED = 'siem_migration_retrieved', 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_UPLOADED_RESOURCES = 'siem_migration_uploaded_resources',
SIEM_MIGRATION_RETRIEVED_RESOURCES = 'siem_migration_retrieved_resources', SIEM_MIGRATION_RETRIEVED_RESOURCES = 'siem_migration_retrieved_resources',
SIEM_MIGRATION_STARTED = 'siem_migration_started', SIEM_MIGRATION_STARTED = 'siem_migration_started',
@ -53,6 +56,9 @@ export const siemMigrationAuditEventType: Record<
[SiemMigrationsAuditActions.SIEM_MIGRATION_STOPPED]: AUDIT_TYPE.END, [SiemMigrationsAuditActions.SIEM_MIGRATION_STOPPED]: AUDIT_TYPE.END,
[SiemMigrationsAuditActions.SIEM_MIGRATION_UPDATED_RULE]: AUDIT_TYPE.CHANGE, [SiemMigrationsAuditActions.SIEM_MIGRATION_UPDATED_RULE]: AUDIT_TYPE.CHANGE,
[SiemMigrationsAuditActions.SIEM_MIGRATION_INSTALLED_RULES]: AUDIT_TYPE.CREATION, [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 { interface SiemMigrationAuditEvent {
@ -106,9 +112,9 @@ export class SiemMigrationAuditLogger {
} }
} }
public async logCreateMigration(params: { migrationId?: string; error?: Error }): Promise<void> { public async logCreateMigration(params: { error?: Error } = {}): Promise<void> {
const { migrationId, error } = params; const { error } = params;
const message = `User created a new SIEM migration with [id=${migrationId}]`; const message = `User created a new SIEM migration`;
return this.log({ return this.log({
action: SiemMigrationsAuditActions.SIEM_MIGRATION_CREATED, action: SiemMigrationsAuditActions.SIEM_MIGRATION_CREATED,
message, 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> { public async logUploadResources(params: { migrationId: string; error?: Error }): Promise<void> {
const { migrationId, error } = params; const { migrationId, error } = params;
const message = `User uploaded resources to the SIEM migration with [id=${migrationId}]`; 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 { SavedObjectsClientContract } from '@kbn/core/server';
import type { RulesClient } from '@kbn/alerting-plugin/server'; import type { RulesClient } from '@kbn/alerting-plugin/server';
import { getErrorMessage } from '../../../../../utils/error_helpers'; 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 { initPromisePool } from '../../../../../utils/promise_pool';
import type { SecuritySolutionApiRequestHandlerContext } from '../../../../..'; import type { SecuritySolutionApiRequestHandlerContext } from '../../../../..';
import { performTimelinesInstallation } from '../../../../detection_engine/prebuilt_rules/logic/perform_timelines_installation'; import { performTimelinesInstallation } from '../../../../detection_engine/prebuilt_rules/logic/perform_timelines_installation';
@ -31,7 +31,7 @@ const installPrebuiltRules = async (
rulesClient: RulesClient, rulesClient: RulesClient,
savedObjectsClient: SavedObjectsClientContract, savedObjectsClient: SavedObjectsClientContract,
detectionRulesClient: IDetectionRulesClient detectionRulesClient: IDetectionRulesClient
): Promise<{ rulesToUpdate: UpdateRuleMigrationData[]; errors: Error[] }> => { ): Promise<{ rulesToUpdate: UpdateRuleMigrationRule[]; errors: Error[] }> => {
// Get required prebuilt rules // Get required prebuilt rules
const prebuiltRulesIds = getUniquePrebuiltRuleIds(rulesToInstall); const prebuiltRulesIds = getUniquePrebuiltRuleIds(rulesToInstall);
const prebuiltRules = await getPrebuiltRules(rulesClient, savedObjectsClient, prebuiltRulesIds); const prebuiltRules = await getPrebuiltRules(rulesClient, savedObjectsClient, prebuiltRulesIds);
@ -68,7 +68,7 @@ const installPrebuiltRules = async (
]; ];
// Create migration rules updates templates // Create migration rules updates templates
const rulesToUpdate: UpdateRuleMigrationData[] = []; const rulesToUpdate: UpdateRuleMigrationRule[] = [];
installedRules.forEach((installedRule) => { installedRules.forEach((installedRule) => {
const filteredRules = rulesToInstall.filter( const filteredRules = rulesToInstall.filter(
(rule) => rule.elastic_rule?.prebuilt_rule_id === installedRule.rule_id (rule) => rule.elastic_rule?.prebuilt_rule_id === installedRule.rule_id
@ -91,11 +91,11 @@ export const installCustomRules = async (
enabled: boolean, enabled: boolean,
detectionRulesClient: IDetectionRulesClient detectionRulesClient: IDetectionRulesClient
): Promise<{ ): Promise<{
rulesToUpdate: UpdateRuleMigrationData[]; rulesToUpdate: UpdateRuleMigrationRule[];
errors: Error[]; errors: Error[];
}> => { }> => {
const errors: Error[] = []; const errors: Error[] = [];
const rulesToUpdate: UpdateRuleMigrationData[] = []; const rulesToUpdate: UpdateRuleMigrationRule[] = [];
const createCustomRulesOutcome = await initPromisePool({ const createCustomRulesOutcome = await initPromisePool({
concurrency: MAX_CUSTOM_RULES_TO_CREATE_IN_PARALLEL, concurrency: MAX_CUSTOM_RULES_TO_CREATE_IN_PARALLEL,
items: rulesToInstall, items: rulesToInstall,

View file

@ -7,15 +7,15 @@
import type { SavedObjectsClientContract } from '@kbn/core/server'; import type { SavedObjectsClientContract } from '@kbn/core/server';
import type { RulesClient } from '@kbn/alerting-plugin/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 type { RuleResponse } from '../../../../../../common/api/detection_engine';
import { createPrebuiltRuleObjectsClient } from '../../../../detection_engine/prebuilt_rules/logic/rule_objects/prebuilt_rule_objects_client'; 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 { 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 { 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 { 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'; 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>(); const rulesIds = new Set<string>();
migrationRules.forEach((rule) => { migrationRules.forEach((rule) => {
if (rule.elastic_rule?.prebuilt_rule_id) { if (rule.elastic_rule?.prebuilt_rule_id) {

View file

@ -6,12 +6,12 @@
*/ */
import { parseEsqlQuery } from '@kbn/securitysolution-utils'; 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 { import {
RuleMigrationTranslationResultEnum, RuleMigrationTranslationResultEnum,
type RuleMigrationTranslationResult, type RuleMigrationTranslationResult,
} from '../../../../../../common/siem_migrations/model/rule_migration.gen'; } from '../../../../../../common/siem_migrations/model/rule_migration.gen';
import type { InternalUpdateRuleMigrationData } from '../../types'; import type { InternalUpdateRuleMigrationRule } from '../../types';
export const isValidEsqlQuery = (esqlQuery: string) => { export const isValidEsqlQuery = (esqlQuery: string) => {
const { isEsqlQueryAggregating, hasMetadataOperator, errors } = parseEsqlQuery(esqlQuery); const { isEsqlQueryAggregating, hasMetadataOperator, errors } = parseEsqlQuery(esqlQuery);
@ -41,8 +41,8 @@ export const convertEsqlQueryToTranslationResult = (
}; };
export const transformToInternalUpdateRuleMigrationData = ( export const transformToInternalUpdateRuleMigrationData = (
ruleMigration: UpdateRuleMigrationData ruleMigration: UpdateRuleMigrationRule
): InternalUpdateRuleMigrationData => { ): InternalUpdateRuleMigrationRule => {
if (ruleMigration.elastic_rule?.query == null) { if (ruleMigration.elastic_rule?.query == null) {
return ruleMigration; 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. * 2.0.
*/ */
import type { RuleMigrationsDataClient } from '../rule_migrations_data_client';
import type { RuleMigrationsDataIntegrationsClient } from '../rule_migrations_data_integrations_client'; import type { RuleMigrationsDataIntegrationsClient } from '../rule_migrations_data_integrations_client';
import type { RuleMigrationsDataLookupsClient } from '../rule_migrations_data_lookups_client'; import type { RuleMigrationsDataLookupsClient } from '../rule_migrations_data_lookups_client';
import type { RuleMigrationsDataMigrationClient } from '../rule_migrations_data_migration_client'; import type { RuleMigrationsDataMigrationClient } from '../rule_migrations_data_migration_client';
@ -63,15 +64,19 @@ export const mockRuleMigrationsDataMigrationsClient = {
get: jest.fn().mockResolvedValue(undefined), get: jest.fn().mockResolvedValue(undefined),
} as unknown as jest.Mocked<RuleMigrationsDataMigrationClient>; } as unknown as jest.Mocked<RuleMigrationsDataMigrationClient>;
export const mockDeleteMigration = jest.fn().mockResolvedValue(undefined);
// Rule migrations data client // Rule migrations data client
export const createRuleMigrationsDataClientMock = () => ({ export const createRuleMigrationsDataClientMock = () =>
rules: mockRuleMigrationsDataRulesClient, ({
resources: mockRuleMigrationsDataResourcesClient, rules: mockRuleMigrationsDataRulesClient,
integrations: mockRuleMigrationsDataIntegrationsClient, resources: mockRuleMigrationsDataResourcesClient,
prebuiltRules: mockRuleMigrationsDataPrebuiltRulesClient, integrations: mockRuleMigrationsDataIntegrationsClient,
lookups: mockRuleMigrationsDataLookupsClient, prebuiltRules: mockRuleMigrationsDataPrebuiltRulesClient,
migrations: mockRuleMigrationsDataMigrationsClient, lookups: mockRuleMigrationsDataLookupsClient,
}); migrations: mockRuleMigrationsDataMigrationsClient,
deleteMigration: mockDeleteMigration,
} as unknown as jest.MockedObjectDeep<RuleMigrationsDataClient>);
export const MockRuleMigrationsDataClient = jest export const MockRuleMigrationsDataClient = jest
.fn() .fn()
@ -81,10 +86,12 @@ export const MockRuleMigrationsDataClient = jest
export const mockIndexName = 'mocked_siem_rule_migrations_index_name'; export const mockIndexName = 'mocked_siem_rule_migrations_index_name';
export const mockInstall = jest.fn().mockResolvedValue(undefined); export const mockInstall = jest.fn().mockResolvedValue(undefined);
export const mockCreateClient = jest.fn(() => createRuleMigrationsDataClientMock()); export const mockCreateClient = jest.fn(() => createRuleMigrationsDataClientMock());
export const mockSetup = jest.fn().mockResolvedValue(undefined);
export const MockRuleMigrationsDataService = jest.fn().mockImplementation(() => ({ export const MockRuleMigrationsDataService = jest.fn().mockImplementation(() => ({
createAdapter: jest.fn(), createAdapter: jest.fn(),
install: mockInstall, install: mockInstall,
createClient: mockCreateClient, createClient: mockCreateClient,
createIndexNameProvider: jest.fn().mockResolvedValue(mockIndexName), createIndexNameProvider: jest.fn().mockResolvedValue(mockIndexName),
setup: mockSetup,
})); }));

View file

@ -18,7 +18,8 @@ import type {
Logger, Logger,
} from '@kbn/core/server'; } from '@kbn/core/server';
import assert from 'assert'; 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; 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'; import { RuleMigrationsDataMigrationClient } from './rule_migrations_data_migration_client';
export class RuleMigrationsDataClient { export class RuleMigrationsDataClient {
protected logger: Logger;
protected esClient: IScopedClusterClient['asInternalUser'];
public readonly migrations: RuleMigrationsDataMigrationClient; public readonly migrations: RuleMigrationsDataMigrationClient;
public readonly rules: RuleMigrationsDataRulesClient; public readonly rules: RuleMigrationsDataRulesClient;
public readonly resources: RuleMigrationsDataResourcesClient; public readonly resources: RuleMigrationsDataResourcesClient;
@ -71,5 +74,40 @@ export class RuleMigrationsDataClient {
logger, logger,
spaceId 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, 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 () => { test('should throw an error if an error occurs', async () => {
const id = 'testId'; const id = 'testId';
( (
@ -107,4 +126,26 @@ describe('RuleMigrationsDataMigrationClient', () => {
expect(logger.error).toHaveBeenCalledWith(`Error getting migration ${id}: Error: Test error`); 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 { v4 as uuidV4 } from 'uuid';
import type { BulkOperationContainer } from '@elastic/elasticsearch/lib/api/types';
import type { StoredSiemMigration } from '../types'; import type { StoredSiemMigration } from '../types';
import { RuleMigrationsDataBaseClient } from './rule_migrations_data_base_client'; import { RuleMigrationsDataBaseClient } from './rule_migrations_data_base_client';
import { isNotFoundError } from './utils';
export class RuleMigrationsDataMigrationClient extends RuleMigrationsDataBaseClient { export class RuleMigrationsDataMigrationClient extends RuleMigrationsDataBaseClient {
async create(): Promise<string> { async create(): Promise<string> {
@ -34,19 +36,42 @@ export class RuleMigrationsDataMigrationClient extends RuleMigrationsDataBaseCli
return migrationId; 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(); const index = await this.getIndexName();
return this.esClient return this.esClient
.get<StoredSiemMigration>({ .get<StoredSiemMigration>({
index, index,
id, id,
}) })
.then((document) => { .then(this.processHit)
return this.processHit(document);
})
.catch((error) => { .catch((error) => {
if (isNotFoundError(error)) {
return undefined;
}
this.logger.error(`Error getting migration ${id}: ${error}`); this.logger.error(`Error getting migration ${id}: ${error}`);
throw 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 { 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 { import type {
RuleMigrationResource, RuleMigrationResource,
RuleMigrationResourceType, RuleMigrationResourceType,
@ -156,4 +160,21 @@ export class RuleMigrationsDataResourcesClient extends RuleMigrationsDataBaseCli
} }
return { bool: { filter } }; 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, AggregationsStringTermsBucket,
QueryDslQueryContainer, QueryDslQueryContainer,
Duration, Duration,
BulkOperationContainer,
} from '@elastic/elasticsearch/lib/api/types'; } from '@elastic/elasticsearch/lib/api/types';
import type { RuleMigrationFilters } from '../../../../../common/siem_migrations/types'; import type { RuleMigrationFilters } from '../../../../../common/siem_migrations/types';
import type { InternalUpdateRuleMigrationData, StoredRuleMigration } from '../types'; import type { InternalUpdateRuleMigrationRule, StoredRuleMigration } from '../types';
import { import {
SiemMigrationStatus, SiemMigrationStatus,
RuleTranslationResult, RuleTranslationResult,
} from '../../../../../common/siem_migrations/constants'; } 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 { import {
type RuleMigrationTaskStats, type RuleMigrationTaskStats,
type RuleMigrationTranslationStats, type RuleMigrationTranslationStats,
@ -30,14 +31,14 @@ import { getSortingOptions, type RuleMigrationSort } from './sort';
import { conditions as searchConditions } from './search'; import { conditions as searchConditions } from './search';
import { RuleMigrationsDataBaseClient } from './rule_migrations_data_base_client'; import { RuleMigrationsDataBaseClient } from './rule_migrations_data_base_client';
export type CreateRuleMigrationInput = Omit< export type AddRuleMigrationRulesInput = Omit<
RuleMigration, RuleMigrationRule,
'@timestamp' | 'id' | 'status' | 'created_by' '@timestamp' | 'id' | 'status' | 'created_by'
>; >;
export type RuleMigrationDataStats = Omit<RuleMigrationTaskStats, 'status'>; export type RuleMigrationDataStats = Omit<RuleMigrationTaskStats, 'status'>;
export type RuleMigrationAllDataStats = RuleMigrationDataStats[]; export type RuleMigrationAllDataStats = RuleMigrationDataStats[];
export interface RuleMigrationGetOptions { export interface RuleMigrationGetRulesOptions {
filters?: RuleMigrationFilters; filters?: RuleMigrationFilters;
sort?: RuleMigrationSort; sort?: RuleMigrationSort;
from?: number; from?: number;
@ -53,11 +54,11 @@ const DEFAULT_SEARCH_BATCH_SIZE = 500 as const;
export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient { export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient {
/** Indexes an array of rule migrations to be processed */ /** 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 index = await this.getIndexName();
const profileId = await this.getProfileUid(); const profileId = await this.getProfileUid();
let ruleMigrationsSlice: CreateRuleMigrationInput[]; let ruleMigrationsSlice: AddRuleMigrationRulesInput[];
const createdAt = new Date().toISOString(); const createdAt = new Date().toISOString();
while ((ruleMigrationsSlice = ruleMigrations.splice(0, BULK_MAX_SIZE)).length) { while ((ruleMigrationsSlice = ruleMigrations.splice(0, BULK_MAX_SIZE)).length) {
await this.esClient await this.esClient
@ -83,11 +84,11 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient
} }
/** Updates an array of rule migrations to be processed */ /** 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 index = await this.getIndexName();
const profileId = await this.getProfileUid(); const profileId = await this.getProfileUid();
let ruleMigrationsSlice: InternalUpdateRuleMigrationData[]; let ruleMigrationsSlice: InternalUpdateRuleMigrationRule[];
const updatedAt = new Date().toISOString(); const updatedAt = new Date().toISOString();
while ((ruleMigrationsSlice = ruleMigrations.splice(0, BULK_MAX_SIZE)).length) { while ((ruleMigrationsSlice = ruleMigrations.splice(0, BULK_MAX_SIZE)).length) {
await this.esClient await this.esClient
@ -117,14 +118,14 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient
/** Retrieves an array of rule documents of a specific migrations */ /** Retrieves an array of rule documents of a specific migrations */
async get( async get(
migrationId: string, migrationId: string,
{ filters = {}, sort: sortParam = {}, from, size }: RuleMigrationGetOptions = {} { filters = {}, sort: sortParam = {}, from, size }: RuleMigrationGetRulesOptions = {}
): Promise<{ total: number; data: StoredRuleMigration[] }> { ): Promise<{ total: number; data: StoredRuleMigration[] }> {
const index = await this.getIndexName(); const index = await this.getIndexName();
const query = this.getFilterQuery(migrationId, filters); const query = this.getFilterQuery(migrationId, filters);
const sort = sortParam.sortField ? getSortingOptions(sortParam) : undefined; const sort = sortParam.sortField ? getSortingOptions(sortParam) : undefined;
const result = await this.esClient const result = await this.esClient
.search<RuleMigration>({ index, query, sort, from, size }) .search<RuleMigrationRule>({ index, query, sort, from, size })
.catch((error) => { .catch((error) => {
this.logger.error(`Error searching rule migrations: ${error.message}`); this.logger.error(`Error searching rule migrations: ${error.message}`);
throw error; throw error;
@ -144,7 +145,7 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient
const query = this.getFilterQuery(migrationId, filters); const query = this.getFilterQuery(migrationId, filters);
const search = { query, sort: '_doc', scroll, size }; // sort by _doc to ensure consistent order const search = { query, sort: '_doc', scroll, size }; // sort by _doc to ensure consistent order
try { try {
return this.getSearchBatches<RuleMigration>(search); return this.getSearchBatches<RuleMigrationRule>(search);
} catch (error) { } catch (error) {
this.logger.error(`Error scrolling rule migrations: ${error.message}`); this.logger.error(`Error scrolling rule migrations: ${error.message}`);
throw error; throw error;
@ -415,4 +416,22 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient
} }
return { bool: { filter } }; 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 { elasticsearchServiceMock } from '@kbn/core-elasticsearch-server-mocks';
import { loggingSystemMock } from '@kbn/core-logging-server-mocks'; import { loggingSystemMock } from '@kbn/core-logging-server-mocks';
import { securityServiceMock } from '@kbn/core-security-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 { IndexPatternAdapter, IndexAdapter } from '@kbn/index-adapter';
import { loggerMock } from '@kbn/logging-mocks';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import type { IndexNameProviders, SiemRuleMigrationsClientDependencies } from '../types'; import type { IndexNameProviders, SiemRuleMigrationsClientDependencies } from '../types';
import type { SetupParams } from './rule_migrations_data_service';
import { INDEX_PATTERN, RuleMigrationsDataService } 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'); jest.mock('@kbn/index-adapter');
@ -86,18 +88,19 @@ describe('SiemRuleMigrationsDataService', () => {
}); });
describe('install', () => { 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 service = new RuleMigrationsDataService(logger, kibanaVersion);
const params: Omit<InstallParams, 'logger'> = { const params: SetupParams = {
esClient, esClient,
pluginStop$: new Subject(), pluginStop$: new Subject(),
}; };
await service.install(params); await service.setup(params);
const [indexPatternAdapter] = MockedIndexPatternAdapter.mock.instances; const [indexPatternAdapter] = MockedIndexPatternAdapter.mock.instances;
const [indexAdapter] = MockedIndexAdapter.mock.instances; const [indexAdapter] = MockedIndexAdapter.mock.instances;
expect(indexPatternAdapter.install).toHaveBeenCalledWith(expect.objectContaining(params)); expect(indexPatternAdapter.install).toHaveBeenCalledWith(expect.objectContaining(params));
expect(indexAdapter.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 () => { it('should install space index pattern', async () => {
const service = new RuleMigrationsDataService(logger, kibanaVersion); const service = new RuleMigrationsDataService(logger, kibanaVersion);
const params: InstallParams = { const params: SetupParams = {
esClient, esClient,
logger: loggerMock.create(),
pluginStop$: new Subject(), pluginStop$: new Subject(),
}; };
@ -122,7 +124,7 @@ describe('SiemRuleMigrationsDataService', () => {
MockedIndexPatternAdapter.mock.instances; MockedIndexPatternAdapter.mock.instances;
(rulesIndexPatternAdapter.install as jest.Mock).mockResolvedValueOnce(undefined); (rulesIndexPatternAdapter.install as jest.Mock).mockResolvedValueOnce(undefined);
await service.install(params); await service.setup(params);
service.createClient(createClientParams); service.createClient(createClientParams);
await mockIndexNameProviders.rules(); 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; you may not use this file except in compliance with the Elastic License
* 2.0. * 2.0.
*/ */
import type { AuthenticatedUser, IScopedClusterClient, Logger } from '@kbn/core/server'; import type {
AuthenticatedUser,
ElasticsearchClient,
IScopedClusterClient,
Logger,
} from '@kbn/core/server';
import { import {
IndexAdapter, IndexAdapter,
IndexPatternAdapter, IndexPatternAdapter,
@ -27,6 +32,7 @@ import {
ruleMigrationResourcesFieldMap, ruleMigrationResourcesFieldMap,
ruleMigrationsFieldMap, ruleMigrationsFieldMap,
} from './rule_migrations_field_maps'; } from './rule_migrations_field_maps';
import { RuleMigrationIndexMigrator } from '../index_migrators';
const TOTAL_FIELDS_LIMIT = 2500; const TOTAL_FIELDS_LIMIT = 2500;
export const INDEX_PATTERN = '.kibana-siem-rule-migrations'; export const INDEX_PATTERN = '.kibana-siem-rule-migrations';
@ -42,6 +48,10 @@ interface CreateAdapterParams {
fieldMap: FieldMap; fieldMap: FieldMap;
} }
export interface SetupParams extends Omit<InstallParams, 'logger'> {
esClient: ElasticsearchClient;
}
export class RuleMigrationsDataService { export class RuleMigrationsDataService {
private readonly adapters: Adapters; private readonly adapters: Adapters;
@ -96,7 +106,12 @@ export class RuleMigrationsDataService {
return adapter; 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([ await Promise.all([
this.adapters.rules.install({ ...params, logger: this.logger }), this.adapters.rules.install({ ...params, logger: this.logger }),
this.adapters.resources.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) { public createClient({ spaceId, currentUser, esScopedClient, dependencies }: CreateClientParams) {
const indexNameProviders: IndexNameProviders = { const indexNameProviders: IndexNameProviders = {
rules: this.createIndexNameProvider(this.adapters.rules, spaceId), rules: this.createIndexNameProvider(this.adapters.rules, spaceId),

View file

@ -9,10 +9,11 @@ import type { FieldMap, SchemaFieldMapKeys } from '@kbn/data-stream-adapter';
import type { import type {
RuleMigration, RuleMigration,
RuleMigrationResource, RuleMigrationResource,
RuleMigrationRule,
} from '../../../../../common/siem_migrations/model/rule_migration.gen'; } 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 }, '@timestamp': { type: 'date', required: false },
migration_id: { type: 'keyword', required: true }, migration_id: { type: 'keyword', required: true },
created_by: { 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 }, 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_at: { type: 'date', required: true },
created_by: { type: 'keyword', 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 { Subject } from 'rxjs';
import { import {
MockRuleMigrationsDataService, MockRuleMigrationsDataService,
mockInstall, mockSetup,
mockCreateClient as mockDataCreateClient, mockCreateClient as mockDataCreateClient,
} from './data/__mocks__/mocks'; } from './data/__mocks__/mocks';
import { mockCreateClient as mockTaskCreateClient, mockStopAll } from './task/__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', () => { it('should set esClusterClient and call dataStreamAdapter.install', () => {
ruleMigrationsService.setup({ esClusterClient, pluginStop$ }); ruleMigrationsService.setup({ esClusterClient, pluginStop$ });
expect(mockInstall).toHaveBeenCalledWith({ expect(mockSetup).toHaveBeenCalledWith({
esClient: esClusterClient.asInternalUser, esClient: esClusterClient.asInternalUser,
pluginStop$, pluginStop$,
}); });
@ -60,7 +60,7 @@ describe('SiemRuleMigrationsService', () => {
it('should log error when data installation fails', async () => { it('should log error when data installation fails', async () => {
const error = 'Failed to install'; const error = 'Failed to install';
mockInstall.mockRejectedValueOnce(error); mockSetup.mockRejectedValueOnce(error);
ruleMigrationsService.setup({ esClusterClient, pluginStop$ }); ruleMigrationsService.setup({ esClusterClient, pluginStop$ });
await waitFor(() => { await waitFor(() => {

View file

@ -54,7 +54,7 @@ export class SiemRuleMigrationsService {
this.esClusterClient = esClusterClient; this.esClusterClient = esClusterClient;
const esClient = esClusterClient.asInternalUser; 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); this.logger.error('Error installing data service.', err);
}); });
} }

View file

@ -11,7 +11,7 @@ import type { RuleTranslationResult } from '../../../../../../common/siem_migrat
import type { import type {
ElasticRulePartial, ElasticRulePartial,
OriginalRule, OriginalRule,
RuleMigration, RuleMigrationRule,
} from '../../../../../../common/siem_migrations/model/rule_migration.gen'; } from '../../../../../../common/siem_migrations/model/rule_migration.gen';
import type { RuleMigrationResources } from '../retrievers/rule_resource_retriever'; import type { RuleMigrationResources } from '../retrievers/rule_resource_retriever';
@ -30,7 +30,7 @@ export const migrateRuleState = Annotation.Root({
default: () => '', default: () => '',
}), }),
translation_result: Annotation<RuleTranslationResult>(), 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. // 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), reducer: (current, value) => uniq(value ? (current ?? []).concat(value) : current),
default: () => [], default: () => [],

View file

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

View file

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

View file

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

View file

@ -198,4 +198,9 @@ export class RuleMigrationsTaskClient {
invocationConfig, 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 { IndexAdapter, IndexPatternAdapter } from '@kbn/index-adapter';
import type { import type {
RuleMigration, RuleMigration,
RuleMigrationRule,
RuleMigrationTranslationResult, RuleMigrationTranslationResult,
UpdateRuleMigrationData, UpdateRuleMigrationRule,
} from '../../../../common/siem_migrations/model/rule_migration.gen'; } from '../../../../common/siem_migrations/model/rule_migration.gen';
import { type RuleMigrationResource } 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 { RuleVersions } from './data/rule_migrations_data_prebuilt_rules_client';
import type { Stored } from '../types';
export type Stored<T extends object> = T & { id: string }; export type StoredSiemMigration = Stored<RuleMigration>;
export type StoredRuleMigration = Stored<RuleMigrationRule>;
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 StoredRuleMigrationResource = Stored<RuleMigrationResource>; export type StoredRuleMigrationResource = Stored<RuleMigrationResource>;
export interface SiemRuleMigrationsClientDependencies { export interface SiemRuleMigrationsClientDependencies {
@ -60,7 +53,7 @@ export interface RuleMigrationPrebuiltRule {
export type RuleSemanticSearchResult = RuleMigrationPrebuiltRule & RuleVersions; export type RuleSemanticSearchResult = RuleMigrationPrebuiltRule & RuleVersions;
export type InternalUpdateRuleMigrationData = UpdateRuleMigrationData & { export type InternalUpdateRuleMigrationRule = UpdateRuleMigrationRule & {
translation_result?: RuleMigrationTranslationResult; translation_result?: RuleMigrationTranslationResult;
}; };

View file

@ -11,3 +11,5 @@ export interface SiemMigrationsSetupParams {
esClusterClient: IClusterClient; esClusterClient: IClusterClient;
tasksTimeoutMs?: number; 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 { 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 { CreateRuleRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_management/crud/create_rule/create_rule_route.gen';
import { import {
CreateRuleMigrationRequestParamsInput, CreateRuleMigrationRulesRequestParamsInput,
CreateRuleMigrationRequestBodyInput, CreateRuleMigrationRulesRequestBodyInput,
} from '@kbn/security-solution-plugin/common/siem_migrations/model/api/rules/rule_migration.gen'; } 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 { CreateTimelinesRequestBodyInput } from '@kbn/security-solution-plugin/common/api/timeline/create_timelines/create_timelines_route.gen';
import { import {
@ -46,6 +46,7 @@ import {
import { DeleteNoteRequestBodyInput } from '@kbn/security-solution-plugin/common/api/timeline/delete_note/delete_note_route.gen'; 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 { 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 { 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 { 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 { 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'; import { EndpointExecuteActionRequestBodyInput } from '@kbn/security-solution-plugin/common/api/endpoint/actions/response_actions/execute/execute.gen';
@ -93,16 +94,17 @@ import {
GetRuleExecutionResultsRequestQueryInput, GetRuleExecutionResultsRequestQueryInput,
GetRuleExecutionResultsRequestParamsInput, 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'; } 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 { import { GetRuleMigrationRequestParamsInput } from '@kbn/security-solution-plugin/common/siem_migrations/model/api/rules/rule_migration.gen';
GetRuleMigrationRequestQueryInput,
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 { GetRuleMigrationPrebuiltRulesRequestParamsInput } from '@kbn/security-solution-plugin/common/siem_migrations/model/api/rules/rule_migration.gen';
import { import {
GetRuleMigrationResourcesRequestQueryInput, GetRuleMigrationResourcesRequestQueryInput,
GetRuleMigrationResourcesRequestParamsInput, GetRuleMigrationResourcesRequestParamsInput,
} from '@kbn/security-solution-plugin/common/siem_migrations/model/api/rules/rule_migration.gen'; } 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 { 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 { 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 { 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'; import { GetTimelineRequestQueryInput } from '@kbn/security-solution-plugin/common/api/timeline/get_timeline/get_timeline_route.gen';
@ -159,9 +161,10 @@ import {
UpdatePrivMonUserRequestBodyInput, UpdatePrivMonUserRequestBodyInput,
} from '@kbn/security-solution-plugin/common/api/entity_analytics/privilege_monitoring/users/update.gen'; } 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 { 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 { import {
UpdateRuleMigrationRequestParamsInput, UpdateRuleMigrationRulesRequestParamsInput,
UpdateRuleMigrationRequestBodyInput, UpdateRuleMigrationRulesRequestBodyInput,
} from '@kbn/security-solution-plugin/common/siem_migrations/model/api/rules/rule_migration.gen'; } from '@kbn/security-solution-plugin/common/siem_migrations/model/api/rules/rule_migration.gen';
import { import {
UpdateWorkflowInsightRequestParamsInput, UpdateWorkflowInsightRequestParamsInput,
@ -414,13 +417,26 @@ For detailed information on Kibana actions and alerting, and additional API call
.send(props.body as object); .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 return supertest
.post( .post(
routeWithNamespace( routeWithNamespace(
replaceParams('/internal/siem_migrations/rules/{migration_id}', props.params), replaceParams('/internal/siem_migrations/rules/{migration_id}/rules', props.params),
kibanaSpace 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') .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.query(props.query); .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. * Delete one or more Timelines or Timeline templates.
*/ */
@ -1018,7 +1049,7 @@ finalize it.
.query(props.query); .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') { getRuleMigration(props: GetRuleMigrationProps, kibanaSpace: string = 'default') {
return supertest return supertest
@ -1030,8 +1061,7 @@ finalize it.
) )
.set('kbn-xsrf', 'true') .set('kbn-xsrf', 'true')
.set(ELASTIC_HTTP_VERSION_HEADER, '1') .set(ELASTIC_HTTP_VERSION_HEADER, '1')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana');
.query(props.query);
}, },
/** /**
* Retrieves all related integrations * Retrieves all related integrations
@ -1114,6 +1144,22 @@ finalize it.
.set(ELASTIC_HTTP_VERSION_HEADER, '1') .set(ELASTIC_HTTP_VERSION_HEADER, '1')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana'); .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 * 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') { startRuleMigration(props: StartRuleMigrationProps, kibanaSpace: string = 'default') {
return supertest return supertest
.put( .post(
routeWithNamespace( routeWithNamespace(
replaceParams('/internal/siem_migrations/rules/{migration_id}/start', props.params), replaceParams('/internal/siem_migrations/rules/{migration_id}/start', props.params),
kibanaSpace 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') { stopRuleMigration(props: StopRuleMigrationProps, kibanaSpace: string = 'default') {
return supertest return supertest
.put( .post(
routeWithNamespace( routeWithNamespace(
replaceParams('/internal/siem_migrations/rules/{migration_id}/stop', props.params), replaceParams('/internal/siem_migrations/rules/{migration_id}/stop', props.params),
kibanaSpace 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); .send(props.body as object);
}, },
/** /**
* Updates rules migrations attributes * Updates rules migrations data
*/ */
updateRuleMigration(props: UpdateRuleMigrationProps, kibanaSpace: string = 'default') { updateRuleMigration(props: UpdateRuleMigrationProps, kibanaSpace: string = 'default') {
return supertest return supertest
.put( .patch(
routeWithNamespace( routeWithNamespace(
replaceParams('/internal/siem_migrations/rules/{migration_id}', props.params), replaceParams('/internal/siem_migrations/rules/{migration_id}', props.params),
kibanaSpace 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('kbn-xsrf', 'true')
.set(ELASTIC_HTTP_VERSION_HEADER, '1') .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') .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send(props.body as object); .send(props.body as object);
}, },
@ -1823,9 +1887,9 @@ export interface CreatePrivMonUserProps {
export interface CreateRuleProps { export interface CreateRuleProps {
body: CreateRuleRequestBodyInput; body: CreateRuleRequestBodyInput;
} }
export interface CreateRuleMigrationProps { export interface CreateRuleMigrationRulesProps {
params: CreateRuleMigrationRequestParamsInput; params: CreateRuleMigrationRulesRequestParamsInput;
body: CreateRuleMigrationRequestBodyInput; body: CreateRuleMigrationRulesRequestBodyInput;
} }
export interface CreateTimelinesProps { export interface CreateTimelinesProps {
body: CreateTimelinesRequestBodyInput; body: CreateTimelinesRequestBodyInput;
@ -1850,6 +1914,9 @@ export interface DeletePrivMonUserProps {
export interface DeleteRuleProps { export interface DeleteRuleProps {
query: DeleteRuleRequestQueryInput; query: DeleteRuleRequestQueryInput;
} }
export interface DeleteRuleMigrationProps {
params: DeleteRuleMigrationRequestParamsInput;
}
export interface DeleteTimelinesProps { export interface DeleteTimelinesProps {
body: DeleteTimelinesRequestBodyInput; body: DeleteTimelinesRequestBodyInput;
} }
@ -1952,7 +2019,6 @@ export interface GetRuleExecutionResultsProps {
params: GetRuleExecutionResultsRequestParamsInput; params: GetRuleExecutionResultsRequestParamsInput;
} }
export interface GetRuleMigrationProps { export interface GetRuleMigrationProps {
query: GetRuleMigrationRequestQueryInput;
params: GetRuleMigrationRequestParamsInput; params: GetRuleMigrationRequestParamsInput;
} }
export interface GetRuleMigrationPrebuiltRulesProps { export interface GetRuleMigrationPrebuiltRulesProps {
@ -1965,6 +2031,10 @@ export interface GetRuleMigrationResourcesProps {
export interface GetRuleMigrationResourcesMissingProps { export interface GetRuleMigrationResourcesMissingProps {
params: GetRuleMigrationResourcesMissingRequestParamsInput; params: GetRuleMigrationResourcesMissingRequestParamsInput;
} }
export interface GetRuleMigrationRulesProps {
query: GetRuleMigrationRulesRequestQueryInput;
params: GetRuleMigrationRulesRequestParamsInput;
}
export interface GetRuleMigrationStatsProps { export interface GetRuleMigrationStatsProps {
params: GetRuleMigrationStatsRequestParamsInput; params: GetRuleMigrationStatsRequestParamsInput;
} }
@ -2087,7 +2157,10 @@ export interface UpdateRuleProps {
} }
export interface UpdateRuleMigrationProps { export interface UpdateRuleMigrationProps {
params: UpdateRuleMigrationRequestParamsInput; params: UpdateRuleMigrationRequestParamsInput;
body: UpdateRuleMigrationRequestBodyInput; }
export interface UpdateRuleMigrationRulesProps {
params: UpdateRuleMigrationRulesRequestParamsInput;
body: UpdateRuleMigrationRulesRequestBodyInput;
} }
export interface UpdateWorkflowInsightProps { export interface UpdateWorkflowInsightProps {
params: UpdateWorkflowInsightRequestParamsInput; params: UpdateWorkflowInsightRequestParamsInput;

View file

@ -6,192 +6,35 @@
*/ */
import expect from 'expect'; import expect from 'expect';
import { v4 as uuidv4 } from 'uuid'; import { deleteAllRuleMigrations, ruleMigrationRouteHelpersFactory } from '../../utils';
import { SiemMigrationStatus } from '@kbn/security-solution-plugin/common/siem_migrations/constants';
import {
defaultOriginalRule,
deleteAllMigrationRules,
migrationResourcesRouteHelpersFactory,
migrationRulesRouteHelpersFactory,
splunkRuleWithResources,
} from '../../utils';
import { FtrProviderContext } from '../../../../ftr_provider_context'; import { FtrProviderContext } from '../../../../ftr_provider_context';
export default ({ getService }: FtrProviderContext) => { export default ({ getService }: FtrProviderContext) => {
const es = getService('es'); const es = getService('es');
const supertest = getService('supertest'); const supertest = getService('supertest');
const migrationRulesRoutes = migrationRulesRouteHelpersFactory(supertest); const ruleMigrationRoutes = ruleMigrationRouteHelpersFactory(supertest);
const migrationResourcesRoutes = migrationResourcesRouteHelpersFactory(supertest);
describe('@ess @serverless @serverlessQA Create API', () => { describe('@ess @serverless @serverlessQA Create API', () => {
beforeEach(async () => { beforeEach(async () => {
await deleteAllMigrationRules(es); await deleteAllRuleMigrations(es);
}); });
describe('Happy path', () => { describe('Happy path', () => {
it('should create migrations with provided id', async () => { it('should create migrations without any issues', 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 () => {
const { const {
body: { migration_id: migrationId }, body: { migration_id: migrationId },
} = await migrationRulesRoutes.create({ payload: [defaultOriginalRule] }); } = await ruleMigrationRoutes.create({});
// fetch migration rule expect(migrationId).not.toBeNull();
const response = await migrationRulesRoutes.get({ migrationId });
expect(response.body.total).toEqual(1);
const migrationRule = response.body.data[0]; const {
expect(migrationRule).toEqual( body: { id, created_by: createdBy },
expect.objectContaining({ } = await ruleMigrationRoutes.get({
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({
migrationId, 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', () => { expect(id).toBe(migrationId);
it('should return no content error', async () => { expect(createdBy).not.toBeNull();
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',
});
}); });
}); });
}); });

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 expect from 'expect';
import { v4 as uuidv4 } from 'uuid'; import { deleteAllRuleMigrations, ruleMigrationRouteHelpersFactory } from '../../utils';
import {
RuleTranslationResult,
SiemMigrationStatus,
} from '@kbn/security-solution-plugin/common/siem_migrations/constants';
import {
RuleMigrationDocument,
createMigrationRules,
defaultElasticRule,
defaultOriginalRule,
deleteAllMigrationRules,
getMigrationRuleDocument,
getMigrationRuleDocuments,
migrationRulesRouteHelpersFactory,
} from '../../utils';
import { FtrProviderContext } from '../../../../ftr_provider_context'; import { FtrProviderContext } from '../../../../ftr_provider_context';
export default ({ getService }: FtrProviderContext) => { export default ({ getService }: FtrProviderContext) => {
const es = getService('es'); const es = getService('es');
const supertest = getService('supertest'); const supertest = getService('supertest');
const migrationRulesRoutes = migrationRulesRouteHelpersFactory(supertest); const ruleMigrationRoutes = ruleMigrationRouteHelpersFactory(supertest);
describe('@ess @serverless @serverlessQA Get API', () => { describe('@ess @serverless @serverlessQA Get API', () => {
let migrationId: string;
beforeEach(async () => { beforeEach(async () => {
await deleteAllMigrationRules(es); await deleteAllRuleMigrations(es);
const creationResponse = await ruleMigrationRoutes.create({});
migrationId = creationResponse.body.migration_id;
}); });
describe('Basic', () => { it('should fetch existing migration', async () => {
it('should fetch existing rules within specified migration', async () => { const migrationResponse = await ruleMigrationRoutes.get({
// create a document migrationId,
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)]));
}); });
expect(migrationResponse.body.id).toBe(migrationId);
}); });
describe('Filtering', () => { describe('Error handling', () => {
it('should fetch rules filtered by `searchTerm`', async () => { it('should return 404 if migration ID does not exist', async () => {
// create a document const { body } = await ruleMigrationRoutes.get({
const migrationId = uuidv4(); migrationId: 'non-existing-migration-id',
expectStatusCode: 404,
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({
migrationId,
queryParams: { search_term: 'Elastic' },
}); });
expect(response.body.total).toEqual(5);
expect(response.body.data).toEqual(expectedRuleDocuments);
// Search by word `Splunk` expect(body).toMatchObject({
expectedRuleDocuments = expect.arrayContaining( statusCode: 404,
migrationRuleDocuments error: 'Not Found',
.slice(5) message: 'No Migration found with id: non-existing-migration-id',
.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);
});
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);
});
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(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 expect from '@kbn/expect';
import { FtrProviderContext } from '../../../../ftr_provider_context'; import { FtrProviderContext } from '../../../../ftr_provider_context';
import { migrationRulesRouteHelpersFactory } from '../../utils'; import { ruleMigrationRouteHelpersFactory } from '../../utils';
export default ({ getService }: FtrProviderContext) => { export default ({ getService }: FtrProviderContext) => {
const supertest = getService('supertest'); const supertest = getService('supertest');
const migrationRulesRoutes = migrationRulesRouteHelpersFactory(supertest); const migrationRulesRoutes = ruleMigrationRouteHelpersFactory(supertest);
describe('Get Integrations', () => { describe('Get Integrations', () => {
it('should return all integrations successfully', async () => { it('should return all integrations successfully', async () => {

View file

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

View file

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

View file

@ -7,17 +7,19 @@
import expect from 'expect'; import expect from 'expect';
import { v4 as uuidv4 } from 'uuid'; 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 { RuleTranslationResult } from '@kbn/security-solution-plugin/common/siem_migrations/constants';
import { RuleResponse } from '@kbn/security-solution-plugin/common/api/detection_engine'; import { RuleResponse } from '@kbn/security-solution-plugin/common/api/detection_engine';
import { deleteAllRules } from '../../../../../common/utils/security_solution'; import { deleteAllRules } from '../../../../../common/utils/security_solution';
import { import {
RuleMigrationDocument,
createMigrationRules, createMigrationRules,
defaultElasticRule, defaultElasticRule,
deleteAllMigrationRules, deleteAllRuleMigrations,
getMigrationRuleDocuments, getMigrationRuleDocuments,
migrationRulesRouteHelpersFactory, ruleMigrationRouteHelpersFactory,
statsOverrideCallbackFactory, statsOverrideCallbackFactory,
} from '../../utils'; } from '../../utils';
import { FtrProviderContext } from '../../../../ftr_provider_context'; import { FtrProviderContext } from '../../../../ftr_provider_context';
@ -33,20 +35,20 @@ export default ({ getService }: FtrProviderContext) => {
const log = getService('log'); const log = getService('log');
const supertest = getService('supertest'); const supertest = getService('supertest');
const securitySolutionApi = getService('securitySolutionApi'); const securitySolutionApi = getService('securitySolutionApi');
const migrationRulesRoutes = migrationRulesRouteHelpersFactory(supertest); const migrationRulesRoutes = ruleMigrationRouteHelpersFactory(supertest);
describe('@ess @serverless @serverlessQA Install API', () => { describe('@ess @serverless @serverlessQA Install API', () => {
beforeEach(async () => { beforeEach(async () => {
await deleteAllRules(supertest, log); await deleteAllRules(supertest, log);
await deleteAllTimelines(es, log); await deleteAllTimelines(es, log);
await deleteAllPrebuiltRuleAssets(es, log); await deleteAllPrebuiltRuleAssets(es, log);
await deleteAllMigrationRules(es); await deleteAllRuleMigrations(es);
}); });
it('should install all installable custom migration rules', async () => { it('should install all installable custom migration rules', async () => {
const migrationId = uuidv4(); const migrationId = uuidv4();
const overrideCallback = (index: number): Partial<RuleMigrationDocument> => { const overrideCallback = (index: number): Partial<RuleMigrationRuleData> => {
const title = `Rule - ${index}`; const title = `Rule - ${index}`;
const elasticRule = { ...defaultElasticRule, title }; const elasticRule = { ...defaultElasticRule, title };
return { return {
@ -63,7 +65,7 @@ export default ({ getService }: FtrProviderContext) => {
expect(installResponse.body).toEqual({ installed: 2 }); expect(installResponse.body).toEqual({ installed: 2 });
// fetch installed migration rules information // 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) => { const installedMigrationRules = response.body.data.reduce((acc, item) => {
if (item.elastic_rule?.id) { if (item.elastic_rule?.id) {
acc.push(item.elastic_rule); acc.push(item.elastic_rule);
@ -100,7 +102,7 @@ export default ({ getService }: FtrProviderContext) => {
const migrationId = uuidv4(); const migrationId = uuidv4();
const overrideCallback = (index: number): Partial<RuleMigrationDocument> => { const overrideCallback = (index: number): Partial<RuleMigrationRuleData> => {
const { query_language: queryLanguage, query, ...rest } = defaultElasticRule; const { query_language: queryLanguage, query, ...rest } = defaultElasticRule;
return { return {
migration_id: migrationId, 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 { v4 as uuidv4 } from 'uuid';
import { import {
createMigrationRules, createMigrationRules,
deleteAllMigrationRules, deleteAllRuleMigrations,
getMigrationRuleDocument, getMigrationRuleDocument,
migrationRulesRouteHelpersFactory, ruleMigrationRouteHelpersFactory,
} from '../../utils'; } from '../../../utils';
import { FtrProviderContext } from '../../../../ftr_provider_context'; import { FtrProviderContext } from '../../../../../ftr_provider_context';
export default ({ getService }: FtrProviderContext) => { export default ({ getService }: FtrProviderContext) => {
const es = getService('es'); const es = getService('es');
const supertest = getService('supertest'); 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 () => { beforeEach(async () => {
await deleteAllMigrationRules(es); await deleteAllRuleMigrations(es);
}); });
describe('Happy path', () => { describe('Happy path', () => {
@ -32,7 +32,10 @@ export default ({ getService }: FtrProviderContext) => {
const [createdDocumentId] = await createMigrationRules(es, [migrationRuleDocument]); const [createdDocumentId] = await createMigrationRules(es, [migrationRuleDocument]);
const now = new Date().toISOString(); const now = new Date().toISOString();
await migrationRulesRoutes.update({
const {
body: { updated },
} = await ruleMigrationRoutes.updateRules({
migrationId, migrationId,
payload: [ payload: [
{ {
@ -43,8 +46,10 @@ export default ({ getService }: FtrProviderContext) => {
], ],
}); });
expect(updated).toBe(true);
// fetch migration rule // fetch migration rule
const response = await migrationRulesRoutes.get({ migrationId }); const response = await ruleMigrationRoutes.getRules({ migrationId });
expect(response.body.total).toEqual(1); expect(response.body.total).toEqual(1);
const { const {
@ -71,7 +76,7 @@ export default ({ getService }: FtrProviderContext) => {
const [createdDocumentId] = await createMigrationRules(es, [migrationRuleDocument]); const [createdDocumentId] = await createMigrationRules(es, [migrationRuleDocument]);
const now = new Date().toISOString(); const now = new Date().toISOString();
await migrationRulesRoutes.update({ await ruleMigrationRoutes.updateRules({
migrationId, migrationId,
payload: [ payload: [
{ {
@ -101,7 +106,7 @@ export default ({ getService }: FtrProviderContext) => {
}); });
// fetch migration rule // fetch migration rule
const response = await migrationRulesRoutes.get({ migrationId }); const response = await ruleMigrationRoutes.getRules({ migrationId });
expect(response.body.total).toEqual(1); expect(response.body.total).toEqual(1);
const migrationRule = response.body.data[0]; const migrationRule = response.body.data[0];
@ -112,7 +117,9 @@ export default ({ getService }: FtrProviderContext) => {
describe('Error handling', () => { describe('Error handling', () => {
it('should return empty content response when no rules passed', async () => { it('should return empty content response when no rules passed', async () => {
const migrationId = uuidv4(); const migrationId = uuidv4();
await migrationRulesRoutes.update({ const migrationRuleDocument = getMigrationRuleDocument({ migration_id: migrationId });
await createMigrationRules(es, [migrationRuleDocument]);
await ruleMigrationRoutes.updateRules({
migrationId, migrationId,
payload: [], payload: [],
expectStatusCode: 204, expectStatusCode: 204,
@ -123,8 +130,7 @@ export default ({ getService }: FtrProviderContext) => {
const migrationId = uuidv4(); const migrationId = uuidv4();
const migrationRuleDocument = getMigrationRuleDocument({ migration_id: migrationId }); const migrationRuleDocument = getMigrationRuleDocument({ migration_id: migrationId });
await createMigrationRules(es, [migrationRuleDocument]); await createMigrationRules(es, [migrationRuleDocument]);
const response = await ruleMigrationRoutes.updateRules({
const response = await migrationRulesRoutes.update({
migrationId, migrationId,
payload: [{ elastic_rule: { title: 'Updated title' } }], payload: [{ elastic_rule: { title: 'Updated title' } }],
expectStatusCode: 400, expectStatusCode: 400,
@ -140,8 +146,7 @@ export default ({ getService }: FtrProviderContext) => {
const migrationId = uuidv4(); const migrationId = uuidv4();
const migrationRuleDocument = getMigrationRuleDocument({ migration_id: migrationId }); const migrationRuleDocument = getMigrationRuleDocument({ migration_id: migrationId });
await createMigrationRules(es, [migrationRuleDocument]); await createMigrationRules(es, [migrationRuleDocument]);
const response = await ruleMigrationRoutes.updateRules({
const response = await migrationRulesRoutes.update({
migrationId, migrationId,
expectStatusCode: 400, 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; you may not use this file except in compliance with the Elastic License
* 2.0. * 2.0.
*/ */
import { v4 as uuidv4 } from 'uuid';
import expect from '@kbn/expect'; import expect from '@kbn/expect';
import { FtrProviderContext } from '../../../../ftr_provider_context'; import { FtrProviderContext } from '../../../../ftr_provider_context';
import { import {
SiemMigrationsAPIErrorResponse, SiemMigrationsAPIErrorResponse,
defaultOriginalRule, defaultOriginalRule,
migrationRulesRouteHelpersFactory, ruleMigrationRouteHelpersFactory,
} from '../../utils'; } from '../../utils';
export default ({ getService }: FtrProviderContext) => { export default ({ getService }: FtrProviderContext) => {
const supertest = getService('supertest'); const supertest = getService('supertest');
const migrationRulesRoutes = migrationRulesRouteHelpersFactory(supertest); const migrationRulesRoutes = ruleMigrationRouteHelpersFactory(supertest);
describe('Start Migration', () => { describe('Start Migration', () => {
let migrationId: string; let migrationId: string;
beforeEach(async () => { beforeEach(async () => {
migrationId = uuidv4(); const createMigrationRespose = await migrationRulesRoutes.create({});
await migrationRulesRoutes.create({ migrationId = createMigrationRespose.body.migration_id;
await migrationRulesRoutes.addRulesToMigration({
migrationId, migrationId,
payload: [defaultOriginalRule], payload: [defaultOriginalRule],
}); });
@ -87,7 +87,7 @@ export default ({ getService }: FtrProviderContext) => {
describe('error scenarios', () => { describe('error scenarios', () => {
it('should reject if connector_id is incorrect', async () => { it('should reject if connector_id is incorrect', async () => {
const response = await migrationRulesRoutes.start({ const response = await migrationRulesRoutes.start({
migrationId: 'invalid_migration_id', migrationId,
expectStatusCode: 400, expectStatusCode: 400,
payload: { payload: {
connector_id: 'preconfigured_bedrock', 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 { v4 as uuidv4 } from 'uuid';
import { import {
createMigrationRules, createMigrationRules,
deleteAllMigrationRules, deleteAllRuleMigrations,
getMigrationRuleDocuments, getMigrationRuleDocuments,
migrationRulesRouteHelpersFactory, ruleMigrationRouteHelpersFactory,
statsOverrideCallbackFactory, statsOverrideCallbackFactory,
} from '../../utils'; } from '../../utils';
import { FtrProviderContext } from '../../../../ftr_provider_context'; import { FtrProviderContext } from '../../../../ftr_provider_context';
@ -19,11 +19,11 @@ import { FtrProviderContext } from '../../../../ftr_provider_context';
export default ({ getService }: FtrProviderContext) => { export default ({ getService }: FtrProviderContext) => {
const es = getService('es'); const es = getService('es');
const supertest = getService('supertest'); const supertest = getService('supertest');
const migrationRulesRoutes = migrationRulesRouteHelpersFactory(supertest); const migrationRulesRoutes = ruleMigrationRouteHelpersFactory(supertest);
describe('@ess @serverless @serverlessQA Stats API', () => { describe('@ess @serverless @serverlessQA Stats API', () => {
beforeEach(async () => { beforeEach(async () => {
await deleteAllMigrationRules(es); await deleteAllRuleMigrations(es);
}); });
it('should return stats for the specific migration', async () => { 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 migrationId1 = uuidv4();
const migrationId2 = 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