From 301236f2374e294103241b40974f21725316b4a4 Mon Sep 17 00:00:00 2001 From: "Devin W. Hurley" Date: Tue, 17 Aug 2021 22:54:01 -0400 Subject: [PATCH] [RAC] [RBAC] working find route for alerts as data client (#107982) Addition of a find api to the alerts client to authorize requests using RBAC, updates alerts histograms to use new API on alerts page, updates new alerts aggs data table on alerts page, and updates alerts histogram on overview page. --- x-pack/plugins/rule_registry/common/types.ts | 264 +++++++++++ .../alerts_client/classes/alertsclient.md | 128 +++++- .../interfaces/bulkupdateoptions.md | 10 +- .../interfaces/constructoroptions.md | 8 +- .../alerts_client/interfaces/updateoptions.md | 8 +- .../alert_data_client/alerts_client.mock.ts | 1 + .../server/alert_data_client/alerts_client.ts | 90 +++- .../tests/bulk_update.test.ts | 6 + .../tests/find_alerts.test.ts | 422 ++++++++++++++++++ .../alert_data_client/tests/get.test.ts | 15 +- .../alert_data_client/tests/update.test.ts | 11 +- .../rule_registry/server/routes/find.ts | 86 ++++ .../rule_registry/server/routes/index.ts | 2 + .../scripts/find_observability_alert.sh | 29 ++ .../security_solution/common/constants.ts | 3 + .../alerts_kpis/alerts_count_panel/index.tsx | 10 + .../alerts_histogram_panel/index.tsx | 17 +- .../containers/detection_engine/alerts/api.ts | 28 +- .../detection_engine/alerts/use_query.tsx | 9 +- .../basic/tests/query_signals.ts | 116 ++++- .../basic/tests/update_rac_alerts.ts | 2 - .../tests/basic/find_alerts.ts | 275 ++++++++++++ .../security_and_spaces/tests/basic/index.ts | 1 + 23 files changed, 1465 insertions(+), 76 deletions(-) create mode 100644 x-pack/plugins/rule_registry/server/alert_data_client/tests/find_alerts.test.ts create mode 100644 x-pack/plugins/rule_registry/server/routes/find.ts create mode 100755 x-pack/plugins/rule_registry/server/scripts/find_observability_alert.sh create mode 100644 x-pack/test/rule_registry/security_and_spaces/tests/basic/find_alerts.ts diff --git a/x-pack/plugins/rule_registry/common/types.ts b/x-pack/plugins/rule_registry/common/types.ts index cc23469524a4e..7b2fde48057a6 100644 --- a/x-pack/plugins/rule_registry/common/types.ts +++ b/x-pack/plugins/rule_registry/common/types.ts @@ -7,6 +7,270 @@ import { estypes } from '@elastic/elasticsearch'; +import * as t from 'io-ts'; + +// note: these schemas are not exhaustive. See the `Sort` type of `@elastic/elasticsearch` if you need to enhance it. +const fieldSchema = t.string; +export const sortOrderSchema = t.union([t.literal('asc'), t.literal('desc'), t.literal('_doc')]); +type SortOrderSchema = 'asc' | 'desc' | '_doc'; +const sortModeSchema = t.union([ + t.literal('min'), + t.literal('max'), + t.literal('sum'), + t.literal('avg'), + t.literal('median'), +]); +const fieldSortSchema = t.exact( + t.partial({ + missing: t.union([t.string, t.number, t.boolean]), + mode: sortModeSchema, + order: sortOrderSchema, + // nested and unmapped_type not implemented yet + }) +); +const sortContainerSchema = t.record(t.string, t.union([sortOrderSchema, fieldSortSchema])); +const sortCombinationsSchema = t.union([fieldSchema, sortContainerSchema]); +export const sortSchema = t.union([sortCombinationsSchema, t.array(sortCombinationsSchema)]); + +export const minDocCount = t.number; + +interface BucketAggsSchemas { + filter?: { + term?: { [x: string]: string | boolean | number }; + }; + histogram?: { + field?: string; + interval?: number; + min_doc_count?: number; + extended_bounds?: { + min: number; + max: number; + }; + hard_bounds?: { + min: number; + max: number; + }; + missing?: number; + keyed?: boolean; + order?: { + _count: string; + _key: string; + }; + }; + nested?: { + path: string; + }; + terms?: { + field?: string; + collect_mode?: string; + exclude?: string | string[]; + include?: string | string[]; + execution_hint?: string; + missing?: number | string; + min_doc_count?: number; + size?: number; + show_term_doc_count_error?: boolean; + order?: + | SortOrderSchema + | { [x: string]: SortOrderSchema } + | Array<{ [x: string]: SortOrderSchema }>; + }; + aggs?: { + [x: string]: BucketAggsSchemas; + }; +} + +/** + * Schemas for the Bucket aggregations. + * + * Currently supported: + * - filter + * - histogram + * - nested + * - terms + * + * Not implemented: + * - adjacency_matrix + * - auto_date_histogram + * - children + * - composite + * - date_histogram + * - date_range + * - diversified_sampler + * - filters + * - geo_distance + * - geohash_grid + * - geotile_grid + * - global + * - ip_range + * - missing + * - multi_terms + * - parent + * - range + * - rare_terms + * - reverse_nested + * - sampler + * - significant_terms + * - significant_text + * - variable_width_histogram + */ +export const BucketAggsSchemas: t.Type = t.recursion('BucketAggsSchemas', () => + t.exact( + t.partial({ + filter: t.exact( + t.partial({ + term: t.record(t.string, t.union([t.string, t.boolean, t.number])), + }) + ), + date_histogram: t.exact( + t.partial({ + field: t.string, + fixed_interval: t.string, + min_doc_count: t.number, + extended_bounds: t.type({ + min: t.string, + max: t.string, + }), + }) + ), + histogram: t.exact( + t.partial({ + field: t.string, + interval: t.number, + min_doc_count: t.number, + extended_bounds: t.exact( + t.type({ + min: t.number, + max: t.number, + }) + ), + hard_bounds: t.exact( + t.type({ + min: t.number, + max: t.number, + }) + ), + missing: t.number, + keyed: t.boolean, + order: t.exact( + t.type({ + _count: t.string, + _key: t.string, + }) + ), + }) + ), + nested: t.type({ + path: t.string, + }), + terms: t.exact( + t.partial({ + field: t.string, + collect_mode: t.string, + exclude: t.union([t.string, t.array(t.string)]), + include: t.union([t.string, t.array(t.string)]), + execution_hint: t.string, + missing: t.union([t.number, t.string]), + min_doc_count: t.number, + size: t.number, + show_term_doc_count_error: t.boolean, + order: t.union([ + sortOrderSchema, + t.record(t.string, sortOrderSchema), + t.array(t.record(t.string, sortOrderSchema)), + ]), + }) + ), + aggs: t.record(t.string, BucketAggsSchemas), + }) + ) +); + +/** + * Schemas for the metrics Aggregations + * + * Currently supported: + * - avg + * - cardinality + * - min + * - max + * - sum + * - top_hits + * - weighted_avg + * + * Not implemented: + * - boxplot + * - extended_stats + * - geo_bounds + * - geo_centroid + * - geo_line + * - matrix_stats + * - median_absolute_deviation + * - percentile_ranks + * - percentiles + * - rate + * - scripted_metric + * - stats + * - string_stats + * - t_test + * - value_count + */ +export const metricsAggsSchemas = t.partial({ + avg: t.partial({ + field: t.string, + missing: t.union([t.string, t.number, t.boolean]), + }), + cardinality: t.partial({ + field: t.string, + precision_threshold: t.number, + rehash: t.boolean, + missing: t.union([t.string, t.number, t.boolean]), + }), + min: t.partial({ + field: t.string, + missing: t.union([t.string, t.number, t.boolean]), + format: t.string, + }), + max: t.partial({ + field: t.string, + missing: t.union([t.string, t.number, t.boolean]), + format: t.string, + }), + sum: t.partial({ + field: t.string, + missing: t.union([t.string, t.number, t.boolean]), + }), + top_hits: t.partial({ + explain: t.boolean, + docvalue_fields: t.union([t.string, t.array(t.string)]), + stored_fields: t.union([t.string, t.array(t.string)]), + from: t.number, + size: t.number, + sort: sortSchema, + seq_no_primary_term: t.boolean, + version: t.boolean, + track_scores: t.boolean, + highlight: t.any, + _source: t.union([t.boolean, t.string, t.array(t.string)]), + }), + weighted_avg: t.partial({ + format: t.string, + value_type: t.string, + value: t.partial({ + field: t.string, + missing: t.number, + }), + weight: t.partial({ + field: t.string, + missing: t.number, + }), + }), +}); + +export type PutIndexTemplateRequest = estypes.IndicesPutIndexTemplateRequest & { + body?: { composed_of?: string[] }; +}; + export interface ClusterPutComponentTemplateBody { template: { settings: { diff --git a/x-pack/plugins/rule_registry/docs/alerts_client/classes/alertsclient.md b/x-pack/plugins/rule_registry/docs/alerts_client/classes/alertsclient.md index 7c79e0a5e4c0f..75f3fd24cbc19 100644 --- a/x-pack/plugins/rule_registry/docs/alerts_client/classes/alertsclient.md +++ b/x-pack/plugins/rule_registry/docs/alerts_client/classes/alertsclient.md @@ -25,8 +25,11 @@ on alerts as data. - [buildEsQueryWithAuthz](alertsclient.md#buildesquerywithauthz) - [bulkUpdate](alertsclient.md#bulkupdate) - [ensureAllAuthorized](alertsclient.md#ensureallauthorized) +- [find](alertsclient.md#find) - [get](alertsclient.md#get) +- [getAlertStatusFieldUpdate](alertsclient.md#getalertstatusfieldupdate) - [getAuthorizedAlertsIndices](alertsclient.md#getauthorizedalertsindices) +- [getOutcome](alertsclient.md#getoutcome) - [mgetAlertsAuditOperate](alertsclient.md#mgetalertsauditoperate) - [queryAndAuditAllAlerts](alertsclient.md#queryandauditallalerts) - [singleSearchAfterAndAudit](alertsclient.md#singlesearchafterandaudit) @@ -46,7 +49,7 @@ on alerts as data. #### Defined in -[alerts_client.ts:93](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L93) +[alerts_client.ts:117](https://github.com/elastic/kibana/blob/42f5a948210/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L117) ## Properties @@ -56,7 +59,7 @@ on alerts as data. #### Defined in -[alerts_client.ts:90](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L90) +[alerts_client.ts:114](https://github.com/elastic/kibana/blob/42f5a948210/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L114) ___ @@ -66,7 +69,7 @@ ___ #### Defined in -[alerts_client.ts:91](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L91) +[alerts_client.ts:115](https://github.com/elastic/kibana/blob/42f5a948210/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L115) ___ @@ -76,7 +79,7 @@ ___ #### Defined in -[alerts_client.ts:92](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L92) +[alerts_client.ts:116](https://github.com/elastic/kibana/blob/42f5a948210/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L116) ___ @@ -86,7 +89,7 @@ ___ #### Defined in -[alerts_client.ts:89](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L89) +[alerts_client.ts:113](https://github.com/elastic/kibana/blob/42f5a948210/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L113) ___ @@ -96,7 +99,7 @@ ___ #### Defined in -[alerts_client.ts:93](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L93) +[alerts_client.ts:117](https://github.com/elastic/kibana/blob/42f5a948210/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L117) ## Methods @@ -108,10 +111,10 @@ ___ | Name | Type | | :------ | :------ | -| `query` | `undefined` \| ``null`` \| `string` | +| `query` | `undefined` \| ``null`` \| `string` \| `object` | | `id` | `undefined` \| ``null`` \| `string` | | `alertSpaceId` | `string` | -| `operation` | `Get` \| `Find` \| `Update` | +| `operation` | `Update` \| `Get` \| `Find` | | `config` | `EsQueryConfig` | #### Returns @@ -120,7 +123,7 @@ ___ #### Defined in -[alerts_client.ts:305](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L305) +[alerts_client.ts:367](https://github.com/elastic/kibana/blob/42f5a948210/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L367) ___ @@ -146,7 +149,7 @@ ___ #### Defined in -[alerts_client.ts:475](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L475) +[alerts_client.ts:570](https://github.com/elastic/kibana/blob/42f5a948210/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L570) ___ @@ -160,8 +163,8 @@ Accepts an array of ES documents and executes ensureAuthorized for the given ope | Name | Type | | :------ | :------ | -| `items` | { `_id`: `string` ; `_source?`: ``null`` \| { `kibana.alert.owner?`: ``null`` \| `string` ; `rule.id?`: ``null`` \| `string` } }[] | -| `operation` | `Get` \| `Find` \| `Update` | +| `items` | { `_id`: `string` ; `_source?`: ``null`` \| { `kibana.alert.rule.consumer?`: ``null`` \| `string` ; `kibana.alert.rule.rule_type_id?`: ``null`` \| `string` } }[] | +| `operation` | `Update` \| `Get` \| `Find` | #### Returns @@ -169,7 +172,39 @@ Accepts an array of ES documents and executes ensureAuthorized for the given ope #### Defined in -[alerts_client.ts:111](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L111) +[alerts_client.ts:152](https://github.com/elastic/kibana/blob/42f5a948210/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L152) + +___ + +### find + +▸ **find**(`__namedParameters`): `Promise`\>\>\> + +#### Type parameters + +| Name | Type | +| :------ | :------ | +| `Params` | `Params`: `AlertTypeParams` = `never` | + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `__namedParameters` | `Object` | +| `__namedParameters._source?` | `string`[] | +| `__namedParameters.aggs?` | `object` | +| `__namedParameters.index` | `undefined` \| `string` | +| `__namedParameters.query?` | `object` | +| `__namedParameters.size?` | `number` | +| `__namedParameters.track_total_hits?` | `boolean` | + +#### Returns + +`Promise`\>\>\> + +#### Defined in + +[alerts_client.ts:628](https://github.com/elastic/kibana/blob/42f5a948210/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L628) ___ @@ -189,7 +224,28 @@ ___ #### Defined in -[alerts_client.ts:407](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L407) +[alerts_client.ts:491](https://github.com/elastic/kibana/blob/42f5a948210/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L491) + +___ + +### getAlertStatusFieldUpdate + +▸ `Private` **getAlertStatusFieldUpdate**(`source`, `status`): { `kibana.alert.workflow_status`: `undefined` ; `signal`: { `status`: `STATUS\_VALUES` } } \| { `kibana.alert.workflow_status`: `STATUS\_VALUES` ; `signal`: `undefined` } + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `source` | `undefined` \| `OutputOf`\> | +| `status` | `STATUS\_VALUES` | + +#### Returns + +{ `kibana.alert.workflow_status`: `undefined` ; `signal`: { `status`: `STATUS\_VALUES` } } \| { `kibana.alert.workflow_status`: `STATUS\_VALUES` ; `signal`: `undefined` } + +#### Defined in + +[alerts_client.ts:137](https://github.com/elastic/kibana/blob/42f5a948210/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L137) ___ @@ -209,7 +265,31 @@ ___ #### Defined in -[alerts_client.ts:533](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L533) +[alerts_client.ts:674](https://github.com/elastic/kibana/blob/42f5a948210/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L674) + +___ + +### getOutcome + +▸ `Private` **getOutcome**(`operation`): `Object` + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `operation` | `Update` \| `Get` \| `Find` | + +#### Returns + +`Object` + +| Name | Type | +| :------ | :------ | +| `outcome` | `EcsEventOutcome` | + +#### Defined in + +[alerts_client.ts:129](https://github.com/elastic/kibana/blob/42f5a948210/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L129) ___ @@ -226,7 +306,7 @@ When an update by ids is requested, do a multi-get, ensure authz and audit alert | `__namedParameters` | `Object` | | `__namedParameters.ids` | `string`[] | | `__namedParameters.indexName` | `string` | -| `__namedParameters.operation` | `Get` \| `Find` \| `Update` | +| `__namedParameters.operation` | `Update` \| `Get` \| `Find` | | `__namedParameters.status` | `STATUS\_VALUES` | #### Returns @@ -235,13 +315,13 @@ When an update by ids is requested, do a multi-get, ensure authz and audit alert #### Defined in -[alerts_client.ts:252](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L252) +[alerts_client.ts:308](https://github.com/elastic/kibana/blob/42f5a948210/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L308) ___ ### queryAndAuditAllAlerts -▸ `Private` **queryAndAuditAllAlerts**(`__namedParameters`): `Promise` +▸ `Private` **queryAndAuditAllAlerts**(`__namedParameters`): `Promise` executes a search after to find alerts with query (+ authz filter) @@ -251,16 +331,16 @@ executes a search after to find alerts with query (+ authz filter) | :------ | :------ | | `__namedParameters` | `Object` | | `__namedParameters.index` | `string` | -| `__namedParameters.operation` | `Get` \| `Find` \| `Update` | -| `__namedParameters.query` | `string` | +| `__namedParameters.operation` | `Update` \| `Get` \| `Find` | +| `__namedParameters.query` | `string` \| `object` | #### Returns -`Promise` +`Promise` #### Defined in -[alerts_client.ts:343](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L343) +[alerts_client.ts:423](https://github.com/elastic/kibana/blob/42f5a948210/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L423) ___ @@ -283,7 +363,7 @@ In the future we will add an "aggs" param #### Defined in -[alerts_client.ts:176](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L176) +[alerts_client.ts:220](https://github.com/elastic/kibana/blob/42f5a948210/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L220) ___ @@ -309,4 +389,4 @@ ___ #### Defined in -[alerts_client.ts:432](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L432) +[alerts_client.ts:520](https://github.com/elastic/kibana/blob/42f5a948210/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L520) diff --git a/x-pack/plugins/rule_registry/docs/alerts_client/interfaces/bulkupdateoptions.md b/x-pack/plugins/rule_registry/docs/alerts_client/interfaces/bulkupdateoptions.md index 28c49c3519f6e..e27790aefbe2a 100644 --- a/x-pack/plugins/rule_registry/docs/alerts_client/interfaces/bulkupdateoptions.md +++ b/x-pack/plugins/rule_registry/docs/alerts_client/interfaces/bulkupdateoptions.md @@ -25,7 +25,7 @@ #### Defined in -[alerts_client.ts:64](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L64) +[alerts_client.ts:84](https://github.com/elastic/kibana/blob/42f5a948210/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L84) ___ @@ -35,17 +35,17 @@ ___ #### Defined in -[alerts_client.ts:66](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L66) +[alerts_client.ts:86](https://github.com/elastic/kibana/blob/42f5a948210/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L86) ___ ### query -• **query**: `undefined` \| ``null`` \| `string` +• **query**: `undefined` \| ``null`` \| `string` \| `object` #### Defined in -[alerts_client.ts:67](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L67) +[alerts_client.ts:87](https://github.com/elastic/kibana/blob/42f5a948210/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L87) ___ @@ -55,4 +55,4 @@ ___ #### Defined in -[alerts_client.ts:65](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L65) +[alerts_client.ts:85](https://github.com/elastic/kibana/blob/42f5a948210/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L85) diff --git a/x-pack/plugins/rule_registry/docs/alerts_client/interfaces/constructoroptions.md b/x-pack/plugins/rule_registry/docs/alerts_client/interfaces/constructoroptions.md index c371719dbced3..a2e24106aa002 100644 --- a/x-pack/plugins/rule_registry/docs/alerts_client/interfaces/constructoroptions.md +++ b/x-pack/plugins/rule_registry/docs/alerts_client/interfaces/constructoroptions.md @@ -19,7 +19,7 @@ #### Defined in -[alerts_client.ts:52](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L52) +[alerts_client.ts:72](https://github.com/elastic/kibana/blob/42f5a948210/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L72) ___ @@ -29,7 +29,7 @@ ___ #### Defined in -[alerts_client.ts:51](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L51) +[alerts_client.ts:71](https://github.com/elastic/kibana/blob/42f5a948210/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L71) ___ @@ -39,7 +39,7 @@ ___ #### Defined in -[alerts_client.ts:53](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L53) +[alerts_client.ts:73](https://github.com/elastic/kibana/blob/42f5a948210/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L73) ___ @@ -49,4 +49,4 @@ ___ #### Defined in -[alerts_client.ts:50](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L50) +[alerts_client.ts:70](https://github.com/elastic/kibana/blob/42f5a948210/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L70) diff --git a/x-pack/plugins/rule_registry/docs/alerts_client/interfaces/updateoptions.md b/x-pack/plugins/rule_registry/docs/alerts_client/interfaces/updateoptions.md index f05a061b279d9..b868123345b4a 100644 --- a/x-pack/plugins/rule_registry/docs/alerts_client/interfaces/updateoptions.md +++ b/x-pack/plugins/rule_registry/docs/alerts_client/interfaces/updateoptions.md @@ -25,7 +25,7 @@ #### Defined in -[alerts_client.ts:59](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L59) +[alerts_client.ts:79](https://github.com/elastic/kibana/blob/42f5a948210/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L79) ___ @@ -35,7 +35,7 @@ ___ #### Defined in -[alerts_client.ts:57](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L57) +[alerts_client.ts:77](https://github.com/elastic/kibana/blob/42f5a948210/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L77) ___ @@ -45,7 +45,7 @@ ___ #### Defined in -[alerts_client.ts:60](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L60) +[alerts_client.ts:80](https://github.com/elastic/kibana/blob/42f5a948210/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L80) ___ @@ -55,4 +55,4 @@ ___ #### Defined in -[alerts_client.ts:58](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L58) +[alerts_client.ts:78](https://github.com/elastic/kibana/blob/42f5a948210/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L78) diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.mock.ts b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.mock.ts index ee81a39052522..d2e841a79cb31 100644 --- a/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.mock.ts +++ b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.mock.ts @@ -17,6 +17,7 @@ const createAlertsClientMock = () => { update: jest.fn(), getAuthorizedAlertsIndices: jest.fn(), bulkUpdate: jest.fn(), + find: jest.fn(), }; return mocked; }; diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts index e78f5f6d51cd2..75b63fe51f7cb 100644 --- a/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts +++ b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts @@ -56,7 +56,7 @@ type AlertType = { _index: string; _id: string } & NonNullableProps< typeof ALERT_RULE_TYPE_ID | typeof ALERT_RULE_CONSUMER | typeof SPACE_IDS >; -const isValidAlert = (source?: estypes.SearchHit): source is AlertType => { +const isValidAlert = (source?: estypes.SearchHit): source is AlertType => { return ( (source?._source?.[ALERT_RULE_TYPE_ID] != null && source?._source?.[ALERT_RULE_CONSUMER] != null && @@ -93,11 +93,15 @@ interface GetAlertParams { } interface SingleSearchAfterAndAudit { - id: string | null | undefined; - query: object | string | null | undefined; + id?: string | null | undefined; + query?: string | object | undefined; + aggs?: object | undefined; index?: string; + _source?: string[] | undefined; + track_total_hits?: boolean | undefined; + size?: number | undefined; operation: WriteOperations.Update | ReadOperations.Find | ReadOperations.Get; - lastSortIds: Array | undefined; + lastSortIds?: Array | undefined; } /** @@ -216,6 +220,10 @@ export class AlertsClient { private async singleSearchAfterAndAudit({ id, query, + aggs, + _source, + track_total_hits: trackTotalHits, + size, index, operation, lastSortIds = [], @@ -233,6 +241,10 @@ export class AlertsClient { let queryBody = { fields: [ALERT_RULE_TYPE_ID, ALERT_RULE_CONSUMER, ALERT_WORKFLOW_STATUS, SPACE_IDS], query: await this.buildEsQueryWithAuthz(query, id, alertSpaceId, operation, config), + aggs, + _source, + track_total_hits: trackTotalHits, + size, sort: [ { '@timestamp': { @@ -265,17 +277,19 @@ export class AlertsClient { throw Boom.badData(errorMessage); } - await this.ensureAllAuthorized(result.body.hits.hits, operation); - - result?.body.hits.hits.map((item) => - this.auditLogger?.log( - alertAuditEvent({ - action: operationAlertAuditActionMap[operation], - id: item._id, - ...this.getOutcome(operation), - }) - ) - ); + if (result?.body?.hits?.hits != null && result?.body.hits.hits.length > 0) { + await this.ensureAllAuthorized(result.body.hits.hits, operation); + + result?.body.hits.hits.map((item) => + this.auditLogger?.log( + alertAuditEvent({ + action: operationAlertAuditActionMap[operation], + id: item._id, + ...this.getOutcome(operation), + }) + ) + ); + } return result.body; } catch (error) { @@ -474,10 +488,8 @@ export class AlertsClient { // first search for the alert by id, then use the alert info to check if user has access to it const alert = await this.singleSearchAfterAndAudit({ id, - query: undefined, index, operation: ReadOperations.Get, - lastSortIds: undefined, }); if (alert == null || alert.hits.hits.length === 0) { @@ -503,10 +515,8 @@ export class AlertsClient { try { const alert = await this.singleSearchAfterAndAudit({ id, - query: null, index, operation: WriteOperations.Update, - lastSortIds: undefined, }); if (alert == null || alert.hits.hits.length === 0) { @@ -565,7 +575,7 @@ export class AlertsClient { }); if (!fetchAndAuditResponse?.auditedAlerts) { - throw Boom.unauthorized('Failed to audit alerts'); + throw Boom.forbidden('Failed to audit alerts'); } // executes updateByQuery with query + authorization filter @@ -598,6 +608,46 @@ export class AlertsClient { } } + public async find({ + query, + aggs, + _source, + track_total_hits: trackTotalHits, + size, + index, + }: { + query?: object | undefined; + aggs?: object | undefined; + index: string | undefined; + track_total_hits?: boolean | undefined; + _source?: string[] | undefined; + size?: number | undefined; + }) { + try { + // first search for the alert by id, then use the alert info to check if user has access to it + const alertsSearchResponse = await this.singleSearchAfterAndAudit({ + query, + aggs, + _source, + track_total_hits: trackTotalHits, + size, + index, + operation: ReadOperations.Find, + }); + + if (alertsSearchResponse == null) { + const errorMessage = `Unable to retrieve alert details for alert with query and operation ${ReadOperations.Find}`; + this.logger.error(errorMessage); + throw Boom.notFound(errorMessage); + } + + return alertsSearchResponse; + } catch (error) { + this.logger.error(`find threw an error: ${error}`); + throw error; + } + } + public async getAuthorizedAlertsIndices(featureIds: string[]): Promise { try { const augmentedRuleTypes = await this.authorization.getAugmentedRuleTypesWithAuthorization( diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/tests/bulk_update.test.ts b/x-pack/plugins/rule_registry/server/alert_data_client/tests/bulk_update.test.ts index a6d42853531d7..11066ffddfadd 100644 --- a/x-pack/plugins/rule_registry/server/alert_data_client/tests/bulk_update.test.ts +++ b/x-pack/plugins/rule_registry/server/alert_data_client/tests/bulk_update.test.ts @@ -41,6 +41,12 @@ beforeEach(() => { alertingAuthMock.getAuthorizationFilter.mockImplementation(async () => Promise.resolve({ filter: [] }) ); + // @ts-expect-error + alertingAuthMock.getAugmentedRuleTypesWithAuthorization.mockImplementation(async () => { + const authorizedRuleTypes = new Set(); + authorizedRuleTypes.add({ producer: 'apm' }); + return Promise.resolve({ authorizedRuleTypes }); + }); alertingAuthMock.ensureAuthorized.mockImplementation( // @ts-expect-error async ({ diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/tests/find_alerts.test.ts b/x-pack/plugins/rule_registry/server/alert_data_client/tests/find_alerts.test.ts new file mode 100644 index 0000000000000..1e6601c7b0862 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/alert_data_client/tests/find_alerts.test.ts @@ -0,0 +1,422 @@ +/* + * 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 { + ALERT_RULE_CONSUMER, + ALERT_RULE_TYPE_ID, + SPACE_IDS, + ALERT_WORKFLOW_STATUS, +} from '@kbn/rule-data-utils'; +import { AlertsClient, ConstructorOptions } from '../alerts_client'; +import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; +import { alertingAuthorizationMock } from '../../../../alerting/server/authorization/alerting_authorization.mock'; +import { AuditLogger } from '../../../../security/server'; +import { AlertingAuthorizationEntity } from '../../../../alerting/server'; + +const alertingAuthMock = alertingAuthorizationMock.create(); +const esClientMock = elasticsearchClientMock.createElasticsearchClient(); +const auditLogger = { + log: jest.fn(), +} as jest.Mocked; + +const alertsClientParams: jest.Mocked = { + logger: loggingSystemMock.create().get(), + authorization: alertingAuthMock, + esClient: esClientMock, + auditLogger, +}; + +const DEFAULT_SPACE = 'test_default_space_id'; + +beforeEach(() => { + jest.resetAllMocks(); + alertingAuthMock.getSpaceId.mockImplementation(() => DEFAULT_SPACE); + // @ts-expect-error + alertingAuthMock.getAuthorizationFilter.mockImplementation(async () => + Promise.resolve({ filter: [] }) + ); + // @ts-expect-error + alertingAuthMock.getAugmentedRuleTypesWithAuthorization.mockImplementation(async () => { + const authorizedRuleTypes = new Set(); + authorizedRuleTypes.add({ producer: 'apm' }); + return Promise.resolve({ authorizedRuleTypes }); + }); + + alertingAuthMock.ensureAuthorized.mockImplementation( + // @ts-expect-error + async ({ + ruleTypeId, + consumer, + operation, + entity, + }: { + ruleTypeId: string; + consumer: string; + operation: string; + entity: typeof AlertingAuthorizationEntity.Alert; + }) => { + if (ruleTypeId === 'apm.error_rate' && consumer === 'apm') { + return Promise.resolve(); + } + return Promise.reject(new Error(`Unauthorized for ${ruleTypeId} and ${consumer}`)); + } + ); +}); + +describe('find()', () => { + test('calls ES client with given params', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + esClientMock.search.mockResolvedValueOnce( + elasticsearchClientMock.createApiResponse({ + body: { + took: 5, + timed_out: false, + _shards: { + total: 1, + successful: 1, + failed: 0, + skipped: 0, + }, + hits: { + total: 1, + max_score: 999, + hits: [ + { + found: true, + _type: 'alert', + _index: '.alerts-observability-apm', + _id: 'NoxgpHkBqbdrfX07MqXV', + _version: 1, + _seq_no: 362, + _primary_term: 2, + _source: { + [ALERT_RULE_TYPE_ID]: 'apm.error_rate', + message: 'hello world 1', + [ALERT_RULE_CONSUMER]: 'apm', + [ALERT_WORKFLOW_STATUS]: 'open', + [SPACE_IDS]: ['test_default_space_id'], + }, + }, + ], + }, + }, + }) + ); + const result = await alertsClient.find({ + query: { match: { [ALERT_WORKFLOW_STATUS]: 'open' } }, + index: '.alerts-observability-apm', + }); + expect(result).toMatchInlineSnapshot(` + Object { + "_shards": Object { + "failed": 0, + "skipped": 0, + "successful": 1, + "total": 1, + }, + "hits": Object { + "hits": Array [ + Object { + "_id": "NoxgpHkBqbdrfX07MqXV", + "_index": ".alerts-observability-apm", + "_primary_term": 2, + "_seq_no": 362, + "_source": Object { + "kibana.alert.rule.consumer": "apm", + "kibana.alert.rule.rule_type_id": "apm.error_rate", + "kibana.alert.workflow_status": "open", + "kibana.space_ids": Array [ + "test_default_space_id", + ], + "message": "hello world 1", + }, + "_type": "alert", + "_version": 1, + "found": true, + }, + ], + "max_score": 999, + "total": 1, + }, + "timed_out": false, + "took": 5, + } + `); + expect(esClientMock.search).toHaveBeenCalledTimes(1); + expect(esClientMock.search.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "body": Object { + "_source": undefined, + "aggs": undefined, + "fields": Array [ + "kibana.alert.rule.rule_type_id", + "kibana.alert.rule.consumer", + "kibana.alert.workflow_status", + "kibana.space_ids", + ], + "query": Object { + "bool": Object { + "filter": Array [ + Object {}, + Object { + "term": Object { + "kibana.space_ids": "test_default_space_id", + }, + }, + ], + "must": Array [ + Object { + "match": Object { + "kibana.alert.workflow_status": "open", + }, + }, + ], + "must_not": Array [], + "should": Array [], + }, + }, + "size": undefined, + "sort": Array [ + Object { + "@timestamp": Object { + "order": "asc", + "unmapped_type": "date", + }, + }, + ], + "track_total_hits": undefined, + }, + "ignore_unavailable": true, + "index": ".alerts-observability-apm", + "seq_no_primary_term": true, + }, + ] + `); + }); + + test('logs successful event in audit logger', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + esClientMock.search.mockResolvedValueOnce( + elasticsearchClientMock.createApiResponse({ + body: { + took: 5, + timed_out: false, + _shards: { + total: 1, + successful: 1, + failed: 0, + skipped: 0, + }, + hits: { + total: 1, + max_score: 999, + hits: [ + { + found: true, + _type: 'alert', + _index: '.alerts-observability-apm', + _id: 'NoxgpHkBqbdrfX07MqXV', + _version: 1, + _seq_no: 362, + _primary_term: 2, + _source: { + [ALERT_RULE_TYPE_ID]: 'apm.error_rate', + message: 'hello world 1', + [ALERT_RULE_CONSUMER]: 'apm', + [ALERT_WORKFLOW_STATUS]: 'open', + [SPACE_IDS]: ['test_default_space_id'], + }, + }, + ], + }, + }, + }) + ); + await alertsClient.find({ + query: { match: { [ALERT_WORKFLOW_STATUS]: 'open' } }, + index: '.alerts-observability-apm', + }); + + expect(auditLogger.log).toHaveBeenCalledWith({ + error: undefined, + event: { action: 'alert_find', category: ['database'], outcome: 'success', type: ['access'] }, + message: 'User has accessed alert [id=NoxgpHkBqbdrfX07MqXV]', + }); + }); + + test('audit error access if user is unauthorized for given alert', async () => { + const indexName = '.alerts-observability-apm'; + const fakeAlertId = 'myfakeid1'; + // fakeRuleTypeId will cause authz to fail + const fakeRuleTypeId = 'fake.rule'; + const alertsClient = new AlertsClient(alertsClientParams); + esClientMock.search.mockResolvedValueOnce( + elasticsearchClientMock.createApiResponse({ + body: { + took: 5, + timed_out: false, + _shards: { + total: 1, + successful: 1, + failed: 0, + skipped: 0, + }, + hits: { + total: 1, + max_score: 999, + hits: [ + { + found: true, + _type: 'alert', + _version: 1, + _seq_no: 362, + _primary_term: 2, + _id: fakeAlertId, + _index: indexName, + _source: { + [ALERT_RULE_TYPE_ID]: fakeRuleTypeId, + [ALERT_RULE_CONSUMER]: 'apm', + [ALERT_WORKFLOW_STATUS]: 'open', + [SPACE_IDS]: [DEFAULT_SPACE], + }, + }, + ], + }, + }, + }) + ); + + await expect( + alertsClient.find({ + query: { match: { [ALERT_WORKFLOW_STATUS]: 'open' } }, + index: '.alerts-observability-apm', + }) + ).rejects.toThrowErrorMatchingInlineSnapshot(` + "Unable to retrieve alert details for alert with id of \\"undefined\\" or with query \\"[object Object]\\" and operation find + Error: Error: Unauthorized for fake.rule and apm" + `); + + expect(auditLogger.log).toHaveBeenNthCalledWith(1, { + message: `Failed attempt to access alert [id=${fakeAlertId}]`, + event: { + action: 'alert_find', + category: ['database'], + outcome: 'failure', + type: ['access'], + }, + error: { + code: 'Error', + message: 'Unauthorized for fake.rule and apm', + }, + }); + }); + + test(`throws an error if ES client get fails`, async () => { + const error = new Error('something went wrong'); + const alertsClient = new AlertsClient(alertsClientParams); + esClientMock.search.mockRejectedValue(error); + + await expect( + alertsClient.find({ + query: { match: { [ALERT_WORKFLOW_STATUS]: 'open' } }, + index: '.alerts-observability-apm', + }) + ).rejects.toThrowErrorMatchingInlineSnapshot(` + "Unable to retrieve alert details for alert with id of \\"undefined\\" or with query \\"[object Object]\\" and operation find + Error: Error: something went wrong" + `); + }); + + describe('authorization', () => { + beforeEach(() => { + esClientMock.search.mockResolvedValueOnce( + elasticsearchClientMock.createApiResponse({ + body: { + took: 5, + timed_out: false, + _shards: { + total: 1, + successful: 1, + failed: 0, + skipped: 0, + }, + hits: { + total: 1, + max_score: 999, + hits: [ + { + found: true, + _type: 'alert', + _index: '.alerts-observability-apm', + _id: 'NoxgpHkBqbdrfX07MqXV', + _version: 1, + _seq_no: 362, + _primary_term: 2, + _source: { + [ALERT_RULE_TYPE_ID]: 'apm.error_rate', + message: 'hello world 1', + [ALERT_RULE_CONSUMER]: 'apm', + [ALERT_WORKFLOW_STATUS]: 'open', + [SPACE_IDS]: ['test_default_space_id'], + }, + }, + ], + }, + }, + }) + ); + }); + + test('returns alert if user is authorized to read alert under the consumer', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + const result = await alertsClient.find({ + query: { match: { [ALERT_WORKFLOW_STATUS]: 'open' } }, + index: '.alerts-observability-apm', + }); + + expect(result).toMatchInlineSnapshot(` + Object { + "_shards": Object { + "failed": 0, + "skipped": 0, + "successful": 1, + "total": 1, + }, + "hits": Object { + "hits": Array [ + Object { + "_id": "NoxgpHkBqbdrfX07MqXV", + "_index": ".alerts-observability-apm", + "_primary_term": 2, + "_seq_no": 362, + "_source": Object { + "kibana.alert.rule.consumer": "apm", + "kibana.alert.rule.rule_type_id": "apm.error_rate", + "kibana.alert.workflow_status": "open", + "kibana.space_ids": Array [ + "test_default_space_id", + ], + "message": "hello world 1", + }, + "_type": "alert", + "_version": 1, + "found": true, + }, + ], + "max_score": 999, + "total": 1, + }, + "timed_out": false, + "took": 5, + } + `); + }); + }); +}); diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/tests/get.test.ts b/x-pack/plugins/rule_registry/server/alert_data_client/tests/get.test.ts index 1a0628bf6e9a8..2f299142166d6 100644 --- a/x-pack/plugins/rule_registry/server/alert_data_client/tests/get.test.ts +++ b/x-pack/plugins/rule_registry/server/alert_data_client/tests/get.test.ts @@ -42,6 +42,13 @@ beforeEach(() => { Promise.resolve({ filter: [] }) ); + // @ts-expect-error + alertingAuthMock.getAugmentedRuleTypesWithAuthorization.mockImplementation(async () => { + const authorizedRuleTypes = new Set(); + authorizedRuleTypes.add({ producer: 'apm' }); + return Promise.resolve({ authorizedRuleTypes }); + }); + alertingAuthMock.ensureAuthorized.mockImplementation( // @ts-expect-error async ({ @@ -119,6 +126,8 @@ describe('get()', () => { Array [ Object { "body": Object { + "_source": undefined, + "aggs": undefined, "fields": Array [ "kibana.alert.rule.rule_type_id", "kibana.alert.rule.consumer", @@ -152,6 +161,7 @@ describe('get()', () => { "should": Array [], }, }, + "size": undefined, "sort": Array [ Object { "@timestamp": Object { @@ -160,6 +170,7 @@ describe('get()', () => { }, }, ], + "track_total_hits": undefined, }, "ignore_unavailable": true, "index": ".alerts-observability-apm", @@ -258,8 +269,8 @@ describe('get()', () => { }) ); - await expect(alertsClient.get({ id: fakeAlertId, index: '.alerts-observability-apm' })).rejects - .toThrowErrorMatchingInlineSnapshot(` + await expect(alertsClient.get({ id: fakeAlertId, index: '.alerts-observability-apm.alerts' })) + .rejects.toThrowErrorMatchingInlineSnapshot(` "Unable to retrieve alert details for alert with id of \\"myfakeid1\\" or with query \\"undefined\\" and operation get Error: Error: Unauthorized for fake.rule and apm" `); diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/tests/update.test.ts b/x-pack/plugins/rule_registry/server/alert_data_client/tests/update.test.ts index 4e084c2c028b1..90ca2da06ccdf 100644 --- a/x-pack/plugins/rule_registry/server/alert_data_client/tests/update.test.ts +++ b/x-pack/plugins/rule_registry/server/alert_data_client/tests/update.test.ts @@ -42,6 +42,13 @@ beforeEach(() => { Promise.resolve({ filter: [] }) ); + // @ts-expect-error + alertingAuthMock.getAugmentedRuleTypesWithAuthorization.mockImplementation(async () => { + const authorizedRuleTypes = new Set(); + authorizedRuleTypes.add({ producer: 'apm' }); + return Promise.resolve({ authorizedRuleTypes }); + }); + alertingAuthMock.ensureAuthorized.mockImplementation( // @ts-expect-error async ({ @@ -267,7 +274,7 @@ describe('update()', () => { index: '.alerts-observability-apm', }) ).rejects.toThrowErrorMatchingInlineSnapshot(` - "Unable to retrieve alert details for alert with id of \\"myfakeid1\\" or with query \\"null\\" and operation update + "Unable to retrieve alert details for alert with id of \\"myfakeid1\\" or with query \\"undefined\\" and operation update Error: Error: Unauthorized for fake.rule and apm" `); @@ -299,7 +306,7 @@ describe('update()', () => { index: '.alerts-observability-apm', }) ).rejects.toThrowErrorMatchingInlineSnapshot(` - "Unable to retrieve alert details for alert with id of \\"NoxgpHkBqbdrfX07MqXV\\" or with query \\"null\\" and operation update + "Unable to retrieve alert details for alert with id of \\"NoxgpHkBqbdrfX07MqXV\\" or with query \\"undefined\\" and operation update Error: Error: something went wrong on update" `); }); diff --git a/x-pack/plugins/rule_registry/server/routes/find.ts b/x-pack/plugins/rule_registry/server/routes/find.ts new file mode 100644 index 0000000000000..8fb3c116e171c --- /dev/null +++ b/x-pack/plugins/rule_registry/server/routes/find.ts @@ -0,0 +1,86 @@ +/* + * 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 { IRouter } from 'kibana/server'; +import * as t from 'io-ts'; +import { id as _id } from '@kbn/securitysolution-io-ts-list-types'; +import { transformError } from '@kbn/securitysolution-es-utils'; +import { PositiveInteger } from '@kbn/securitysolution-io-ts-types'; + +import { RacRequestHandlerContext } from '../types'; +import { BASE_RAC_ALERTS_API_PATH } from '../../common/constants'; +import { buildRouteValidation } from './utils/route_validation'; +import { BucketAggsSchemas } from '../../common/types'; + +export const findAlertsByQueryRoute = (router: IRouter) => { + router.post( + { + path: `${BASE_RAC_ALERTS_API_PATH}/find`, + validate: { + body: buildRouteValidation( + t.exact( + t.partial({ + index: t.string, + query: t.object, + aggs: t.union([t.record(t.string, BucketAggsSchemas), t.undefined]), + size: t.union([PositiveInteger, t.undefined]), + track_total_hits: t.union([t.boolean, t.undefined]), + _source: t.union([t.array(t.string), t.undefined]), + }) + ) + ), + }, + options: { + tags: ['access:rac'], + }, + }, + async (context, request, response) => { + try { + // eslint-disable-next-line @typescript-eslint/naming-convention + const { query, aggs, _source, track_total_hits, size, index } = request.body; + + const alertsClient = await context.rac.getAlertsClient(); + + const alerts = await alertsClient.find({ + query, + aggs, + _source, + track_total_hits, + size, + index, + }); + if (alerts == null) { + return response.notFound({ + body: { message: `alerts with query and index ${index} not found` }, + }); + } + return response.ok({ + body: alerts, + }); + } catch (exc) { + const err = transformError(exc); + const contentType = { + 'content-type': 'application/json', + }; + const defaultedHeaders = { + ...contentType, + }; + + return response.customError({ + headers: defaultedHeaders, + statusCode: err.statusCode, + body: { + message: err.message, + attributes: { + success: false, + }, + }, + }); + } + } + ); +}; diff --git a/x-pack/plugins/rule_registry/server/routes/index.ts b/x-pack/plugins/rule_registry/server/routes/index.ts index 0b900f26e56e6..4de121c7b9b5e 100644 --- a/x-pack/plugins/rule_registry/server/routes/index.ts +++ b/x-pack/plugins/rule_registry/server/routes/index.ts @@ -11,10 +11,12 @@ import { getAlertByIdRoute } from './get_alert_by_id'; import { updateAlertByIdRoute } from './update_alert_by_id'; import { getAlertsIndexRoute } from './get_alert_index'; import { bulkUpdateAlertsRoute } from './bulk_update_alerts'; +import { findAlertsByQueryRoute } from './find'; export function defineRoutes(router: IRouter) { getAlertByIdRoute(router); updateAlertByIdRoute(router); getAlertsIndexRoute(router); bulkUpdateAlertsRoute(router); + findAlertsByQueryRoute(router); } diff --git a/x-pack/plugins/rule_registry/server/scripts/find_observability_alert.sh b/x-pack/plugins/rule_registry/server/scripts/find_observability_alert.sh new file mode 100755 index 0000000000000..4c4ee5f75836c --- /dev/null +++ b/x-pack/plugins/rule_registry/server/scripts/find_observability_alert.sh @@ -0,0 +1,29 @@ +#!/bin/sh + +# +# 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. +# + +set -e + +IDS=${1:-[\"Do4JnHoBqkRSppNZ6vre\"]} +STATUS=${2} + +echo $IDS +echo "'"$STATUS"'" + +cd ./hunter && sh ./post_detections_role.sh && sh ./post_detections_user.sh +cd ../observer && sh ./post_detections_role.sh && sh ./post_detections_user.sh +cd .. + +# Example: ./update_observability_alert.sh [\"my-alert-id\",\"another-alert-id\"] +# curl -s -k \ +curl -v \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ + -u observer:changeme \ + -X POST ${KIBANA_URL}${SPACE_URL}/internal/rac/alerts/find \ +-d "{\"query\": { \"match\": { \"kibana.alert.status\": \"open\" }}, \"index\":\".alerts-observability-apm\"}" | jq . \ No newline at end of file diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index 346568c3a9609..548716880478b 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -251,6 +251,9 @@ export const DETECTION_ENGINE_SIGNALS_MIGRATION_URL = `${DETECTION_ENGINE_SIGNAL export const DETECTION_ENGINE_SIGNALS_MIGRATION_STATUS_URL = `${DETECTION_ENGINE_SIGNALS_URL}/migration_status`; export const DETECTION_ENGINE_SIGNALS_FINALIZE_MIGRATION_URL = `${DETECTION_ENGINE_SIGNALS_URL}/finalize_migration`; +export const ALERTS_AS_DATA_URL = '/internal/rac/alerts'; +export const ALERTS_AS_DATA_FIND_URL = `${ALERTS_AS_DATA_URL}/find`; + /** * Common naming convention for an unauthenticated user */ diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx index 001567d7d2cc8..9cc844a80b031 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx @@ -14,6 +14,8 @@ import { HeaderSection } from '../../../../common/components/header_section'; import { useQueryAlerts } from '../../../containers/detection_engine/alerts/use_query'; import { InspectButtonContainer } from '../../../../common/components/inspect'; +import { fetchQueryRuleRegistryAlerts } from '../../../containers/detection_engine/alerts/api'; + import { getAlertsCountQuery } from './helpers'; import * as i18n from './translations'; import { AlertsCount } from './alerts_count'; @@ -42,6 +44,13 @@ export const AlertsCountPanel = memo( DEFAULT_STACK_BY_FIELD ); + // TODO: Once we are past experimental phase this code should be removed + // const fetchMethod = useIsExperimentalFeatureEnabled('ruleRegistryEnabled') + // ? fetchQueryRuleRegistryAlerts + // : fetchQueryAlerts; + + const fetchMethod = fetchQueryRuleRegistryAlerts; + const additionalFilters = useMemo(() => { try { return [ @@ -64,6 +73,7 @@ export const AlertsCountPanel = memo( request, refetch, } = useQueryAlerts<{}, AlertsCountAggregation>({ + fetchMethod, query: getAlertsCountQuery(selectedStackByOption, from, to, additionalFilters), indexName: signalIndexName, }); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx index 2182ed7da0c4f..b296371bae58d 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx @@ -43,6 +43,7 @@ import type { AlertsStackByField } from '../common/types'; import { KpiPanel, StackBySelect } from '../common/components'; import { useInspectButton } from '../common/hooks'; +import { fetchQueryRuleRegistryAlerts } from '../../../containers/detection_engine/alerts/api'; const defaultTotalAlertsObj: AlertsTotal = { value: 0, @@ -116,12 +117,16 @@ export const AlertsHistogramPanel = memo( request, refetch, } = useQueryAlerts<{}, AlertsAggregation>({ - query: getAlertsHistogramQuery( - selectedStackByOption, - from, - to, - buildCombinedQueries(combinedQueries) - ), + fetchMethod: fetchQueryRuleRegistryAlerts, + query: { + index: signalIndexName, + ...getAlertsHistogramQuery( + selectedStackByOption, + from, + to, + buildCombinedQueries(combinedQueries) + ), + }, indexName: signalIndexName, }); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/api.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/api.ts index 3d4a7dba0de57..c2930a4ae59ff 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/api.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/api.ts @@ -13,6 +13,7 @@ import { DETECTION_ENGINE_SIGNALS_STATUS_URL, DETECTION_ENGINE_INDEX_URL, DETECTION_ENGINE_PRIVILEGES_URL, + ALERTS_AS_DATA_FIND_URL, } from '../../../../../common/constants'; import { HOST_METADATA_GET_ROUTE } from '../../../../../common/endpoint/constants'; import { KibanaServices } from '../../../../common/lib/kibana'; @@ -39,8 +40,8 @@ import { resolvePathVariables } from '../../../../common/utils/resolve_path_vari export const fetchQueryAlerts = async ({ query, signal, -}: QueryAlerts): Promise> => - KibanaServices.get().http.fetch>( +}: QueryAlerts): Promise> => { + return KibanaServices.get().http.fetch>( DETECTION_ENGINE_QUERY_SIGNALS_URL, { method: 'POST', @@ -48,6 +49,29 @@ export const fetchQueryAlerts = async ({ signal, } ); +}; + +/** + * Fetch Alerts by providing a query + * + * @param query String to match a dsl + * @param signal to cancel request + * + * @throws An error if response is not OK + */ +export const fetchQueryRuleRegistryAlerts = async ({ + query, + signal, +}: QueryAlerts): Promise> => { + return KibanaServices.get().http.fetch>( + ALERTS_AS_DATA_FIND_URL, + { + method: 'POST', + body: JSON.stringify(query), + signal, + } + ); +}; /** * Update alert status by query diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_query.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_query.tsx index 4d7d80b74a24d..b2bbcdf277992 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_query.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_query.tsx @@ -8,7 +8,7 @@ import { isEmpty } from 'lodash'; import React, { SetStateAction, useEffect, useState } from 'react'; -import { fetchQueryAlerts } from './api'; +import { fetchQueryAlerts, fetchQueryRuleRegistryAlerts } from './api'; import { AlertSearchResponse } from './types'; type Func = () => Promise; @@ -23,6 +23,7 @@ export interface ReturnQueryAlerts { } interface AlertsQueryParams { + fetchMethod?: typeof fetchQueryAlerts | typeof fetchQueryRuleRegistryAlerts; query: object; indexName?: string | null; skip?: boolean; @@ -35,6 +36,7 @@ interface AlertsQueryParams { * */ export const useQueryAlerts = ({ + fetchMethod = fetchQueryAlerts, query: initialQuery, indexName, skip, @@ -58,7 +60,8 @@ export const useQueryAlerts = ({ const fetchData = async () => { try { setLoading(true); - const alertResponse = await fetchQueryAlerts({ + + const alertResponse = await fetchMethod({ query, signal: abortCtrl.signal, }); @@ -95,7 +98,7 @@ export const useQueryAlerts = ({ isSubscribed = false; abortCtrl.abort(); }; - }, [query, indexName, skip]); + }, [query, indexName, skip, fetchMethod]); return { loading, ...alerts }; }; diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/query_signals.ts b/x-pack/test/detection_engine_api_integration/basic/tests/query_signals.ts index 000e3a5dbfa7e..969315cb3f98d 100644 --- a/x-pack/test/detection_engine_api_integration/basic/tests/query_signals.ts +++ b/x-pack/test/detection_engine_api_integration/basic/tests/query_signals.ts @@ -7,7 +7,10 @@ import expect from '@kbn/expect'; -import { DETECTION_ENGINE_QUERY_SIGNALS_URL } from '../../../../plugins/security_solution/common/constants'; +import { + DETECTION_ENGINE_QUERY_SIGNALS_URL, + ALERTS_AS_DATA_FIND_URL, +} from '../../../../plugins/security_solution/common/constants'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { getSignalStatus, createSignalsIndex, deleteSignalsIndex } from '../../utils'; @@ -15,7 +18,7 @@ import { getSignalStatus, createSignalsIndex, deleteSignalsIndex } from '../../u export default ({ getService }: FtrProviderContext) => { const supertest = getService('supertest'); - describe('query_signals_route', () => { + describe('query_signals_route and find_alerts_route', () => { describe('validation checks', () => { it('should not give errors when querying and the signals index does not exist yet', async () => { const { body } = await supertest @@ -57,5 +60,114 @@ export default ({ getService }: FtrProviderContext) => { await deleteSignalsIndex(supertest); }); }); + + describe('find_alerts_route', () => { + describe('validation checks', () => { + it('should not give errors when querying and the signals index does not exist yet', async () => { + const { body } = await supertest + .post(ALERTS_AS_DATA_FIND_URL) + .set('kbn-xsrf', 'true') + .send({ ...getSignalStatus(), index: '.siem-signals-default' }) + .expect(200); + + // remove any server generated items that are indeterministic + delete body.took; + + expect(body).to.eql({ + timed_out: false, + _shards: { total: 0, successful: 0, skipped: 0, failed: 0 }, + hits: { total: { value: 0, relation: 'eq' }, max_score: 0, hits: [] }, + }); + }); + + it('should not give errors when querying and the signals index does exist and is empty', async () => { + await createSignalsIndex(supertest); + const { body } = await supertest + .post(ALERTS_AS_DATA_FIND_URL) + .set('kbn-xsrf', 'true') + .send({ ...getSignalStatus(), index: '.siem-signals-default' }) + .expect(200); + + // remove any server generated items that are indeterministic + delete body.took; + + expect(body).to.eql({ + timed_out: false, + _shards: { total: 1, successful: 1, skipped: 0, failed: 0 }, + hits: { total: { value: 0, relation: 'eq' }, max_score: null, hits: [] }, + aggregations: { + statuses: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + }); + + await deleteSignalsIndex(supertest); + }); + + it('should not give errors when executing security solution histogram aggs', async () => { + await createSignalsIndex(supertest); + await supertest + .post(ALERTS_AS_DATA_FIND_URL) + .set('kbn-xsrf', 'true') + .send({ + index: '.siem-signals-default', + aggs: { + alertsByGrouping: { + terms: { + field: 'event.category', + missing: 'All others', + order: { _count: 'desc' }, + size: 10, + }, + aggs: { + alerts: { + date_histogram: { + field: '@timestamp', + fixed_interval: '2699999ms', + min_doc_count: 0, + extended_bounds: { + min: '2021-08-17T04:00:00.000Z', + max: '2021-08-18T03:59:59.999Z', + }, + }, + }, + }, + }, + }, + query: { + bool: { + filter: [ + { + bool: { + must: [], + filter: [ + { + match_phrase: { + 'signal.rule.id': 'c76f1a10-ffb6-11eb-8914-9b237bf6808c', + }, + }, + { term: { 'signal.status': 'open' } }, + ], + should: [], + must_not: [{ exists: { field: 'signal.rule.building_block_type' } }], + }, + }, + { + range: { + '@timestamp': { + gte: '2021-08-17T04:00:00.000Z', + lte: '2021-08-18T03:59:59.999Z', + }, + }, + }, + ], + }, + }, + }) + .expect(200); + + await deleteSignalsIndex(supertest); + }); + }); + }); }); }; diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/update_rac_alerts.ts b/x-pack/test/detection_engine_api_integration/basic/tests/update_rac_alerts.ts index 43c9a3d0d7608..e14b29765e7b0 100644 --- a/x-pack/test/detection_engine_api_integration/basic/tests/update_rac_alerts.ts +++ b/x-pack/test/detection_engine_api_integration/basic/tests/update_rac_alerts.ts @@ -37,12 +37,10 @@ export default ({ getService }: FtrProviderContext) => { .post(RAC_ALERTS_BULK_UPDATE_URL) .set('kbn-xsrf', 'true') .send({ ids: ['123'], status: 'open', index: '.siem-signals-default' }); - // remove any server generated items that are indeterministic delete body.took; expect(body).to.eql(getSignalStatusEmptyResponse()); }); - it('should not give errors when querying and the signals index does exist and is empty', async () => { await createSignalsIndex(supertest); await supertest diff --git a/x-pack/test/rule_registry/security_and_spaces/tests/basic/find_alerts.ts b/x-pack/test/rule_registry/security_and_spaces/tests/basic/find_alerts.ts new file mode 100644 index 0000000000000..409fbbde5cac9 --- /dev/null +++ b/x-pack/test/rule_registry/security_and_spaces/tests/basic/find_alerts.ts @@ -0,0 +1,275 @@ +/* + * 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 '@kbn/expect'; + +import { ALERT_WORKFLOW_STATUS } from '../../../../../plugins/rule_registry/common/technical_rule_data_field_names'; +import { + superUser, + globalRead, + obsOnly, + obsOnlyRead, + obsSec, + obsSecRead, + secOnly, + secOnlyRead, + secOnlySpace2, + secOnlyReadSpace2, + obsSecAllSpace2, + obsSecReadSpace2, + obsOnlySpace2, + obsOnlyReadSpace2, + obsOnlySpacesAll, + obsSecSpacesAll, + secOnlySpacesAll, + noKibanaPrivileges, +} from '../../../common/lib/authentication/users'; +import type { User } from '../../../common/lib/authentication/types'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { getSpaceUrlPrefix } from '../../../common/lib/authentication/spaces'; + +interface TestCase { + /** The space where the alert exists */ + space: string; + /** The ID of the alert */ + alertId: string; + /** The index of the alert */ + index: string; + /** Authorized users */ + authorizedUsers: User[]; + /** Unauthorized users */ + unauthorizedUsers: User[]; +} + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const esArchiver = getService('esArchiver'); + + const TEST_URL = '/internal/rac/alerts'; + const ALERTS_INDEX_URL = `${TEST_URL}/index`; + const SPACE1 = 'space1'; + const SPACE2 = 'space2'; + const APM_ALERT_ID = 'NoxgpHkBqbdrfX07MqXV'; + const APM_ALERT_INDEX = '.alerts-observability-apm'; + const SECURITY_SOLUTION_ALERT_ID = '020202'; + const SECURITY_SOLUTION_ALERT_INDEX = '.alerts-security.alerts'; + + const getAPMIndexName = async (user: User) => { + const { + body: indexNames, + }: { body: { index_name: string[] | undefined } } = await supertestWithoutAuth + .get(`${getSpaceUrlPrefix(SPACE1)}${ALERTS_INDEX_URL}`) + .auth(user.username, user.password) + .set('kbn-xsrf', 'true') + .expect(200); + const observabilityIndex = indexNames?.index_name?.find( + (indexName) => indexName === APM_ALERT_INDEX + ); + expect(observabilityIndex).to.eql(APM_ALERT_INDEX); // assert this here so we can use constants in the dynamically-defined test cases below + }; + + const getSecuritySolutionIndexName = async (user: User) => { + const { + body: indexNames, + }: { body: { index_name: string[] | undefined } } = await supertestWithoutAuth + .get(`${getSpaceUrlPrefix(SPACE1)}${ALERTS_INDEX_URL}`) + .auth(user.username, user.password) + .set('kbn-xsrf', 'true') + .expect(200); + const securitySolution = indexNames?.index_name?.find( + (indexName) => indexName === SECURITY_SOLUTION_ALERT_INDEX + ); + expect(securitySolution).to.eql(SECURITY_SOLUTION_ALERT_INDEX); // assert this here so we can use constants in the dynamically-defined test cases below + }; + + describe('Alert - Find - RBAC - spaces', () => { + before(async () => { + await getSecuritySolutionIndexName(superUser); + await getAPMIndexName(superUser); + }); + + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/rule_registry/alerts'); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/rule_registry/alerts'); + }); + + it(`${superUser.username} should reject at route level when nested aggs contains script alerts which match query in ${SPACE1}/${SECURITY_SOLUTION_ALERT_INDEX}`, async () => { + const found = await supertestWithoutAuth + .post(`${getSpaceUrlPrefix(SPACE1)}${TEST_URL}/find`) + .auth(superUser.username, superUser.password) + .set('kbn-xsrf', 'true') + .send({ + query: { match: { [ALERT_WORKFLOW_STATUS]: 'open' } }, + aggs: { + alertsByGroupingCount: { + terms: { + field: 'signal.rule.name', + order: { + _count: 'desc', + }, + size: 10000, + }, + aggs: { + test: { + terms: { + field: 'signal.rule.name', + size: 10, + script: { + source: 'SCRIPT', + }, + }, + }, + }, + }, + }, + index: SECURITY_SOLUTION_ALERT_INDEX, + }); + expect(found.statusCode).to.eql(400); + }); + + it(`${superUser.username} should allow nested aggs and return alerts which match query in ${SPACE1}/${SECURITY_SOLUTION_ALERT_INDEX}`, async () => { + const found = await supertestWithoutAuth + .post(`${getSpaceUrlPrefix(SPACE1)}${TEST_URL}/find`) + .auth(superUser.username, superUser.password) + .set('kbn-xsrf', 'true') + .send({ + query: { match: { [ALERT_WORKFLOW_STATUS]: 'open' } }, + aggs: { + alertsByGroupingCount: { + terms: { + field: 'signal.rule.name', + order: { + _count: 'desc', + }, + size: 10000, + }, + aggs: { + test: { + terms: { + field: 'signal.rule.name', + size: 10, + }, + }, + }, + }, + }, + index: SECURITY_SOLUTION_ALERT_INDEX, + }); + expect(found.statusCode).to.eql(200); + expect(found.body.hits.total.value).to.be.above(0); + }); + + function addTests({ space, authorizedUsers, unauthorizedUsers, alertId, index }: TestCase) { + authorizedUsers.forEach(({ username, password }) => { + it(`${username} should finds alerts which match query in ${space}/${index}`, async () => { + const found = await supertestWithoutAuth + .post(`${getSpaceUrlPrefix(space)}${TEST_URL}/find`) + .auth(username, password) + .set('kbn-xsrf', 'true') + .send({ + query: { match: { [ALERT_WORKFLOW_STATUS]: 'open' } }, + index, + }); + expect(found.statusCode).to.eql(200); + expect(found.body.hits.total.value).to.be.above(0); + }); + }); + + unauthorizedUsers.forEach(({ username, password }) => { + it(`${username} should NOT be able to find alert ${alertId} in ${space}/${index}`, async () => { + const res = await supertestWithoutAuth + .post(`${getSpaceUrlPrefix(space)}${TEST_URL}/find`) + .auth(username, password) + .set('kbn-xsrf', 'true') + .send({ + query: { term: { _id: alertId } }, + index, + }); + expect([403, 404, 200]).to.contain(res.statusCode); + if (res.statusCode === 200) { + expect(res.body.hits.hits.length).to.eql(0); + } + }); + }); + } + + // Alert - Update - RBAC - spaces Security Solution superuser should bulk update alerts which match query in space1/.alerts-security.alerts + // Alert - Update - RBAC - spaces superuser should bulk update alert with given id 020202 in space1/.alerts-security.alerts + describe('Security Solution', () => { + const authorizedInAllSpaces = [superUser, globalRead, secOnlySpacesAll, obsSecSpacesAll]; + const authorizedOnlyInSpace1 = [secOnly, secOnlyRead, obsSecRead, obsSec]; + const authorizedOnlyInSpace2 = [ + secOnlyReadSpace2, + obsSecReadSpace2, + secOnlySpace2, + obsSecAllSpace2, + ]; + const unauthorized = [ + // these users are not authorized to get alerts for the Security Solution in any space + obsOnly, + obsOnlyRead, + obsOnlySpace2, + obsOnlyReadSpace2, + obsOnlySpacesAll, + noKibanaPrivileges, + ]; + + addTests({ + space: SPACE1, + alertId: SECURITY_SOLUTION_ALERT_ID, + index: SECURITY_SOLUTION_ALERT_INDEX, + authorizedUsers: [...authorizedInAllSpaces, ...authorizedOnlyInSpace1], + unauthorizedUsers: [...authorizedOnlyInSpace2, ...unauthorized], + }); + addTests({ + space: SPACE2, + alertId: SECURITY_SOLUTION_ALERT_ID, + index: SECURITY_SOLUTION_ALERT_INDEX, + authorizedUsers: [...authorizedInAllSpaces, ...authorizedOnlyInSpace2], + unauthorizedUsers: [...authorizedOnlyInSpace1, ...unauthorized], + }); + }); + + describe('APM', () => { + const authorizedInAllSpaces = [superUser, globalRead, obsOnlySpacesAll, obsSecSpacesAll]; + const authorizedOnlyInSpace1 = [obsOnly, obsSec, obsOnlyRead, obsSecRead]; + const authorizedOnlyInSpace2 = [ + obsOnlySpace2, + obsSecReadSpace2, + obsOnlyReadSpace2, + obsSecAllSpace2, + ]; + const unauthorized = [ + // these users are not authorized to update alerts for APM in any space + secOnly, + secOnlyRead, + secOnlySpace2, + secOnlyReadSpace2, + secOnlySpacesAll, + noKibanaPrivileges, + ]; + + addTests({ + space: SPACE1, + alertId: APM_ALERT_ID, + index: APM_ALERT_INDEX, + authorizedUsers: [...authorizedInAllSpaces, ...authorizedOnlyInSpace1], + unauthorizedUsers: [...authorizedOnlyInSpace2, ...unauthorized], + }); + addTests({ + space: SPACE2, + alertId: APM_ALERT_ID, + index: APM_ALERT_INDEX, + authorizedUsers: [...authorizedInAllSpaces, ...authorizedOnlyInSpace2], + unauthorizedUsers: [...authorizedOnlyInSpace1, ...unauthorized], + }); + }); + }); +}; diff --git a/x-pack/test/rule_registry/security_and_spaces/tests/basic/index.ts b/x-pack/test/rule_registry/security_and_spaces/tests/basic/index.ts index 26bd5b72771d7..7069aae292267 100644 --- a/x-pack/test/rule_registry/security_and_spaces/tests/basic/index.ts +++ b/x-pack/test/rule_registry/security_and_spaces/tests/basic/index.ts @@ -26,5 +26,6 @@ export default ({ loadTestFile, getService }: FtrProviderContext): void => { loadTestFile(require.resolve('./get_alert_by_id')); loadTestFile(require.resolve('./update_alert')); loadTestFile(require.resolve('./bulk_update_alerts')); + loadTestFile(require.resolve('./find_alerts')); }); };