From 9e6a2821c9149438d9882c4e8cc3dc2a32584700 Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Wed, 1 Sep 2021 16:31:01 +0200 Subject: [PATCH] [7.x] [RAC] Remove rbac on security solution side (#110472) (#110765) * [RAC] Remove rbac on security solution side (#110472) * wip to remove rbac * Revert "[Cases] Include rule registry client for updating alert statuses (#108588)" This reverts commit 1fd7038b34084052895bb926b80c2301e4588de9. This leaves the rule registry mock changes * remove rbac on Trend/Count alert * update detection api for status * remove @kbn-alerts packages * fix leftover * Switching cases to leverage update by query for alert status * Adding missed files * fix bad logic * updating tests for use_alerts_privileges * remove index alias/fields * fix types * fix plugin to get the right index names * left over of alis on template * forget to use current user for create/read route index * updated alerts page to not show table when no privileges and updates to tests * fix bug when switching between o11y and security solution * updates tests and move to use privileges page when user tries to access alerts without proper access * updating jest tests * pairing with yara * bring back kbn-alerts after discussion with the team * fix types * fix index field for o11y * fix bug with updating index priv state * fix i18n issue and update api docs * fix refresh on alerts * fix render view on alerts * updating tests and checking for null in alerts page to not show no privileges page before load * fix details rules Co-authored-by: Jonathan Buttner Co-authored-by: Yara Tercero # Conflicts: # x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts * skip test Co-authored-by: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> --- ...-plugin-core-public.doclinksstart.links.md | 1 + ...kibana-plugin-core-public.doclinksstart.md | 2 +- .../public/doc_links/doc_links_service.ts | 2 + src/core/public/public.api.md | 1 + x-pack/plugins/cases/kibana.json | 1 - .../plugins/cases/server/client/alerts/get.ts | 14 +- .../cases/server/client/alerts/types.ts | 12 +- .../server/client/alerts/update_status.ts | 4 +- .../cases/server/client/attachments/add.ts | 34 +- .../plugins/cases/server/client/cases/push.ts | 65 ++-- .../cases/server/client/cases/update.ts | 27 +- x-pack/plugins/cases/server/client/factory.ts | 19 +- .../cases/server/client/sub_cases/update.ts | 21 +- x-pack/plugins/cases/server/client/types.ts | 3 +- .../server/common/models/commentable_case.ts | 34 +- .../connectors/servicenow/sir_format.test.ts | 159 +++------- .../connectors/servicenow/sir_format.ts | 36 +-- x-pack/plugins/cases/server/plugin.ts | 7 +- .../server/services/alerts/index.test.ts | 295 +++++++++++++++--- .../cases/server/services/alerts/index.ts | 207 +++++++----- .../cases/server/services/alerts/types.ts | 13 - .../detection_alerts/alerts_details.spec.ts | 2 +- .../security_solution/cypress/tasks/alerts.ts | 7 +- .../cypress/tasks/create_new_rule.ts | 1 - .../public/app/deep_links/index.test.ts | 64 ---- .../public/app/deep_links/index.ts | 11 +- .../cases/components/case_view/index.tsx | 2 +- .../events_viewer/events_viewer.test.tsx | 8 - .../common/components/events_viewer/index.tsx | 13 +- .../index.test.tsx | 7 - .../use_navigation_items.tsx | 12 +- .../components/user_privileges/index.tsx | 26 +- .../alerts_kpis/alerts_count_panel/index.tsx | 6 +- .../alerts_histogram_panel/index.tsx | 17 +- .../components/alerts_table/index.tsx | 4 +- .../alert_context_menu.test.tsx | 8 - .../timeline_actions/use_alerts_actions.tsx | 11 +- .../translations.tsx | 70 +++-- .../use_missing_privileges.ts | 17 +- .../take_action_dropdown/index.test.tsx | 6 +- .../components/user_info/index.test.tsx | 1 + .../detections/components/user_info/index.tsx | 56 ++-- .../alerts/use_alerts_privileges.test.tsx | 78 ++++- .../alerts/use_alerts_privileges.tsx | 25 +- .../alerts/use_signal_index.test.tsx | 7 - .../public/detections/pages/alerts/index.tsx | 15 +- .../detection_engine.test.tsx | 3 +- .../detection_engine/detection_engine.tsx | 180 ++++++----- .../rules/details/index.test.tsx | 7 - .../pages/detection_engine/translations.ts | 15 + .../public/overview/pages/overview.tsx | 5 +- .../security_solution/public/plugin.tsx | 28 +- .../components/side_panel/index.test.tsx | 8 - .../timeline/body/actions/index.test.tsx | 8 - .../body/events/event_column_view.test.tsx | 7 - .../security_solution/server/features.ts | 2 +- .../get_signals_template.test.ts.snap | 21 -- .../routes/index/create_index_route.ts | 61 ++-- .../routes/index/get_signals_template.test.ts | 13 +- .../routes/index/get_signals_template.ts | 12 +- .../routes/index/read_index_route.ts | 2 +- .../signals/open_close_signals_route.ts | 8 +- x-pack/plugins/timelines/common/constants.ts | 1 + .../common/types/timeline/actions/index.ts | 1 + .../components/t_grid/body/height_hack.ts | 10 +- .../components/t_grid/integrated/index.tsx | 19 +- .../alert_status_bulk_actions.tsx | 1 + .../timelines/public/container/index.tsx | 4 +- .../public/container/use_update_alerts.ts | 33 +- .../hooks/use_status_bulk_action_items.tsx | 3 +- .../plugins/timelines/public/mock/t_grid.tsx | 1 + x-pack/plugins/timelines/public/plugin.ts | 7 + .../search_strategy/index_fields/index.ts | 2 +- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - .../apis/security/privileges.ts | 11 +- .../security_and_spaces/tests/create_index.ts | 54 ++-- 77 files changed, 1071 insertions(+), 889 deletions(-) delete mode 100644 x-pack/plugins/cases/server/services/alerts/types.ts diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md index cadb34ae63b86..26d0c38f72fd7 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md @@ -128,6 +128,7 @@ readonly links: { readonly rollupJobs: string; readonly elasticsearch: Record; readonly siem: { + readonly privileges: string; readonly guide: string; readonly gettingStarted: string; readonly ml: string; diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md index aded69733b58b..aa3f958018041 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md @@ -17,5 +17,5 @@ export interface DocLinksStart | --- | --- | --- | | [DOC\_LINK\_VERSION](./kibana-plugin-core-public.doclinksstart.doc_link_version.md) | string | | | [ELASTIC\_WEBSITE\_URL](./kibana-plugin-core-public.doclinksstart.elastic_website_url.md) | string | | -| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly settings: string;
readonly canvas: {
readonly guide: string;
};
readonly dashboard: {
readonly guide: string;
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly discover: Record<string, string>;
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly elasticsearchModule: string;
readonly startup: string;
readonly exportedFields: string;
readonly suricataModule: string;
readonly zeekModule: string;
};
readonly auditbeat: {
readonly base: string;
readonly auditdModule: string;
readonly systemModule: string;
};
readonly metricbeat: {
readonly base: string;
readonly configure: string;
readonly httpEndpoint: string;
readonly install: string;
readonly start: string;
};
readonly enterpriseSearch: {
readonly base: string;
readonly appSearchBase: string;
readonly workplaceSearchBase: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly libbeat: {
readonly getStarted: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly composite: string;
readonly composite_missing_bucket: string;
readonly date_histogram: string;
readonly date_range: string;
readonly date_format_pattern: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly runtimeFields: {
readonly overview: string;
readonly mapping: string;
};
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessLangSpec: string;
readonly painlessSyntax: string;
readonly painlessWalkthrough: string;
readonly luceneExpressions: string;
};
readonly search: {
readonly sessions: string;
readonly sessionLimits: string;
};
readonly indexPatterns: {
readonly introduction: string;
readonly fieldFormattersNumber: string;
readonly fieldFormattersString: string;
readonly runtimeFields: string;
};
readonly addData: string;
readonly kibana: string;
readonly upgradeAssistant: string;
readonly rollupJobs: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly guide: string;
readonly gettingStarted: string;
readonly ml: string;
readonly ruleChangeLog: string;
readonly detectionsReq: string;
readonly networkMap: string;
};
readonly query: {
readonly eql: string;
readonly kueryQuerySyntax: string;
readonly luceneQuerySyntax: string;
readonly percolate: string;
readonly queryDsl: string;
readonly autocompleteChanges: string;
};
readonly date: {
readonly dateMath: string;
readonly dateMathIndexNames: string;
};
readonly management: Record<string, string>;
readonly ml: Record<string, string>;
readonly transforms: Record<string, string>;
readonly visualize: Record<string, string>;
readonly apis: Readonly<{
bulkIndexAlias: string;
byteSizeUnits: string;
createAutoFollowPattern: string;
createFollower: string;
createIndex: string;
createSnapshotLifecyclePolicy: string;
createRoleMapping: string;
createRoleMappingTemplates: string;
createRollupJobsRequest: string;
createApiKey: string;
createPipeline: string;
createTransformRequest: string;
cronExpressions: string;
executeWatchActionModes: string;
indexExists: string;
openIndex: string;
putComponentTemplate: string;
painlessExecute: string;
painlessExecuteAPIContexts: string;
putComponentTemplateMetadata: string;
putSnapshotLifecyclePolicy: string;
putIndexTemplateV1: string;
putWatch: string;
simulatePipeline: string;
timeUnits: string;
updateTransform: string;
}>;
readonly observability: Record<string, string>;
readonly alerting: Record<string, string>;
readonly maps: Record<string, string>;
readonly monitoring: Record<string, string>;
readonly security: Readonly<{
apiKeyServiceSettings: string;
clusterPrivileges: string;
elasticsearchSettings: string;
elasticsearchEnableSecurity: string;
indicesPrivileges: string;
kibanaTLS: string;
kibanaPrivileges: string;
mappingRoles: string;
mappingRolesFieldRules: string;
runAsPrivilege: string;
}>;
readonly watcher: Record<string, string>;
readonly ccs: Record<string, string>;
readonly plugins: Record<string, string>;
readonly snapshotRestore: Record<string, string>;
readonly ingest: Record<string, string>;
readonly fleet: Readonly<{
guide: string;
fleetServer: string;
fleetServerAddFleetServer: string;
settings: string;
settingsFleetServerHostSettings: string;
troubleshooting: string;
elasticAgent: string;
datastreams: string;
datastreamsNamingScheme: string;
upgradeElasticAgent: string;
upgradeElasticAgent712lower: string;
}>;
readonly ecs: {
readonly guide: string;
};
} | | +| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly settings: string;
readonly canvas: {
readonly guide: string;
};
readonly dashboard: {
readonly guide: string;
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly discover: Record<string, string>;
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly elasticsearchModule: string;
readonly startup: string;
readonly exportedFields: string;
readonly suricataModule: string;
readonly zeekModule: string;
};
readonly auditbeat: {
readonly base: string;
readonly auditdModule: string;
readonly systemModule: string;
};
readonly metricbeat: {
readonly base: string;
readonly configure: string;
readonly httpEndpoint: string;
readonly install: string;
readonly start: string;
};
readonly enterpriseSearch: {
readonly base: string;
readonly appSearchBase: string;
readonly workplaceSearchBase: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly libbeat: {
readonly getStarted: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly composite: string;
readonly composite_missing_bucket: string;
readonly date_histogram: string;
readonly date_range: string;
readonly date_format_pattern: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly runtimeFields: {
readonly overview: string;
readonly mapping: string;
};
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessLangSpec: string;
readonly painlessSyntax: string;
readonly painlessWalkthrough: string;
readonly luceneExpressions: string;
};
readonly search: {
readonly sessions: string;
readonly sessionLimits: string;
};
readonly indexPatterns: {
readonly introduction: string;
readonly fieldFormattersNumber: string;
readonly fieldFormattersString: string;
readonly runtimeFields: string;
};
readonly addData: string;
readonly kibana: string;
readonly upgradeAssistant: string;
readonly rollupJobs: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly privileges: string;
readonly guide: string;
readonly gettingStarted: string;
readonly ml: string;
readonly ruleChangeLog: string;
readonly detectionsReq: string;
readonly networkMap: string;
};
readonly query: {
readonly eql: string;
readonly kueryQuerySyntax: string;
readonly luceneQuerySyntax: string;
readonly percolate: string;
readonly queryDsl: string;
readonly autocompleteChanges: string;
};
readonly date: {
readonly dateMath: string;
readonly dateMathIndexNames: string;
};
readonly management: Record<string, string>;
readonly ml: Record<string, string>;
readonly transforms: Record<string, string>;
readonly visualize: Record<string, string>;
readonly apis: Readonly<{
bulkIndexAlias: string;
byteSizeUnits: string;
createAutoFollowPattern: string;
createFollower: string;
createIndex: string;
createSnapshotLifecyclePolicy: string;
createRoleMapping: string;
createRoleMappingTemplates: string;
createRollupJobsRequest: string;
createApiKey: string;
createPipeline: string;
createTransformRequest: string;
cronExpressions: string;
executeWatchActionModes: string;
indexExists: string;
openIndex: string;
putComponentTemplate: string;
painlessExecute: string;
painlessExecuteAPIContexts: string;
putComponentTemplateMetadata: string;
putSnapshotLifecyclePolicy: string;
putIndexTemplateV1: string;
putWatch: string;
simulatePipeline: string;
timeUnits: string;
updateTransform: string;
}>;
readonly observability: Record<string, string>;
readonly alerting: Record<string, string>;
readonly maps: Record<string, string>;
readonly monitoring: Record<string, string>;
readonly security: Readonly<{
apiKeyServiceSettings: string;
clusterPrivileges: string;
elasticsearchSettings: string;
elasticsearchEnableSecurity: string;
indicesPrivileges: string;
kibanaTLS: string;
kibanaPrivileges: string;
mappingRoles: string;
mappingRolesFieldRules: string;
runAsPrivilege: string;
}>;
readonly watcher: Record<string, string>;
readonly ccs: Record<string, string>;
readonly plugins: Record<string, string>;
readonly snapshotRestore: Record<string, string>;
readonly ingest: Record<string, string>;
readonly fleet: Readonly<{
guide: string;
fleetServer: string;
fleetServerAddFleetServer: string;
settings: string;
settingsFleetServerHostSettings: string;
troubleshooting: string;
elasticAgent: string;
datastreams: string;
datastreamsNamingScheme: string;
upgradeElasticAgent: string;
upgradeElasticAgent712lower: string;
}>;
readonly ecs: {
readonly guide: string;
};
} | | diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index a366202ceddda..50cf41e3f3db8 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -205,6 +205,7 @@ export class DocLinksService { siem: { guide: `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/index.html`, gettingStarted: `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/index.html`, + privileges: `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/sec-requirements.html`, ml: `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/machine-learning.html`, ruleChangeLog: `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/prebuilt-rules-changelog.html`, detectionsReq: `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/detections-permissions-section.html`, @@ -571,6 +572,7 @@ export interface DocLinksStart { readonly rollupJobs: string; readonly elasticsearch: Record; readonly siem: { + readonly privileges: string; readonly guide: string; readonly gettingStarted: string; readonly ml: string; diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 2217b71d2f1a3..a5b903312357d 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -593,6 +593,7 @@ export interface DocLinksStart { readonly rollupJobs: string; readonly elasticsearch: Record; readonly siem: { + readonly privileges: string; readonly guide: string; readonly gettingStarted: string; readonly ml: string; diff --git a/x-pack/plugins/cases/kibana.json b/x-pack/plugins/cases/kibana.json index 3889c559238b3..ebac6295166df 100644 --- a/x-pack/plugins/cases/kibana.json +++ b/x-pack/plugins/cases/kibana.json @@ -10,7 +10,6 @@ "id":"cases", "kibanaVersion":"kibana", "optionalPlugins":[ - "ruleRegistry", "security", "spaces" ], diff --git a/x-pack/plugins/cases/server/client/alerts/get.ts b/x-pack/plugins/cases/server/client/alerts/get.ts index 391279aab5a83..2048ccae4fa60 100644 --- a/x-pack/plugins/cases/server/client/alerts/get.ts +++ b/x-pack/plugins/cases/server/client/alerts/get.ts @@ -12,11 +12,19 @@ export const get = async ( { alertsInfo }: AlertGet, clientArgs: CasesClientArgs ): Promise => { - const { alertsService, logger } = clientArgs; + const { alertsService, scopedClusterClient, logger } = clientArgs; if (alertsInfo.length === 0) { return []; } - const alerts = await alertsService.getAlerts({ alertsInfo, logger }); - return alerts ?? []; + const alerts = await alertsService.getAlerts({ alertsInfo, scopedClusterClient, logger }); + if (!alerts) { + return []; + } + + return alerts.docs.map((alert) => ({ + id: alert._id, + index: alert._index, + ...alert._source, + })); }; diff --git a/x-pack/plugins/cases/server/client/alerts/types.ts b/x-pack/plugins/cases/server/client/alerts/types.ts index 6b3a49f20d1e5..95cd9ae33bff9 100644 --- a/x-pack/plugins/cases/server/client/alerts/types.ts +++ b/x-pack/plugins/cases/server/client/alerts/types.ts @@ -7,7 +7,17 @@ import { CaseStatuses } from '../../../common/api'; import { AlertInfo } from '../../common'; -import { Alert } from '../../services/alerts/types'; + +interface Alert { + id: string; + index: string; + destination?: { + ip: string; + }; + source?: { + ip: string; + }; +} export type CasesClientGetAlertsResponse = Alert[]; diff --git a/x-pack/plugins/cases/server/client/alerts/update_status.ts b/x-pack/plugins/cases/server/client/alerts/update_status.ts index 9c8cc33264413..a0684b59241b0 100644 --- a/x-pack/plugins/cases/server/client/alerts/update_status.ts +++ b/x-pack/plugins/cases/server/client/alerts/update_status.ts @@ -16,6 +16,6 @@ export const updateStatus = async ( { alerts }: UpdateAlertsStatusArgs, clientArgs: CasesClientArgs ): Promise => { - const { alertsService, logger } = clientArgs; - await alertsService.updateAlertsStatus({ alerts, logger }); + const { alertsService, scopedClusterClient, logger } = clientArgs; + await alertsService.updateAlertsStatus({ alerts, scopedClusterClient, logger }); }; diff --git a/x-pack/plugins/cases/server/client/attachments/add.ts b/x-pack/plugins/cases/server/client/attachments/add.ts index 5393a108d6af2..166ae2ae65012 100644 --- a/x-pack/plugins/cases/server/client/attachments/add.ts +++ b/x-pack/plugins/cases/server/client/attachments/add.ts @@ -40,7 +40,12 @@ import { } from '../../services/user_actions/helpers'; import { AttachmentService, CasesService, CaseUserActionService } from '../../services'; -import { createCaseError, CommentableCase, isCommentRequestTypeGenAlert } from '../../common'; +import { + createCaseError, + CommentableCase, + createAlertUpdateRequest, + isCommentRequestTypeGenAlert, +} from '../../common'; import { CasesClientArgs, CasesClientInternal } from '..'; import { decodeCommentRequest } from '../utils'; @@ -190,9 +195,22 @@ const addGeneratedAlerts = async ( user: userDetails, commentReq: query, id: savedObjectID, - casesClientInternal, }); + if ( + (newComment.attributes.type === CommentType.alert || + newComment.attributes.type === CommentType.generatedAlert) && + caseInfo.attributes.settings.syncAlerts + ) { + const alertsToUpdate = createAlertUpdateRequest({ + comment: query, + status: subCase.attributes.status, + }); + await casesClientInternal.alerts.updateStatus({ + alerts: alertsToUpdate, + }); + } + await userActionService.bulkCreate({ unsecuredSavedObjectsClient, actions: [ @@ -368,9 +386,19 @@ export const addComment = async ( user: userInfo, commentReq: query, id: savedObjectID, - casesClientInternal, }); + if (newComment.attributes.type === CommentType.alert && updatedCase.settings.syncAlerts) { + const alertsToUpdate = createAlertUpdateRequest({ + comment: query, + status: updatedCase.status, + }); + + await casesClientInternal.alerts.updateStatus({ + alerts: alertsToUpdate, + }); + } + await userActionService.bulkCreate({ unsecuredSavedObjectsClient, actions: [ diff --git a/x-pack/plugins/cases/server/client/cases/push.ts b/x-pack/plugins/cases/server/client/cases/push.ts index 80e69d53e9e8b..3048cf01bb3ba 100644 --- a/x-pack/plugins/cases/server/client/cases/push.ts +++ b/x-pack/plugins/cases/server/client/cases/push.ts @@ -6,7 +6,7 @@ */ import Boom from '@hapi/boom'; -import { SavedObjectsFindResponse, SavedObject, Logger } from 'kibana/server'; +import { SavedObjectsFindResponse, SavedObject } from 'kibana/server'; import { ActionConnector, @@ -22,16 +22,26 @@ import { import { buildCaseUserActionItem } from '../../services/user_actions/helpers'; import { createIncident, getCommentContextFromAttributes } from './utils'; -import { - AlertInfo, - createCaseError, - flattenCaseSavedObject, - getAlertInfoFromComments, -} from '../../common'; +import { createCaseError, flattenCaseSavedObject, getAlertInfoFromComments } from '../../common'; import { CasesClient, CasesClientArgs, CasesClientInternal } from '..'; import { Operations } from '../../authorization'; import { casesConnectors } from '../../connectors'; -import { CasesClientGetAlertsResponse } from '../alerts/types'; + +/** + * Returns true if the case should be closed based on the configuration settings and whether the case + * is a collection. Collections are not closable because we aren't allowing their status to be changed. + * In the future we could allow push to close all the sub cases of a collection but that's not currently supported. + */ +function shouldCloseByPush( + configureSettings: SavedObjectsFindResponse, + caseInfo: SavedObject +): boolean { + return ( + configureSettings.total > 0 && + configureSettings.saved_objects[0].attributes.closure_type === 'close-by-pushing' && + caseInfo.attributes.type !== CaseType.collection + ); +} /** * Parameters for pushing a case to an external system @@ -96,7 +106,9 @@ export const push = async ( const alertsInfo = getAlertInfoFromComments(theCase?.comments); - const alerts = await getAlertsCatchErrors({ casesClientInternal, alertsInfo, logger }); + const alerts = await casesClientInternal.alerts.get({ + alertsInfo, + }); const getMappingsResponse = await casesClientInternal.configuration.getMappings({ connector: theCase.connector, @@ -266,38 +278,3 @@ export const push = async ( throw createCaseError({ message: `Failed to push case: ${error}`, error, logger }); } }; - -async function getAlertsCatchErrors({ - casesClientInternal, - alertsInfo, - logger, -}: { - casesClientInternal: CasesClientInternal; - alertsInfo: AlertInfo[]; - logger: Logger; -}): Promise { - try { - return await casesClientInternal.alerts.get({ - alertsInfo, - }); - } catch (error) { - logger.error(`Failed to retrieve alerts during push: ${error}`); - return []; - } -} - -/** - * Returns true if the case should be closed based on the configuration settings and whether the case - * is a collection. Collections are not closable because we aren't allowing their status to be changed. - * In the future we could allow push to close all the sub cases of a collection but that's not currently supported. - */ -function shouldCloseByPush( - configureSettings: SavedObjectsFindResponse, - caseInfo: SavedObject -): boolean { - return ( - configureSettings.total > 0 && - configureSettings.saved_objects[0].attributes.closure_type === 'close-by-pushing' && - caseInfo.attributes.type !== CaseType.collection - ); -} diff --git a/x-pack/plugins/cases/server/client/cases/update.ts b/x-pack/plugins/cases/server/client/cases/update.ts index 611c9e09fa76e..ed19444414d57 100644 --- a/x-pack/plugins/cases/server/client/cases/update.ts +++ b/x-pack/plugins/cases/server/client/cases/update.ts @@ -12,7 +12,6 @@ import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; import { - Logger, SavedObject, SavedObjectsClientContract, SavedObjectsFindResponse, @@ -308,14 +307,12 @@ async function updateAlerts({ caseService, unsecuredSavedObjectsClient, casesClientInternal, - logger, }: { casesWithSyncSettingChangedToOn: UpdateRequestWithOriginalCase[]; casesWithStatusChangedAndSynced: UpdateRequestWithOriginalCase[]; caseService: CasesService; unsecuredSavedObjectsClient: SavedObjectsClientContract; casesClientInternal: CasesClientInternal; - logger: Logger; }) { /** * It's possible that a case ID can appear multiple times in each array. I'm intentionally placing the status changes @@ -364,9 +361,7 @@ async function updateAlerts({ [] ); - await casesClientInternal.alerts.updateStatus({ - alerts: alertsToUpdate, - }); + await casesClientInternal.alerts.updateStatus({ alerts: alertsToUpdate }); } function partitionPatchRequest( @@ -567,6 +562,15 @@ export const update = async ( ); }); + // Update the alert's status to match any case status or sync settings changes + await updateAlerts({ + casesWithStatusChangedAndSynced, + casesWithSyncSettingChangedToOn, + caseService, + unsecuredSavedObjectsClient, + casesClientInternal, + }); + const returnUpdatedCase = myCases.saved_objects .filter((myCase) => updatedCases.saved_objects.some((updatedCase) => updatedCase.id === myCase.id) @@ -594,17 +598,6 @@ export const update = async ( }), }); - // Update the alert's status to match any case status or sync settings changes - // Attempt to do this after creating/changing the other entities just in case it fails - await updateAlerts({ - casesWithStatusChangedAndSynced, - casesWithSyncSettingChangedToOn, - caseService, - unsecuredSavedObjectsClient, - casesClientInternal, - logger, - }); - return CasesResponseRt.encode(returnUpdatedCase); } catch (error) { const idVersions = cases.cases.map((caseInfo) => ({ diff --git a/x-pack/plugins/cases/server/client/factory.ts b/x-pack/plugins/cases/server/client/factory.ts index a1a3ccdd3bc52..2fae6996f4aa2 100644 --- a/x-pack/plugins/cases/server/client/factory.ts +++ b/x-pack/plugins/cases/server/client/factory.ts @@ -5,7 +5,12 @@ * 2.0. */ -import { KibanaRequest, SavedObjectsServiceStart, Logger } from 'kibana/server'; +import { + KibanaRequest, + SavedObjectsServiceStart, + Logger, + ElasticsearchClient, +} from 'kibana/server'; import { SecurityPluginSetup, SecurityPluginStart } from '../../../security/server'; import { SAVED_OBJECT_TYPES } from '../../common'; import { Authorization } from '../authorization/authorization'; @@ -20,8 +25,8 @@ import { } from '../services'; import { PluginStartContract as FeaturesPluginStart } from '../../../features/server'; import { PluginStartContract as ActionsPluginStart } from '../../../actions/server'; -import { RuleRegistryPluginStartContract } from '../../../rule_registry/server'; import { LensServerPluginSetup } from '../../../lens/server'; + import { AuthorizationAuditLogger } from '../authorization'; import { CasesClient, createCasesClient } from '.'; @@ -31,7 +36,6 @@ interface CasesClientFactoryArgs { getSpace: GetSpaceFn; featuresPluginStart: FeaturesPluginStart; actionsPluginStart: ActionsPluginStart; - ruleRegistryPluginStart?: RuleRegistryPluginStartContract; lensEmbeddableFactory: LensServerPluginSetup['lensEmbeddableFactory']; } @@ -65,10 +69,12 @@ export class CasesClientFactory { */ public async create({ request, + scopedClusterClient, savedObjectsService, }: { request: KibanaRequest; savedObjectsService: SavedObjectsServiceStart; + scopedClusterClient: ElasticsearchClient; }): Promise { if (!this.isInitialized || !this.options) { throw new Error('CasesClientFactory must be initialized before calling create'); @@ -88,12 +94,9 @@ export class CasesClientFactory { const caseService = new CasesService(this.logger, this.options?.securityPluginStart?.authc); const userInfo = caseService.getUser({ request }); - const alertsClient = await this.options.ruleRegistryPluginStart?.getRacClientWithRequest( - request - ); - return createCasesClient({ - alertsService: new AlertService(alertsClient), + alertsService: new AlertService(), + scopedClusterClient, unsecuredSavedObjectsClient: savedObjectsService.getScopedClient(request, { includedHiddenTypes: SAVED_OBJECT_TYPES, // this tells the security plugin to not perform SO authorization and audit logging since we are handling diff --git a/x-pack/plugins/cases/server/client/sub_cases/update.ts b/x-pack/plugins/cases/server/client/sub_cases/update.ts index 56610ea6858e3..c8cb96cbb6b8c 100644 --- a/x-pack/plugins/cases/server/client/sub_cases/update.ts +++ b/x-pack/plugins/cases/server/client/sub_cases/update.ts @@ -246,9 +246,7 @@ async function updateAlerts({ [] ); - await casesClientInternal.alerts.updateStatus({ - alerts: alertsToUpdate, - }); + await casesClientInternal.alerts.updateStatus({ alerts: alertsToUpdate }); } catch (error) { throw createCaseError({ message: `Failed to update alert status while updating sub cases: ${JSON.stringify( @@ -357,6 +355,14 @@ export async function update({ ); }); + await updateAlerts({ + caseService, + unsecuredSavedObjectsClient, + casesClientInternal, + subCasesToSync: subCasesToSyncAlertsFor, + logger: clientArgs.logger, + }); + const returnUpdatedSubCases = updatedCases.saved_objects.reduce( (acc, updatedSO) => { const originalSubCase = subCasesMap.get(updatedSO.id); @@ -388,15 +394,6 @@ export async function update({ }), }); - // attempt to update the status of the alerts after creating all the user actions just in case it fails - await updateAlerts({ - caseService, - unsecuredSavedObjectsClient, - casesClientInternal, - subCasesToSync: subCasesToSyncAlertsFor, - logger: clientArgs.logger, - }); - return SubCasesResponseRt.encode(returnUpdatedSubCases); } catch (error) { const idVersions = query.subCases.map((subCase) => ({ diff --git a/x-pack/plugins/cases/server/client/types.ts b/x-pack/plugins/cases/server/client/types.ts index 3979c19949d9a..27829d2539c7d 100644 --- a/x-pack/plugins/cases/server/client/types.ts +++ b/x-pack/plugins/cases/server/client/types.ts @@ -6,7 +6,7 @@ */ import type { PublicMethodsOf } from '@kbn/utility-types'; -import { SavedObjectsClientContract, Logger } from 'kibana/server'; +import { ElasticsearchClient, SavedObjectsClientContract, Logger } from 'kibana/server'; import { User } from '../../common'; import { Authorization } from '../authorization/authorization'; import { @@ -24,6 +24,7 @@ import { LensServerPluginSetup } from '../../../lens/server'; * Parameters for initializing a cases client */ export interface CasesClientArgs { + readonly scopedClusterClient: ElasticsearchClient; readonly caseConfigureService: CaseConfigureService; readonly caseService: CasesService; readonly connectorMappingsService: ConnectorMappingsService; diff --git a/x-pack/plugins/cases/server/common/models/commentable_case.ts b/x-pack/plugins/cases/server/common/models/commentable_case.ts index e540332b1ff84..856d6378d5900 100644 --- a/x-pack/plugins/cases/server/common/models/commentable_case.ts +++ b/x-pack/plugins/cases/server/common/models/commentable_case.ts @@ -34,16 +34,10 @@ import { CommentRequestUserType, CaseAttributes, } from '../../../common'; -import { - createAlertUpdateRequest, - flattenCommentSavedObjects, - flattenSubCaseSavedObject, - transformNewComment, -} from '..'; +import { flattenCommentSavedObjects, flattenSubCaseSavedObject, transformNewComment } from '..'; import { AttachmentService, CasesService } from '../../services'; import { createCaseError } from '../error'; import { countAlertsForID } from '../index'; -import { CasesClientInternal } from '../../client'; import { getOrUpdateLensReferences } from '../utils'; interface UpdateCommentResp { @@ -279,13 +273,11 @@ export class CommentableCase { user, commentReq, id, - casesClientInternal, }: { createdDate: string; user: User; commentReq: CommentRequest; id: string; - casesClientInternal: CasesClientInternal; }): Promise { try { if (commentReq.type === CommentType.alert) { @@ -302,10 +294,6 @@ export class CommentableCase { throw Boom.badRequest('The owner field of the comment must match the case'); } - // Let's try to sync the alert's status before creating the attachment, that way if the alert doesn't exist - // we'll throw an error early before creating the attachment - await this.syncAlertStatus(commentReq, casesClientInternal); - let references = this.buildRefsToCase(); if (commentReq.type === CommentType.user && commentReq?.comment) { @@ -343,26 +331,6 @@ export class CommentableCase { } } - private async syncAlertStatus( - commentRequest: CommentRequest, - casesClientInternal: CasesClientInternal - ) { - if ( - (commentRequest.type === CommentType.alert || - commentRequest.type === CommentType.generatedAlert) && - this.settings.syncAlerts - ) { - const alertsToUpdate = createAlertUpdateRequest({ - comment: commentRequest, - status: this.status, - }); - - await casesClientInternal.alerts.updateStatus({ - alerts: alertsToUpdate, - }); - } - } - private formatCollectionForEncoding(totalComment: number) { return { id: this.collection.id, diff --git a/x-pack/plugins/cases/server/connectors/servicenow/sir_format.test.ts b/x-pack/plugins/cases/server/connectors/servicenow/sir_format.test.ts index 7a1efe8b366d0..fa103d4c1142d 100644 --- a/x-pack/plugins/cases/server/connectors/servicenow/sir_format.test.ts +++ b/x-pack/plugins/cases/server/connectors/servicenow/sir_format.test.ts @@ -24,7 +24,7 @@ describe('ITSM formatter', () => { } as CaseResponse; it('it formats correctly without alerts', async () => { - const res = format(theCase, []); + const res = await format(theCase, []); expect(res).toEqual({ dest_ip: null, source_ip: null, @@ -38,7 +38,7 @@ describe('ITSM formatter', () => { it('it formats correctly when fields do not exist ', async () => { const invalidFields = { connector: { fields: null } } as CaseResponse; - const res = format(invalidFields, []); + const res = await format(invalidFields, []); expect(res).toEqual({ dest_ip: null, source_ip: null, @@ -55,31 +55,25 @@ describe('ITSM formatter', () => { { id: 'alert-1', index: 'index-1', - source: { - destination: { ip: '192.168.1.1' }, - source: { ip: '192.168.1.2' }, - file: { - hash: { sha256: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08' }, - }, - url: { full: 'https://attack.com' }, + destination: { ip: '192.168.1.1' }, + source: { ip: '192.168.1.2' }, + file: { + hash: { sha256: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08' }, }, + url: { full: 'https://attack.com' }, }, { id: 'alert-2', index: 'index-2', - source: { - source: { - ip: '192.168.1.3', - }, - destination: { ip: '192.168.1.4' }, - file: { - hash: { sha256: '60303ae22b998861bce3b28f33eec1be758a213c86c93c076dbe9f558c11c752' }, - }, - url: { full: 'https://attack.com/api' }, + destination: { ip: '192.168.1.4' }, + source: { ip: '192.168.1.3' }, + file: { + hash: { sha256: '60303ae22b998861bce3b28f33eec1be758a213c86c93c076dbe9f558c11c752' }, }, + url: { full: 'https://attack.com/api' }, }, ]; - const res = format(theCase, alerts); + const res = await format(theCase, alerts); expect(res).toEqual({ dest_ip: '192.168.1.1,192.168.1.4', source_ip: '192.168.1.2,192.168.1.3', @@ -92,109 +86,30 @@ describe('ITSM formatter', () => { }); }); - it('it ignores alerts with an error', async () => { - const alerts = [ - { - id: 'alert-1', - index: 'index-1', - error: new Error('an error'), - source: { - destination: { ip: '192.168.1.1' }, - source: { ip: '192.168.1.2' }, - file: { - hash: { sha256: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08' }, - }, - url: { full: 'https://attack.com' }, - }, - }, - { - id: 'alert-2', - index: 'index-2', - source: { - source: { - ip: '192.168.1.3', - }, - destination: { ip: '192.168.1.4' }, - file: { - hash: { sha256: '60303ae22b998861bce3b28f33eec1be758a213c86c93c076dbe9f558c11c752' }, - }, - url: { full: 'https://attack.com/api' }, - }, - }, - ]; - const res = format(theCase, alerts); - expect(res).toEqual({ - dest_ip: '192.168.1.4', - source_ip: '192.168.1.3', - category: 'Denial of Service', - subcategory: 'Inbound DDos', - malware_hash: '60303ae22b998861bce3b28f33eec1be758a213c86c93c076dbe9f558c11c752', - malware_url: 'https://attack.com/api', - priority: '2 - High', - }); - }); - - it('it ignores alerts without a source field', async () => { - const alerts = [ - { - id: 'alert-1', - index: 'index-1', - }, - { - id: 'alert-2', - index: 'index-2', - source: { - source: { - ip: '192.168.1.3', - }, - destination: { ip: '192.168.1.4' }, - file: { - hash: { sha256: '60303ae22b998861bce3b28f33eec1be758a213c86c93c076dbe9f558c11c752' }, - }, - url: { full: 'https://attack.com/api' }, - }, - }, - ]; - const res = format(theCase, alerts); - expect(res).toEqual({ - dest_ip: '192.168.1.4', - source_ip: '192.168.1.3', - category: 'Denial of Service', - subcategory: 'Inbound DDos', - malware_hash: '60303ae22b998861bce3b28f33eec1be758a213c86c93c076dbe9f558c11c752', - malware_url: 'https://attack.com/api', - priority: '2 - High', - }); - }); - it('it handles duplicates correctly', async () => { const alerts = [ { id: 'alert-1', index: 'index-1', - source: { - destination: { ip: '192.168.1.1' }, - source: { ip: '192.168.1.2' }, - file: { - hash: { sha256: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08' }, - }, - url: { full: 'https://attack.com' }, + destination: { ip: '192.168.1.1' }, + source: { ip: '192.168.1.2' }, + file: { + hash: { sha256: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08' }, }, + url: { full: 'https://attack.com' }, }, { id: 'alert-2', index: 'index-2', - source: { - destination: { ip: '192.168.1.1' }, - source: { ip: '192.168.1.3' }, - file: { - hash: { sha256: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08' }, - }, - url: { full: 'https://attack.com/api' }, + destination: { ip: '192.168.1.1' }, + source: { ip: '192.168.1.3' }, + file: { + hash: { sha256: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08' }, }, + url: { full: 'https://attack.com/api' }, }, ]; - const res = format(theCase, alerts); + const res = await format(theCase, alerts); expect(res).toEqual({ dest_ip: '192.168.1.1', source_ip: '192.168.1.2,192.168.1.3', @@ -211,26 +126,22 @@ describe('ITSM formatter', () => { { id: 'alert-1', index: 'index-1', - source: { - destination: { ip: '192.168.1.1' }, - source: { ip: '192.168.1.2' }, - file: { - hash: { sha256: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08' }, - }, - url: { full: 'https://attack.com' }, + destination: { ip: '192.168.1.1' }, + source: { ip: '192.168.1.2' }, + file: { + hash: { sha256: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08' }, }, + url: { full: 'https://attack.com' }, }, { id: 'alert-2', index: 'index-2', - source: { - destination: { ip: '192.168.1.1' }, - source: { ip: '192.168.1.3' }, - file: { - hash: { sha256: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08' }, - }, - url: { full: 'https://attack.com/api' }, + destination: { ip: '192.168.1.1' }, + source: { ip: '192.168.1.3' }, + file: { + hash: { sha256: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08' }, }, + url: { full: 'https://attack.com/api' }, }, ]; @@ -239,7 +150,7 @@ describe('ITSM formatter', () => { connector: { fields: { ...theCase.connector.fields, destIp: false, malwareHash: false } }, } as CaseResponse; - const res = format(newCase, alerts); + const res = await format(newCase, alerts); expect(res).toEqual({ dest_ip: null, source_ip: '192.168.1.2,192.168.1.3', diff --git a/x-pack/plugins/cases/server/connectors/servicenow/sir_format.ts b/x-pack/plugins/cases/server/connectors/servicenow/sir_format.ts index 88b8f79d3ba5b..b48a1b7f734c8 100644 --- a/x-pack/plugins/cases/server/connectors/servicenow/sir_format.ts +++ b/x-pack/plugins/cases/server/connectors/servicenow/sir_format.ts @@ -44,25 +44,23 @@ export const format: ServiceNowSIRFormat = (theCase, alerts) => { ); if (fieldsToAdd.length > 0) { - sirFields = alerts - .filter((alert) => !alert.error && alert.source != null) - .reduce>((acc, alert) => { - fieldsToAdd.forEach((alertField) => { - const field = get(alertFieldMapping[alertField].alertPath, alert.source); - if (field && !manageDuplicate[alertFieldMapping[alertField].sirFieldKey].has(field)) { - manageDuplicate[alertFieldMapping[alertField].sirFieldKey].add(field); - acc = { - ...acc, - [alertFieldMapping[alertField].sirFieldKey]: `${ - acc[alertFieldMapping[alertField].sirFieldKey] != null - ? `${acc[alertFieldMapping[alertField].sirFieldKey]},${field}` - : field - }`, - }; - } - }); - return acc; - }, sirFields); + sirFields = alerts.reduce>((acc, alert) => { + fieldsToAdd.forEach((alertField) => { + const field = get(alertFieldMapping[alertField].alertPath, alert); + if (field && !manageDuplicate[alertFieldMapping[alertField].sirFieldKey].has(field)) { + manageDuplicate[alertFieldMapping[alertField].sirFieldKey].add(field); + acc = { + ...acc, + [alertFieldMapping[alertField].sirFieldKey]: `${ + acc[alertFieldMapping[alertField].sirFieldKey] != null + ? `${acc[alertFieldMapping[alertField].sirFieldKey]},${field}` + : field + }`, + }; + } + }); + return acc; + }, sirFields); } return { diff --git a/x-pack/plugins/cases/server/plugin.ts b/x-pack/plugins/cases/server/plugin.ts index 49220fc716034..bb1be163585a8 100644 --- a/x-pack/plugins/cases/server/plugin.ts +++ b/x-pack/plugins/cases/server/plugin.ts @@ -32,7 +32,6 @@ import type { CasesRequestHandlerContext } from './types'; import { CasesClientFactory } from './client/factory'; import { SpacesPluginStart } from '../../spaces/server'; import { PluginStartContract as FeaturesPluginStart } from '../../features/server'; -import { RuleRegistryPluginStartContract } from '../../rule_registry/server'; import { LensServerPluginSetup } from '../../lens/server'; function createConfig(context: PluginInitializerContext) { @@ -50,7 +49,6 @@ export interface PluginsStart { features: FeaturesPluginStart; spaces?: SpacesPluginStart; actions: ActionsPluginStart; - ruleRegistry?: RuleRegistryPluginStartContract; } /** @@ -139,13 +137,15 @@ export class CasePlugin { }, featuresPluginStart: plugins.features, actionsPluginStart: plugins.actions, - ruleRegistryPluginStart: plugins.ruleRegistry, lensEmbeddableFactory: this.lensEmbeddableFactory!, }); + const client = core.elasticsearch.client; + const getCasesClientWithRequest = async (request: KibanaRequest): Promise => { return this.clientFactory.create({ request, + scopedClusterClient: client.asScoped(request).asCurrentUser, savedObjectsService: core.savedObjects, }); }; @@ -171,6 +171,7 @@ export class CasePlugin { return this.clientFactory.create({ request, + scopedClusterClient: context.core.elasticsearch.client.asCurrentUser, savedObjectsService: savedObjects, }); }, diff --git a/x-pack/plugins/cases/server/services/alerts/index.test.ts b/x-pack/plugins/cases/server/services/alerts/index.test.ts index 0e1ad03a32af2..d7dd44b33628b 100644 --- a/x-pack/plugins/cases/server/services/alerts/index.test.ts +++ b/x-pack/plugins/cases/server/services/alerts/index.test.ts @@ -7,73 +7,280 @@ import { CaseStatuses } from '../../../common'; import { AlertService, AlertServiceContract } from '.'; -import { loggingSystemMock } from 'src/core/server/mocks'; -import { ruleRegistryMocks } from '../../../../rule_registry/server/mocks'; -import { AlertsClient } from '../../../../rule_registry/server'; -import { PublicMethodsOf } from '@kbn/utility-types'; +import { elasticsearchServiceMock, loggingSystemMock } from 'src/core/server/mocks'; +import { ALERT_WORKFLOW_STATUS } from '../../../../rule_registry/common/technical_rule_data_field_names'; describe('updateAlertsStatus', () => { + const esClient = elasticsearchServiceMock.createElasticsearchClient(); const logger = loggingSystemMock.create().get('case'); - let alertsClient: jest.Mocked>; - let alertService: AlertServiceContract; - - beforeEach(async () => { - alertsClient = ruleRegistryMocks.createAlertsClientMock.create(); - alertService = new AlertService(alertsClient); - jest.restoreAllMocks(); - }); describe('happy path', () => { - const args = { - alerts: [{ id: 'alert-id-1', index: '.siem-signals', status: CaseStatuses.closed }], - logger, - }; + let alertService: AlertServiceContract; + + beforeEach(async () => { + alertService = new AlertService(); + jest.resetAllMocks(); + }); it('updates the status of the alert correctly', async () => { + const args = { + alerts: [{ id: 'alert-id-1', index: '.siem-signals', status: CaseStatuses.closed }], + scopedClusterClient: esClient, + logger, + }; + await alertService.updateAlertsStatus(args); - expect(alertsClient.update).toHaveBeenCalledWith({ - id: 'alert-id-1', + expect(esClient.updateByQuery).toHaveBeenCalledWith({ index: '.siem-signals', - status: CaseStatuses.closed, + conflicts: 'abort', + body: { + script: { + source: `if (ctx._source['${ALERT_WORKFLOW_STATUS}'] != null) { + ctx._source['${ALERT_WORKFLOW_STATUS}'] = 'closed' + } + if (ctx._source.signal != null && ctx._source.signal.status != null) { + ctx._source.signal.status = 'closed' + }`, + lang: 'painless', + }, + query: { + ids: { + values: ['alert-id-1'], + }, + }, + }, + ignore_unavailable: true, }); }); - it('translates the in-progress status to acknowledged', async () => { - await alertService.updateAlertsStatus({ - alerts: [{ id: 'alert-id-1', index: '.siem-signals', status: CaseStatuses['in-progress'] }], + it('buckets the alerts by index', async () => { + const args = { + alerts: [ + { id: 'id1', index: '1', status: CaseStatuses.closed }, + { id: 'id2', index: '1', status: CaseStatuses.closed }, + ], + scopedClusterClient: esClient, logger, - }); + }; - expect(alertsClient.update).toHaveBeenCalledWith({ - id: 'alert-id-1', - index: '.siem-signals', - status: 'acknowledged', + await alertService.updateAlertsStatus(args); + + expect(esClient.updateByQuery).toBeCalledTimes(1); + expect(esClient.updateByQuery).toHaveBeenCalledWith({ + index: '1', + conflicts: 'abort', + body: { + script: { + source: `if (ctx._source['${ALERT_WORKFLOW_STATUS}'] != null) { + ctx._source['${ALERT_WORKFLOW_STATUS}'] = 'closed' + } + if (ctx._source.signal != null && ctx._source.signal.status != null) { + ctx._source.signal.status = 'closed' + }`, + lang: 'painless', + }, + query: { + ids: { + values: ['id1', 'id2'], + }, + }, + }, + ignore_unavailable: true, }); }); - it('defaults an unknown status to open', async () => { - await alertService.updateAlertsStatus({ - alerts: [{ id: 'alert-id-1', index: '.siem-signals', status: 'bananas' as CaseStatuses }], + it('translates in-progress to acknowledged', async () => { + const args = { + alerts: [{ id: 'id1', index: '1', status: CaseStatuses['in-progress'] }], + scopedClusterClient: esClient, logger, - }); + }; - expect(alertsClient.update).toHaveBeenCalledWith({ - id: 'alert-id-1', - index: '.siem-signals', - status: 'open', - }); + await alertService.updateAlertsStatus(args); + + expect(esClient.updateByQuery).toBeCalledTimes(1); + expect(esClient.updateByQuery.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "body": Object { + "query": Object { + "ids": Object { + "values": Array [ + "id1", + ], + }, + }, + "script": Object { + "lang": "painless", + "source": "if (ctx._source['kibana.alert.workflow_status'] != null) { + ctx._source['kibana.alert.workflow_status'] = 'acknowledged' + } + if (ctx._source.signal != null && ctx._source.signal.status != null) { + ctx._source.signal.status = 'acknowledged' + }", + }, + }, + "conflicts": "abort", + "ignore_unavailable": true, + "index": "1", + }, + ] + `); + }); + + it('makes two calls when the statuses are different', async () => { + const args = { + alerts: [ + { id: 'id1', index: '1', status: CaseStatuses.closed }, + { id: 'id2', index: '1', status: CaseStatuses.open }, + ], + scopedClusterClient: esClient, + logger, + }; + + await alertService.updateAlertsStatus(args); + + expect(esClient.updateByQuery).toBeCalledTimes(2); + // id1 should be closed + expect(esClient.updateByQuery.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "body": Object { + "query": Object { + "ids": Object { + "values": Array [ + "id1", + ], + }, + }, + "script": Object { + "lang": "painless", + "source": "if (ctx._source['kibana.alert.workflow_status'] != null) { + ctx._source['kibana.alert.workflow_status'] = 'closed' + } + if (ctx._source.signal != null && ctx._source.signal.status != null) { + ctx._source.signal.status = 'closed' + }", + }, + }, + "conflicts": "abort", + "ignore_unavailable": true, + "index": "1", + }, + ] + `); + + // id2 should be open + expect(esClient.updateByQuery.mock.calls[1]).toMatchInlineSnapshot(` + Array [ + Object { + "body": Object { + "query": Object { + "ids": Object { + "values": Array [ + "id2", + ], + }, + }, + "script": Object { + "lang": "painless", + "source": "if (ctx._source['kibana.alert.workflow_status'] != null) { + ctx._source['kibana.alert.workflow_status'] = 'open' + } + if (ctx._source.signal != null && ctx._source.signal.status != null) { + ctx._source.signal.status = 'open' + }", + }, + }, + "conflicts": "abort", + "ignore_unavailable": true, + "index": "1", + }, + ] + `); + }); + + it('makes two calls when the indices are different', async () => { + const args = { + alerts: [ + { id: 'id1', index: '1', status: CaseStatuses.closed }, + { id: 'id2', index: '2', status: CaseStatuses.open }, + ], + scopedClusterClient: esClient, + logger, + }; + + await alertService.updateAlertsStatus(args); + + expect(esClient.updateByQuery).toBeCalledTimes(2); + // id1 should be closed in index 1 + expect(esClient.updateByQuery.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "body": Object { + "query": Object { + "ids": Object { + "values": Array [ + "id1", + ], + }, + }, + "script": Object { + "lang": "painless", + "source": "if (ctx._source['kibana.alert.workflow_status'] != null) { + ctx._source['kibana.alert.workflow_status'] = 'closed' + } + if (ctx._source.signal != null && ctx._source.signal.status != null) { + ctx._source.signal.status = 'closed' + }", + }, + }, + "conflicts": "abort", + "ignore_unavailable": true, + "index": "1", + }, + ] + `); + + // id2 should be open in index 2 + expect(esClient.updateByQuery.mock.calls[1]).toMatchInlineSnapshot(` + Array [ + Object { + "body": Object { + "query": Object { + "ids": Object { + "values": Array [ + "id2", + ], + }, + }, + "script": Object { + "lang": "painless", + "source": "if (ctx._source['kibana.alert.workflow_status'] != null) { + ctx._source['kibana.alert.workflow_status'] = 'open' + } + if (ctx._source.signal != null && ctx._source.signal.status != null) { + ctx._source.signal.status = 'open' + }", + }, + }, + "conflicts": "abort", + "ignore_unavailable": true, + "index": "2", + }, + ] + `); }); - }); - describe('unhappy path', () => { it('ignores empty indices', async () => { - expect( - await alertService.updateAlertsStatus({ - alerts: [{ id: 'alert-id-1', index: '', status: CaseStatuses.closed }], - logger, - }) - ).toBeUndefined(); + await alertService.updateAlertsStatus({ + alerts: [{ id: 'alert-id-1', index: '', status: CaseStatuses.open }], + scopedClusterClient: esClient, + logger, + }); + + expect(esClient.updateByQuery).not.toHaveBeenCalled(); }); }); }); diff --git a/x-pack/plugins/cases/server/services/alerts/index.ts b/x-pack/plugins/cases/server/services/alerts/index.ts index ccb0fca4f995f..6bb2fb3ee3c56 100644 --- a/x-pack/plugins/cases/server/services/alerts/index.ts +++ b/x-pack/plugins/cases/server/services/alerts/index.ts @@ -5,71 +5,62 @@ * 2.0. */ +import pMap from 'p-map'; import { isEmpty } from 'lodash'; import type { PublicMethodsOf } from '@kbn/utility-types'; -import { Logger } from 'kibana/server'; -import { CaseStatuses, MAX_ALERTS_PER_SUB_CASE } from '../../../common'; +import { ElasticsearchClient, Logger } from 'kibana/server'; +import { CaseStatuses, MAX_ALERTS_PER_SUB_CASE, MAX_CONCURRENT_SEARCHES } from '../../../common'; import { AlertInfo, createCaseError } from '../../common'; import { UpdateAlertRequest } from '../../client/alerts/types'; -import { AlertsClient } from '../../../../rule_registry/server'; -import { Alert } from './types'; -import { STATUS_VALUES } from '../../../../rule_registry/common/technical_rule_data_field_names'; +import { + ALERT_WORKFLOW_STATUS, + STATUS_VALUES, +} from '../../../../rule_registry/common/technical_rule_data_field_names'; export type AlertServiceContract = PublicMethodsOf; interface UpdateAlertsStatusArgs { alerts: UpdateAlertRequest[]; + scopedClusterClient: ElasticsearchClient; logger: Logger; } interface GetAlertsArgs { alertsInfo: AlertInfo[]; + scopedClusterClient: ElasticsearchClient; logger: Logger; } +interface Alert { + _id: string; + _index: string; + _source: Record; +} + +interface AlertsResponse { + docs: Alert[]; +} + function isEmptyAlert(alert: AlertInfo): boolean { return isEmpty(alert.id) || isEmpty(alert.index); } export class AlertService { - constructor(private readonly alertsClient?: PublicMethodsOf) {} + constructor() {} - public async updateAlertsStatus({ alerts, logger }: UpdateAlertsStatusArgs) { + public async updateAlertsStatus({ alerts, scopedClusterClient, logger }: UpdateAlertsStatusArgs) { try { - if (!this.alertsClient) { - throw new Error( - 'Alert client is undefined, the rule registry plugin must be enabled to updated the status of alerts' - ); - } - - const alertsToUpdate = alerts.filter((alert) => !isEmptyAlert(alert)); - - if (alertsToUpdate.length <= 0) { - return; - } - - const updatedAlerts = await Promise.allSettled( - alertsToUpdate.map((alert) => - this.alertsClient?.update({ - id: alert.id, - index: alert.index, - status: translateStatus({ alert, logger }), - _version: undefined, - }) - ) + const bucketedAlerts = bucketAlertsByIndexAndStatus(alerts, logger); + const indexBuckets = Array.from(bucketedAlerts.entries()); + + await pMap( + indexBuckets, + async (indexBucket: [string, Map]) => + updateByQuery(indexBucket, scopedClusterClient), + { concurrency: MAX_CONCURRENT_SEARCHES } ); - - updatedAlerts.forEach((updatedAlert, index) => { - if (updatedAlert.status === 'rejected') { - logger.error( - `Failed to update status for alert: ${JSON.stringify(alertsToUpdate[index])}: ${ - updatedAlert.reason - }` - ); - } - }); } catch (error) { throw createCaseError({ message: `Failed to update alert status ids: ${JSON.stringify(alerts)}: ${error}`, @@ -79,51 +70,25 @@ export class AlertService { } } - public async getAlerts({ alertsInfo, logger }: GetAlertsArgs): Promise { + public async getAlerts({ + scopedClusterClient, + alertsInfo, + logger, + }: GetAlertsArgs): Promise { try { - if (!this.alertsClient) { - throw new Error( - 'Alert client is undefined, the rule registry plugin must be enabled to retrieve alerts' - ); - } + const docs = alertsInfo + .filter((alert) => !isEmptyAlert(alert)) + .slice(0, MAX_ALERTS_PER_SUB_CASE) + .map((alert) => ({ _id: alert.id, _index: alert.index })); - const alertsToGet = alertsInfo - .filter((alert) => !isEmpty(alert)) - .slice(0, MAX_ALERTS_PER_SUB_CASE); - - if (alertsToGet.length <= 0) { + if (docs.length <= 0) { return; } - const retrievedAlerts = await Promise.allSettled( - alertsToGet.map(({ id, index }) => this.alertsClient?.get({ id, index })) - ); - - retrievedAlerts.forEach((alert, index) => { - if (alert.status === 'rejected') { - logger.error( - `Failed to retrieve alert: ${JSON.stringify(alertsToGet[index])}: ${alert.reason}` - ); - } - }); + const results = await scopedClusterClient.mget({ body: { docs } }); - return retrievedAlerts.map((alert, index) => { - let source: unknown | undefined; - let error: Error | undefined; - - if (alert.status === 'fulfilled') { - source = alert.value; - } else { - error = alert.reason; - } - - return { - id: alertsToGet[index].id, - index: alertsToGet[index].index, - source, - error, - }; - }); + // @ts-expect-error @elastic/elasticsearch _source is optional + return results.body; } catch (error) { throw createCaseError({ message: `Failed to retrieve alerts ids: ${JSON.stringify(alertsInfo)}: ${error}`, @@ -134,6 +99,44 @@ export class AlertService { } } +interface TranslatedUpdateAlertRequest { + id: string; + index: string; + status: STATUS_VALUES; +} + +function bucketAlertsByIndexAndStatus( + alerts: UpdateAlertRequest[], + logger: Logger +): Map> { + return alerts.reduce>>( + (acc, alert) => { + // skip any alerts that are empty + if (isEmptyAlert(alert)) { + return acc; + } + + const translatedAlert = { ...alert, status: translateStatus({ alert, logger }) }; + const statusToAlertId = acc.get(translatedAlert.index); + + // if we haven't seen the index before + if (!statusToAlertId) { + // add a new index in the parent map, with an entry for the status the alert set to pointing + // to an initial array of only the current alert + acc.set(translatedAlert.index, createStatusToAlertMap(translatedAlert)); + } else { + // We had the index in the map so check to see if we have a bucket for the + // status, if not add a new status entry with the alert, if so update the status entry + // with the alert + updateIndexEntryWithStatus(statusToAlertId, translatedAlert); + } + + return acc; + }, + new Map() + ); +} + function translateStatus({ alert, logger, @@ -157,3 +160,53 @@ function translateStatus({ } return translatedStatus ?? 'open'; } + +function createStatusToAlertMap( + alert: TranslatedUpdateAlertRequest +): Map { + return new Map([[alert.status, [alert]]]); +} + +function updateIndexEntryWithStatus( + statusToAlerts: Map, + alert: TranslatedUpdateAlertRequest +) { + const statusBucket = statusToAlerts.get(alert.status); + + if (!statusBucket) { + statusToAlerts.set(alert.status, [alert]); + } else { + statusBucket.push(alert); + } +} + +async function updateByQuery( + [index, statusToAlertMap]: [string, Map], + scopedClusterClient: ElasticsearchClient +) { + const statusBuckets = Array.from(statusToAlertMap); + return Promise.all( + // this will create three update by query calls one for each of the three statuses + statusBuckets.map(([status, translatedAlerts]) => + scopedClusterClient.updateByQuery({ + index, + conflicts: 'abort', + body: { + script: { + source: `if (ctx._source['${ALERT_WORKFLOW_STATUS}'] != null) { + ctx._source['${ALERT_WORKFLOW_STATUS}'] = '${status}' + } + if (ctx._source.signal != null && ctx._source.signal.status != null) { + ctx._source.signal.status = '${status}' + }`, + lang: 'painless', + }, + // the query here will contain all the ids that have the same status for the same index + // being updated + query: { ids: { values: translatedAlerts.map(({ id }) => id) } }, + }, + ignore_unavailable: true, + }) + ) + ); +} diff --git a/x-pack/plugins/cases/server/services/alerts/types.ts b/x-pack/plugins/cases/server/services/alerts/types.ts deleted file mode 100644 index 5ddc57fa5861c..0000000000000 --- a/x-pack/plugins/cases/server/services/alerts/types.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* - * 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 interface Alert { - id: string; - index: string; - error?: Error; - source?: unknown; -} diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts index 7d72e759c9f1a..e7d04a38305fb 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts @@ -52,7 +52,7 @@ describe('Alert details with unmapped fields', () => { }); }); - it('Displays the unmapped field on the table', () => { + it.skip('Displays the unmapped field on the table', () => { const expectedUnmmappedField = { row: 91, field: 'unmapped', diff --git a/x-pack/plugins/security_solution/cypress/tasks/alerts.ts b/x-pack/plugins/security_solution/cypress/tasks/alerts.ts index 1520a88ec31bc..871ef0ca51ce3 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/alerts.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/alerts.ts @@ -63,7 +63,12 @@ export const closeAlerts = () => { }; export const expandFirstAlert = () => { - cy.get(EXPAND_ALERT_BTN).should('exist').first().click({ force: true }); + cy.get(EXPAND_ALERT_BTN).should('exist'); + + cy.get(EXPAND_ALERT_BTN) + .first() + .pipe(($el) => $el.trigger('click')) + .should('exist'); }; export const viewThreatIntelTab = () => cy.get(THREAT_INTEL_TAB).click(); diff --git a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts index f34c3f598e934..e2d27a11ed717 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts @@ -532,7 +532,6 @@ export const waitForAlertsToPopulate = async (alertCountThreshold = 1) => { cy.waitUntil( () => { refreshPage(); - cy.get(LOADING_INDICATOR).should('exist'); cy.get(LOADING_INDICATOR).should('not.exist'); return cy .get(SERVER_SIDE_EVENT_COUNT) diff --git a/x-pack/plugins/security_solution/public/app/deep_links/index.test.ts b/x-pack/plugins/security_solution/public/app/deep_links/index.test.ts index 4df49b957ad9c..59af6737e495f 100644 --- a/x-pack/plugins/security_solution/public/app/deep_links/index.test.ts +++ b/x-pack/plugins/security_solution/public/app/deep_links/index.test.ts @@ -101,68 +101,4 @@ describe('public search functions', () => { }); expect(deepLinks.some((l) => l.id === SecurityPageName.ueba)).toBeTruthy(); }); - - describe('Detections Alerts deep links', () => { - it('should return alerts link for basic license with only read_alerts capabilities', () => { - const basicLicense = 'basic'; - const basicLinks = getDeepLinks(mockGlobalState.app.enableExperimental, basicLicense, ({ - siem: { read_alerts: true, crud_alerts: false }, - } as unknown) as Capabilities); - - const detectionsDeepLinks = - basicLinks.find((l) => l.id === SecurityPageName.detections)?.deepLinks ?? []; - - expect( - detectionsDeepLinks.length && - detectionsDeepLinks.some((l) => l.id === SecurityPageName.alerts) - ).toBeTruthy(); - }); - - it('should return alerts link with for basic license with crud_alerts capabilities', () => { - const basicLicense = 'basic'; - const basicLinks = getDeepLinks(mockGlobalState.app.enableExperimental, basicLicense, ({ - siem: { read_alerts: true, crud_alerts: true }, - } as unknown) as Capabilities); - - const detectionsDeepLinks = - basicLinks.find((l) => l.id === SecurityPageName.detections)?.deepLinks ?? []; - - expect( - detectionsDeepLinks.length && - detectionsDeepLinks.some((l) => l.id === SecurityPageName.alerts) - ).toBeTruthy(); - }); - - it('should NOT return alerts link for basic license with NO read_alerts capabilities', () => { - const basicLicense = 'basic'; - const basicLinks = getDeepLinks(mockGlobalState.app.enableExperimental, basicLicense, ({ - siem: { read_alerts: false, crud_alerts: false }, - } as unknown) as Capabilities); - - const detectionsDeepLinks = - basicLinks.find((l) => l.id === SecurityPageName.detections)?.deepLinks ?? []; - - expect( - detectionsDeepLinks.length && - detectionsDeepLinks.some((l) => l.id === SecurityPageName.alerts) - ).toBeFalsy(); - }); - - it('should return alerts link for basic license with undefined capabilities', () => { - const basicLicense = 'basic'; - const basicLinks = getDeepLinks( - mockGlobalState.app.enableExperimental, - basicLicense, - undefined - ); - - const detectionsDeepLinks = - basicLinks.find((l) => l.id === SecurityPageName.detections)?.deepLinks ?? []; - - expect( - detectionsDeepLinks.length && - detectionsDeepLinks.some((l) => l.id === SecurityPageName.alerts) - ).toBeTruthy(); - }); - }); }); diff --git a/x-pack/plugins/security_solution/public/app/deep_links/index.ts b/x-pack/plugins/security_solution/public/app/deep_links/index.ts index bafab2dd659f4..9f13a8be0e13a 100644 --- a/x-pack/plugins/security_solution/public/app/deep_links/index.ts +++ b/x-pack/plugins/security_solution/public/app/deep_links/index.ts @@ -368,16 +368,7 @@ export function getDeepLinks( deepLinks: [], }; } - if ( - deepLinkId === SecurityPageName.detections && - capabilities != null && - capabilities.siem.read_alerts === false - ) { - return { - ...deepLink, - deepLinks: baseDeepLinks.filter(({ id }) => id !== SecurityPageName.alerts), - }; - } + if (isPremiumLicense(licenseType) && subPluginDeepLinks?.premium) { return { ...deepLink, diff --git a/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx b/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx index 3ec616127f243..7041cc4264504 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx @@ -59,7 +59,7 @@ const TimelineDetailsPanel = () => { diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx index 29ba8fc0bd541..7b7a1ead5d702 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx @@ -33,14 +33,6 @@ import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_fe import { defaultCellActions } from '../../lib/cell_actions/default_cell_actions'; import { mockTimelines } from '../../mock/mock_timelines_plugin'; -jest.mock('@kbn/alerts', () => ({ - useGetUserAlertsPermissions: () => ({ - loading: false, - crud: true, - read: true, - }), -})); - jest.mock('../../lib/kibana', () => ({ useKibana: () => ({ services: { diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx index 4b8851b0373a4..c91b646aba967 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx @@ -5,8 +5,8 @@ * 2.0. */ -import React, { useMemo, useEffect } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; +import React, { useCallback, useMemo, useEffect } from 'react'; +import { connect, ConnectedProps, useDispatch } from 'react-redux'; import deepEqual from 'fast-deep-equal'; import styled from 'styled-components'; @@ -109,6 +109,7 @@ const StatefulEventsViewerComponent: React.FC = ({ hasAlertsCrud = false, unit, }) => { + const dispatch = useDispatch(); const { timelines: timelinesUi } = useKibana().services; const { browserFields, @@ -151,6 +152,13 @@ const StatefulEventsViewerComponent: React.FC = ({ ) : null, [graphEventId, id] ); + const setQuery = useCallback( + (inspect, loading, refetch) => { + dispatch(inputsActions.setQuery({ id, inputId: 'global', inspect, loading, refetch })); + }, + [dispatch, id] + ); + return ( <> @@ -182,6 +190,7 @@ const StatefulEventsViewerComponent: React.FC = ({ onRuleChange, renderCellValue, rowRenderers, + setQuery, start, sort, additionalFilters, diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx index 1f98d3b826129..b488000ac8736 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx @@ -4,7 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { useGetUserAlertsPermissions } from '@kbn/alerts'; import { renderHook } from '@testing-library/react-hooks'; import { KibanaPageTemplateProps } from '../../../../../../../../src/plugins/kibana_react/public'; @@ -24,7 +23,6 @@ jest.mock('../../../lib/kibana'); jest.mock('../../../hooks/use_selector'); jest.mock('../../../hooks/use_experimental_features'); jest.mock('../../../utils/route/use_route_spy'); -jest.mock('@kbn/alerts'); describe('useSecuritySolutionNavigation', () => { const mockUrlState = { [CONSTANTS.appQuery]: { query: 'host.name:"security-solution-es"', language: 'kuery' }, @@ -76,11 +74,6 @@ describe('useSecuritySolutionNavigation', () => { (useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(false); (useDeepEqualSelector as jest.Mock).mockReturnValue({ urlState: mockUrlState }); (useRouteSpy as jest.Mock).mockReturnValue(mockRouteSpy); - (useGetUserAlertsPermissions as jest.Mock).mockReturnValue({ - loading: false, - crud: true, - read: true, - }); (useKibana as jest.Mock).mockReturnValue({ services: { diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx index ca574a5872761..1630bc47fd0c3 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx @@ -7,15 +7,13 @@ import React, { useCallback, useMemo } from 'react'; import { EuiSideNavItemType } from '@elastic/eui/src/components/side_nav/side_nav_types'; -import { useGetUserAlertsPermissions } from '@kbn/alerts'; import { securityNavGroup } from '../../../../app/home/home_navigations'; import { getSearch } from '../helpers'; import { PrimaryNavigationItemsProps } from './types'; -import { useGetUserCasesPermissions, useKibana } from '../../../lib/kibana'; +import { useGetUserCasesPermissions } from '../../../lib/kibana'; import { useNavigation } from '../../../lib/kibana/hooks'; import { NavTab } from '../types'; -import { SERVER_APP_ID } from '../../../../../common/constants'; export const usePrimaryNavigationItems = ({ navTabs, @@ -63,9 +61,7 @@ export const usePrimaryNavigationItems = ({ }; function usePrimaryNavigationItemsToDisplay(navTabs: Record) { - const uiCapabilities = useKibana().services.application.capabilities; const hasCasesReadPermissions = useGetUserCasesPermissions()?.read; - const hasAlertsReadPermissions = useGetUserAlertsPermissions(uiCapabilities, SERVER_APP_ID); return useMemo( () => [ { @@ -75,9 +71,7 @@ function usePrimaryNavigationItemsToDisplay(navTabs: Record) { }, { ...securityNavGroup.detect, - items: hasAlertsReadPermissions.read - ? [navTabs.alerts, navTabs.rules, navTabs.exceptions] - : [navTabs.rules, navTabs.exceptions], + items: [navTabs.alerts, navTabs.rules, navTabs.exceptions], }, { ...securityNavGroup.explore, @@ -92,6 +86,6 @@ function usePrimaryNavigationItemsToDisplay(navTabs: Record) { items: [navTabs.endpoints, navTabs.trusted_apps, navTabs.event_filters], }, ], - [navTabs, hasCasesReadPermissions, hasAlertsReadPermissions] + [navTabs, hasCasesReadPermissions] ); } diff --git a/x-pack/plugins/security_solution/public/common/components/user_privileges/index.tsx b/x-pack/plugins/security_solution/public/common/components/user_privileges/index.tsx index fa9de895f7d03..028473f5c2001 100644 --- a/x-pack/plugins/security_solution/public/common/components/user_privileges/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/user_privileges/index.tsx @@ -5,9 +5,8 @@ * 2.0. */ -import React, { createContext, useContext } from 'react'; +import React, { createContext, useContext, useEffect, useState } from 'react'; import { DeepReadonly } from 'utility-types'; -import { useGetUserAlertsPermissions } from '@kbn/alerts'; import { Capabilities } from '../../../../../../../src/core/public'; import { useFetchDetectionEnginePrivileges } from '../../../detections/components/user_privileges/use_fetch_detection_engine_privileges'; @@ -19,14 +18,14 @@ export interface UserPrivilegesState { listPrivileges: ReturnType; detectionEnginePrivileges: ReturnType; endpointPrivileges: EndpointPrivileges; - alertsPrivileges: ReturnType; + kibanaSecuritySolutionsPrivileges: { crud: boolean; read: boolean }; } export const initialUserPrivilegesState = (): UserPrivilegesState => ({ listPrivileges: { loading: false, error: undefined, result: undefined }, detectionEnginePrivileges: { loading: false, error: undefined, result: undefined }, endpointPrivileges: { loading: true, canAccessEndpointManagement: false, canAccessFleet: false }, - alertsPrivileges: { loading: false, read: false, crud: false }, + kibanaSecuritySolutionsPrivileges: { crud: false, read: false }, }); const UserPrivilegesContext = createContext(initialUserPrivilegesState()); @@ -43,14 +42,29 @@ export const UserPrivilegesProvider = ({ const listPrivileges = useFetchListPrivileges(); const detectionEnginePrivileges = useFetchDetectionEnginePrivileges(); const endpointPrivileges = useEndpointPrivileges(); - const alertsPrivileges = useGetUserAlertsPermissions(kibanaCapabilities, SERVER_APP_ID); + const [kibanaSecuritySolutionsPrivileges, setKibanaSecuritySolutionsPrivileges] = useState({ + crud: false, + read: false, + }); + const crud: boolean = kibanaCapabilities[SERVER_APP_ID].crud === true; + const read: boolean = kibanaCapabilities[SERVER_APP_ID].show === true; + + useEffect(() => { + setKibanaSecuritySolutionsPrivileges((currPrivileges) => { + if (currPrivileges.read !== read || currPrivileges.crud !== crud) { + return { read, crud }; + } + return currPrivileges; + }); + }, [crud, read]); + return ( {children} 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 9cc844a80b031..6bd902658c8e4 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,8 +14,6 @@ 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'; @@ -49,7 +47,8 @@ export const AlertsCountPanel = memo( // ? fetchQueryRuleRegistryAlerts // : fetchQueryAlerts; - const fetchMethod = fetchQueryRuleRegistryAlerts; + // Disabling the fecth method in useQueryAlerts since it is defaulted to the old one + // const fetchMethod = fetchQueryRuleRegistryAlerts; const additionalFilters = useMemo(() => { try { @@ -73,7 +72,6 @@ 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 b296371bae58d..2182ed7da0c4f 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,7 +43,6 @@ 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, @@ -117,16 +116,12 @@ export const AlertsHistogramPanel = memo( request, refetch, } = useQueryAlerts<{}, AlertsAggregation>({ - fetchMethod: fetchQueryRuleRegistryAlerts, - query: { - index: signalIndexName, - ...getAlertsHistogramQuery( - selectedStackByOption, - from, - to, - buildCombinedQueries(combinedQueries) - ), - }, + query: getAlertsHistogramQuery( + selectedStackByOption, + from, + to, + buildCombinedQueries(combinedQueries) + ), indexName: signalIndexName, }); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx index 4b3c792319cd1..e179c02987462 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx @@ -381,7 +381,7 @@ export const AlertsTableComponent: React.FC = ({ pageFilters={defaultFiltersMemo} defaultCellActions={defaultCellActions} defaultModel={defaultTimelineModel} - entityType="alerts" + entityType="events" end={to} currentFilter={filterGroup} id={timelineId} @@ -392,7 +392,7 @@ export const AlertsTableComponent: React.FC = ({ start={from} utilityBar={utilityBarCallback} additionalFilters={additionalFiltersComponent} - hasAlertsCrud={hasIndexWrite} + hasAlertsCrud={hasIndexWrite && hasIndexMaintenance} /> ); }; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx index eb31a59f0ca87..9568f9c894e24 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx @@ -13,14 +13,6 @@ import React from 'react'; import { Ecs } from '../../../../../common/ecs'; import { mockTimelines } from '../../../../common/mock/mock_timelines_plugin'; -jest.mock('@kbn/alerts', () => ({ - useGetUserAlertsPermissions: () => ({ - loading: false, - crud: true, - read: true, - }), -})); - const ecsRowData: Ecs = { _id: '1', agent: { type: ['blah'] } }; const props = { diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_alerts_actions.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_alerts_actions.tsx index 3568972aef2e9..8da4ce1c3ed7f 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_alerts_actions.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_alerts_actions.tsx @@ -7,15 +7,12 @@ import { useCallback } from 'react'; import { useDispatch } from 'react-redux'; -import { useGetUserAlertsPermissions } from '@kbn/alerts'; import { useStatusBulkActionItems } from '../../../../../../timelines/public'; import { Status } from '../../../../../common/detection_engine/schemas/common/schemas'; import { timelineActions } from '../../../../timelines/store/timeline'; +import { useAlertsPrivileges } from '../../../containers/detection_engine/alerts/use_alerts_privileges'; import { SetEventsDeletedProps, SetEventsLoadingProps } from '../types'; - -import { useKibana } from '../../../../common/lib/kibana'; -import { SERVER_APP_ID } from '../../../../../common/constants'; interface Props { alertStatus?: Status; closePopover: () => void; @@ -34,8 +31,7 @@ export const useAlertsActions = ({ refetch, }: Props) => { const dispatch = useDispatch(); - const uiCapabilities = useKibana().services.application.capabilities; - const alertsPrivileges = useGetUserAlertsPermissions(uiCapabilities, SERVER_APP_ID); + const { hasIndexWrite, hasKibanaCRUD } = useAlertsPrivileges(); const onStatusUpdate = useCallback(() => { closePopover(); @@ -66,9 +62,10 @@ export const useAlertsActions = ({ setEventsDeleted, onUpdateSuccess: onStatusUpdate, onUpdateFailure: onStatusUpdate, + timelineId, }); return { - actionItems: alertsPrivileges.crud ? actionItems : [], + actionItems: hasIndexWrite && hasKibanaCRUD ? actionItems : [], }; }; diff --git a/x-pack/plugins/security_solution/public/detections/components/callouts/missing_privileges_callout/translations.tsx b/x-pack/plugins/security_solution/public/detections/components/callouts/missing_privileges_callout/translations.tsx index 3509ad73001ec..0d628d89c0925 100644 --- a/x-pack/plugins/security_solution/public/detections/components/callouts/missing_privileges_callout/translations.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/callouts/missing_privileges_callout/translations.tsx @@ -9,10 +9,6 @@ import { EuiCode } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; -import { - DetectionsRequirementsLink, - SecuritySolutionRequirementsLink, -} from '../../../../common/components/links_to_docs'; import { DEFAULT_ITEMS_INDEX, DEFAULT_LISTS_INDEX, @@ -21,6 +17,10 @@ import { } from '../../../../../common/constants'; import { CommaSeparatedValues } from './comma_separated_values'; import { MissingPrivileges } from './use_missing_privileges'; +import { + DetectionsRequirementsLink, + SecuritySolutionRequirementsLink, +} from '../../../../common/components/links_to_docs'; export const MISSING_PRIVILEGES_CALLOUT_TITLE = i18n.translate( 'xpack.securitySolution.detectionEngine.missingPrivilegesCallOut.messageTitle', @@ -46,17 +46,17 @@ const CANNOT_EDIT_LISTS = i18n.translate( const CANNOT_EDIT_ALERTS = i18n.translate( 'xpack.securitySolution.detectionEngine.missingPrivilegesCallOut.cannotEditAlerts', { - defaultMessage: 'Without these privileges, you cannot open or close alerts.', + defaultMessage: 'Without these privileges, you cannot view or change status of alerts.', } ); export const missingPrivilegesCallOutBody = ({ indexPrivileges, - featurePrivileges, + featurePrivileges = [], }: MissingPrivileges) => ( @@ -77,23 +77,30 @@ export const missingPrivilegesCallOutBody = ({ {indexPrivileges.map(([index, missingPrivileges]) => (
  • {missingIndexPrivileges(index, missingPrivileges)}
  • ))} - - - ) : null, - featurePrivileges: - featurePrivileges.length > 0 ? ( - <> - -
      - {featurePrivileges.map(([feature, missingPrivileges]) => ( + { + // TODO: Uncomment once RBAC for alerts is reenabled + /* {featurePrivileges.map(([feature, missingPrivileges]) => (
    • {missingFeaturePrivileges(feature, missingPrivileges)}
    • - ))} + ))} */ + }
    ) : null, + // TODO: Uncomment once RBAC for alerts is reenabled + // featurePrivileges: + // featurePrivileges.length > 0 ? ( + // <> + // + //
      + // {featurePrivileges.map(([feature, missingPrivileges]) => ( + //
    • {missingFeaturePrivileges(feature, missingPrivileges)}
    • + // ))} + //
    + // + // ) : null, docs: (
    • @@ -152,14 +159,15 @@ const missingIndexPrivileges = (index: string, privileges: string[]) => ( /> ); -const missingFeaturePrivileges = (feature: string, privileges: string[]) => ( - , - index: {feature}, - explanation: getPrivilegesExplanation(privileges, feature), - }} - /> -); +// TODO: Uncomment once RBAC for alerts is reenabled +// const missingFeaturePrivileges = (feature: string, privileges: string[]) => ( +// , +// index: {feature}, +// explanation: getPrivilegesExplanation(privileges, feature), +// }} +// /> +// ); diff --git a/x-pack/plugins/security_solution/public/detections/components/callouts/missing_privileges_callout/use_missing_privileges.ts b/x-pack/plugins/security_solution/public/detections/components/callouts/missing_privileges_callout/use_missing_privileges.ts index ea2b081239fda..eec9bd1f09053 100644 --- a/x-pack/plugins/security_solution/public/detections/components/callouts/missing_privileges_callout/use_missing_privileges.ts +++ b/x-pack/plugins/security_solution/public/detections/components/callouts/missing_privileges_callout/use_missing_privileges.ts @@ -40,14 +40,18 @@ export interface MissingPrivileges { } export const useMissingPrivileges = (): MissingPrivileges => { - const { listPrivileges } = useUserPrivileges(); + const { detectionEnginePrivileges, listPrivileges } = useUserPrivileges(); const [{ canUserCRUD }] = useUserData(); return useMemo(() => { const featurePrivileges: MissingFeaturePrivileges[] = []; const indexPrivileges: MissingIndexPrivileges[] = []; - if (canUserCRUD == null || listPrivileges.result == null) { + if ( + canUserCRUD == null || + listPrivileges.result == null || + detectionEnginePrivileges.result == null + ) { /** * Do not check privileges till we get all the data. That helps to reduce * subsequent layout shift while loading and skip unneeded re-renders. @@ -72,9 +76,16 @@ export const useMissingPrivileges = (): MissingPrivileges => { indexPrivileges.push(missingListsPrivileges); } + const missingDetectionPrivileges = getMissingIndexPrivileges( + detectionEnginePrivileges.result.index + ); + if (missingDetectionPrivileges) { + indexPrivileges.push(missingDetectionPrivileges); + } + return { featurePrivileges, indexPrivileges, }; - }, [canUserCRUD, listPrivileges]); + }, [canUserCRUD, listPrivileges, detectionEnginePrivileges]); }; diff --git a/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.test.tsx index 76c0017f6fa9c..bba652bcdd030 100644 --- a/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.test.tsx @@ -24,14 +24,14 @@ jest.mock('../../../common/lib/kibana', () => ({ useKibana: jest.fn(), useGetUserCasesPermissions: jest.fn().mockReturnValue({ crud: true }), })); +jest.mock('../../containers/detection_engine/alerts/use_alerts_privileges', () => ({ + useAlertsPrivileges: jest.fn().mockReturnValue({ hasIndexWrite: true, hasKibanaCRUD: true }), +})); jest.mock('../../../cases/components/use_insert_timeline'); jest.mock('../../../common/hooks/use_experimental_features', () => ({ useIsExperimentalFeatureEnabled: jest.fn().mockReturnValue(true), })); -jest.mock('@kbn/alerts', () => { - return { useGetUserAlertsPermissions: jest.fn().mockReturnValue({ crud: true }) }; -}); jest.mock('../../../common/utils/endpoint_alert_check', () => { return { endpointAlertCheck: jest.fn().mockReturnValue(true) }; diff --git a/x-pack/plugins/security_solution/public/detections/components/user_info/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/user_info/index.test.tsx index 9972233dce351..67863f05c7d83 100644 --- a/x-pack/plugins/security_solution/public/detections/components/user_info/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/user_info/index.test.tsx @@ -43,6 +43,7 @@ describe('useUserInfo', () => { expect(result.all).toHaveLength(1); expect(result.current).toEqual({ canUserCRUD: null, + canUserREAD: null, hasEncryptionKey: null, hasIndexManage: null, hasIndexMaintenance: null, diff --git a/x-pack/plugins/security_solution/public/detections/components/user_info/index.tsx b/x-pack/plugins/security_solution/public/detections/components/user_info/index.tsx index da6df631d951e..9c81b51445f60 100644 --- a/x-pack/plugins/security_solution/public/detections/components/user_info/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/user_info/index.tsx @@ -10,11 +10,11 @@ import React, { useEffect, useReducer, Dispatch, createContext, useContext } fro import { useAlertsPrivileges } from '../../containers/detection_engine/alerts/use_alerts_privileges'; import { useSignalIndex } from '../../containers/detection_engine/alerts/use_signal_index'; -import { useKibana } from '../../../common/lib/kibana'; import { useCreateTransforms } from '../../../transforms/containers/use_create_transforms'; export interface State { canUserCRUD: boolean | null; + canUserREAD: boolean | null; hasIndexManage: boolean | null; hasIndexMaintenance: boolean | null; hasIndexWrite: boolean | null; @@ -30,6 +30,7 @@ export interface State { export const initialState: State = { canUserCRUD: null, + canUserREAD: null, hasIndexManage: null, hasIndexMaintenance: null, hasIndexWrite: null, @@ -77,10 +78,6 @@ export type Action = type: 'updateHasEncryptionKey'; hasEncryptionKey: boolean | null; } - | { - type: 'updateCanUserCRUD'; - canUserCRUD: boolean | null; - } | { type: 'updateSignalIndexName'; signalIndexName: string | null; @@ -88,6 +85,14 @@ export type Action = | { type: 'updateSignalIndexMappingOutdated'; signalIndexMappingOutdated: boolean | null; + } + | { + type: 'updateCanUserCRUD'; + canUserCRUD: boolean | null; + } + | { + type: 'updateCanUserREAD'; + canUserREAD: boolean | null; }; export const userInfoReducer = (state: State, action: Action): State => { @@ -146,12 +151,6 @@ export const userInfoReducer = (state: State, action: Action): State => { hasEncryptionKey: action.hasEncryptionKey, }; } - case 'updateCanUserCRUD': { - return { - ...state, - canUserCRUD: action.canUserCRUD, - }; - } case 'updateSignalIndexName': { return { ...state, @@ -164,6 +163,18 @@ export const userInfoReducer = (state: State, action: Action): State => { signalIndexMappingOutdated: action.signalIndexMappingOutdated, }; } + case 'updateCanUserCRUD': { + return { + ...state, + canUserCRUD: action.canUserCRUD, + }; + } + case 'updateCanUserREAD': { + return { + ...state, + canUserREAD: action.canUserREAD, + }; + } default: return state; } @@ -187,6 +198,7 @@ export const useUserInfo = (): State => { const [ { canUserCRUD, + canUserREAD, hasIndexManage, hasIndexMaintenance, hasIndexWrite, @@ -210,6 +222,8 @@ export const useUserInfo = (): State => { hasIndexUpdateDelete: hasApiIndexUpdateDelete, hasIndexWrite: hasApiIndexWrite, hasIndexRead: hasApiIndexRead, + hasKibanaCRUD, + hasKibanaREAD, } = useAlertsPrivileges(); const { loading: indexNameLoading, @@ -221,8 +235,17 @@ export const useUserInfo = (): State => { const { createTransforms } = useCreateTransforms(); - const uiCapabilities = useKibana().services.application.capabilities; - const capabilitiesCanUserCRUD: boolean = uiCapabilities.siem.crud === true; + useEffect(() => { + if (!loading && canUserCRUD !== hasKibanaCRUD) { + dispatch({ type: 'updateCanUserCRUD', canUserCRUD: hasKibanaCRUD }); + } + }, [dispatch, loading, canUserCRUD, hasKibanaCRUD]); + + useEffect(() => { + if (!loading && canUserREAD !== hasKibanaREAD) { + dispatch({ type: 'updateCanUserREAD', canUserREAD: hasKibanaREAD }); + } + }, [dispatch, loading, canUserREAD, hasKibanaREAD]); useEffect(() => { if (loading !== (privilegeLoading || indexNameLoading)) { @@ -293,12 +316,6 @@ export const useUserInfo = (): State => { } }, [dispatch, loading, hasEncryptionKey, isApiEncryptionKey]); - useEffect(() => { - if (!loading && canUserCRUD !== capabilitiesCanUserCRUD) { - dispatch({ type: 'updateCanUserCRUD', canUserCRUD: capabilitiesCanUserCRUD }); - } - }, [dispatch, loading, canUserCRUD, capabilitiesCanUserCRUD]); - useEffect(() => { if (!loading && signalIndexName !== apiSignalIndexName && apiSignalIndexName != null) { dispatch({ type: 'updateSignalIndexName', signalIndexName: apiSignalIndexName }); @@ -351,6 +368,7 @@ export const useUserInfo = (): State => { isAuthenticated, hasEncryptionKey, canUserCRUD, + canUserREAD, hasIndexManage, hasIndexMaintenance, hasIndexWrite, diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_alerts_privileges.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_alerts_privileges.test.tsx index 64d9db80316a9..cbab24835c1ac 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_alerts_privileges.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_alerts_privileges.test.tsx @@ -87,10 +87,10 @@ const userPrivilegesInitial: ReturnType = { error: undefined, }, endpointPrivileges: { loading: true, canAccessEndpointManagement: false, canAccessFleet: false }, - alertsPrivileges: { loading: true, crud: false, read: false }, + kibanaSecuritySolutionsPrivileges: { crud: true, read: true }, }; -describe('usePrivilegeUser', () => { +describe('useAlertsPrivileges', () => { let appToastsMock: jest.Mocked>; beforeEach(() => { @@ -113,13 +113,15 @@ describe('usePrivilegeUser', () => { hasIndexMaintenance: null, hasIndexWrite: null, hasIndexUpdateDelete: null, + hasKibanaCRUD: false, + hasKibanaREAD: false, isAuthenticated: null, loading: false, }); }); }); - test('if there is an error when fetching user privilege, we should get back false for every properties', async () => { + test('if there is an error when fetching user privilege, we should get back false for all index related properties', async () => { const userPrivileges = produce(userPrivilegesInitial, (draft) => { draft.detectionEnginePrivileges.error = new Error('Something went wrong'); }); @@ -137,6 +139,8 @@ describe('usePrivilegeUser', () => { hasIndexRead: false, hasIndexWrite: false, hasIndexUpdateDelete: false, + hasKibanaCRUD: true, + hasKibanaREAD: true, isAuthenticated: false, loading: false, }); @@ -162,9 +166,11 @@ describe('usePrivilegeUser', () => { hasEncryptionKey: true, hasIndexManage: false, hasIndexMaintenance: true, - hasIndexRead: false, - hasIndexWrite: false, + hasIndexRead: true, + hasIndexWrite: true, hasIndexUpdateDelete: true, + hasKibanaCRUD: true, + hasKibanaREAD: true, isAuthenticated: true, loading: false, }); @@ -187,9 +193,67 @@ describe('usePrivilegeUser', () => { hasEncryptionKey: true, hasIndexManage: true, hasIndexMaintenance: true, - hasIndexRead: false, - hasIndexWrite: false, + hasIndexRead: true, + hasIndexWrite: true, + hasIndexUpdateDelete: true, + hasKibanaCRUD: true, + hasKibanaREAD: true, + isAuthenticated: true, + loading: false, + }); + }); + }); + + test('returns "hasKibanaCRUD" as false if user does not have SIEM Kibana "all" privileges', async () => { + const userPrivileges = produce(userPrivilegesInitial, (draft) => { + draft.detectionEnginePrivileges.result = privilege; + draft.kibanaSecuritySolutionsPrivileges = { crud: false, read: true }; + }); + useUserPrivilegesMock.mockReturnValue(userPrivileges); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useAlertsPrivileges() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(result.current).toEqual({ + hasEncryptionKey: true, + hasIndexManage: true, + hasIndexMaintenance: true, + hasIndexRead: true, + hasIndexWrite: true, + hasIndexUpdateDelete: true, + hasKibanaCRUD: false, + hasKibanaREAD: true, + isAuthenticated: true, + loading: false, + }); + }); + }); + + test('returns "hasKibanaREAD" as false if user does not have at least SIEM Kibana "read" privileges', async () => { + const userPrivileges = produce(userPrivilegesInitial, (draft) => { + draft.detectionEnginePrivileges.result = privilege; + draft.kibanaSecuritySolutionsPrivileges = { crud: false, read: false }; + }); + useUserPrivilegesMock.mockReturnValue(userPrivileges); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useAlertsPrivileges() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(result.current).toEqual({ + hasEncryptionKey: true, + hasIndexManage: true, + hasIndexMaintenance: true, + hasIndexRead: true, + hasIndexWrite: true, hasIndexUpdateDelete: true, + hasKibanaCRUD: false, + hasKibanaREAD: false, isAuthenticated: true, loading: false, }); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_alerts_privileges.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_alerts_privileges.tsx index 1d9b8228b5070..b377eda49d0cd 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_alerts_privileges.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_alerts_privileges.tsx @@ -20,6 +20,8 @@ export interface AlertsPrivelegesState { hasIndexUpdateDelete: boolean | null; hasIndexMaintenance: boolean | null; hasIndexRead: boolean | null; + hasKibanaCRUD: boolean; + hasKibanaREAD: boolean; } /** * Hook to get user privilege from @@ -34,8 +36,13 @@ export const useAlertsPrivileges = (): UseAlertsPrivelegesReturn => { hasIndexWrite: null, hasIndexUpdateDelete: null, hasIndexMaintenance: null, + hasKibanaCRUD: false, + hasKibanaREAD: false, }); - const { detectionEnginePrivileges, alertsPrivileges } = useUserPrivileges(); + const { + detectionEnginePrivileges, + kibanaSecuritySolutionsPrivileges: { crud: hasKibanaCRUD, read: hasKibanaREAD }, + } = useUserPrivileges(); useEffect(() => { if (detectionEnginePrivileges.error != null) { @@ -47,9 +54,11 @@ export const useAlertsPrivileges = (): UseAlertsPrivelegesReturn => { hasIndexWrite: false, hasIndexUpdateDelete: false, hasIndexMaintenance: false, + hasKibanaCRUD, + hasKibanaREAD, }); } - }, [detectionEnginePrivileges.error]); + }, [detectionEnginePrivileges.error, hasKibanaCRUD, hasKibanaREAD]); useEffect(() => { if (detectionEnginePrivileges.result != null) { @@ -62,13 +71,19 @@ export const useAlertsPrivileges = (): UseAlertsPrivelegesReturn => { hasEncryptionKey: privilege.has_encryption_key, hasIndexManage: privilege.index[indexName].manage && privilege.cluster.manage, hasIndexMaintenance: privilege.index[indexName].maintenance, - hasIndexRead: alertsPrivileges.read, - hasIndexWrite: alertsPrivileges.crud, + hasIndexRead: privilege.index[indexName].read, + hasIndexWrite: + privilege.index[indexName].create || + privilege.index[indexName].create_doc || + privilege.index[indexName].index || + privilege.index[indexName].write, hasIndexUpdateDelete: privilege.index[indexName].write, + hasKibanaCRUD, + hasKibanaREAD, }); } } - }, [detectionEnginePrivileges.result, alertsPrivileges]); + }, [detectionEnginePrivileges.result, hasKibanaCRUD, hasKibanaREAD]); return { loading: detectionEnginePrivileges.loading, ...privileges }; }; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.test.tsx index 6d68dae375866..ade83fed4fd6b 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.test.tsx @@ -14,13 +14,6 @@ import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; jest.mock('./api'); jest.mock('../../../../common/hooks/use_app_toasts'); jest.mock('../../../../common/components/user_privileges/use_endpoint_privileges'); -jest.mock('@kbn/alerts', () => ({ - useGetUserAlertsPermissions: () => ({ - loading: false, - crud: true, - read: true, - }), -})); describe('useSignalIndex', () => { let appToastsMock: jest.Mocked>; diff --git a/x-pack/plugins/security_solution/public/detections/pages/alerts/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/alerts/index.tsx index dbd59d2510238..18952feee528b 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/alerts/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/alerts/index.tsx @@ -7,15 +7,15 @@ import React, { useEffect } from 'react'; import { Route, Switch } from 'react-router-dom'; -import { useGetUserAlertsPermissions } from '@kbn/alerts'; -import { ALERTS_PATH, SecurityPageName, SERVER_APP_ID } from '../../../../common/constants'; +import { ALERTS_PATH, SecurityPageName } from '../../../../common/constants'; import { NotFoundPage } from '../../../app/404'; import * as i18n from './translations'; import { TrackApplicationView } from '../../../../../../../src/plugins/usage_collection/public'; import { DetectionEnginePage } from '../../pages/detection_engine/detection_engine'; import { useKibana } from '../../../common/lib/kibana'; import { SpyRoute } from '../../../common/utils/route/spy_routes'; +import { useAlertsPrivileges } from '../../containers/detection_engine/alerts/use_alerts_privileges'; const AlertsRoute = () => ( @@ -25,15 +25,12 @@ const AlertsRoute = () => ( ); const AlertsContainerComponent: React.FC = () => { - const { - chrome, - application: { capabilities }, - } = useKibana().services; - const userPermissions = useGetUserAlertsPermissions(capabilities, SERVER_APP_ID); + const { chrome } = useKibana().services; + const { hasIndexRead, hasIndexWrite } = useAlertsPrivileges(); useEffect(() => { // if the user is read only then display the glasses badge in the global navigation header - if (userPermissions != null && !userPermissions.crud && userPermissions.read) { + if (!hasIndexWrite && hasIndexRead) { chrome.setBadge({ text: i18n.READ_ONLY_BADGE_TEXT, tooltip: i18n.READ_ONLY_BADGE_TOOLTIP, @@ -45,7 +42,7 @@ const AlertsContainerComponent: React.FC = () => { return () => { chrome.setBadge(); }; - }, [userPermissions, chrome]); + }, [chrome, hasIndexRead, hasIndexWrite]); return ( diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx index a92f4d706dc7c..0d0c51bc540b4 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx @@ -80,7 +80,7 @@ jest.mock('../../../common/lib/kibana', () => { docLinks: { links: { siem: { - gettingStarted: 'link', + privileges: 'link', }, }, }, @@ -107,6 +107,7 @@ describe('DetectionEnginePageComponent', () => { (useUserData as jest.Mock).mockReturnValue([ { hasIndexRead: true, + canUserREAD: true, }, ]); (useSourcererScope as jest.Mock).mockReturnValue({ diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx index d6531198c1884..71542e6931489 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx @@ -5,6 +5,10 @@ * 2.0. */ +// No bueno, I know! Encountered when reverting RBAC work post initial BCs +// Don't want to include large amounts of refactor in this temporary workaround +// TODO: Refactor code - component can be broken apart +/* eslint-disable complexity */ import { EuiFlexGroup, EuiFlexItem, @@ -17,7 +21,6 @@ import { noop } from 'lodash/fp'; import React, { useCallback, useMemo, useRef, useState } from 'react'; import { connect, ConnectedProps, useDispatch } from 'react-redux'; import { Dispatch } from 'redux'; -import { AlertsFeatureNoPermissions } from '@kbn/alerts'; import { Status } from '../../../../common/detection_engine/schemas/common/schemas'; import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features'; @@ -73,6 +76,7 @@ import { AlertsTableFilterGroup, FILTER_OPEN, } from '../../components/alerts_table/alerts_filter_group'; +import { EmptyPage } from '../../../common/components/empty_page'; /** * Need a 100% height here to account for the graph/analyze tool, which sets no explicit height parameters, but fills the available space. @@ -117,7 +121,10 @@ const DetectionEnginePageComponent: React.FC = ({ isAuthenticated: isUserAuthenticated, hasEncryptionKey, signalIndexName, - hasIndexWrite, + hasIndexWrite = false, + hasIndexMaintenance = false, + canUserCRUD = false, + canUserREAD, hasIndexRead, }, ] = useUserData(); @@ -249,6 +256,18 @@ const DetectionEnginePageComponent: React.FC = ({ [containerElement, onSkipFocusBeforeEventsTable, onSkipFocusAfterEventsTable] ); + const emptyPageActions = useMemo( + () => ({ + feature: { + icon: 'documents', + label: i18n.GO_TO_DOCUMENTATION, + url: `${docLinks.links.siem.privileges}`, + target: '_blank', + }, + }), + [docLinks] + ); + if (isUserAuthenticated != null && !isUserAuthenticated && !loading) { return ( @@ -275,92 +294,89 @@ const DetectionEnginePageComponent: React.FC = ({ {hasEncryptionKey != null && !hasEncryptionKey && } - {indicesExist ? ( + {indicesExist && (hasIndexRead === false || canUserREAD === false) ? ( + + ) : indicesExist && hasIndexRead && canUserREAD ? ( - {hasIndexRead ? ( - <> - - - - - - - - {i18n.BUTTON_MANAGE_RULES} - - - - - - - - - {timelinesUi.getLastUpdated({ - updatedAt: updatedAt || 0, - showUpdating: loading, - })} - - - - - - - + + + + + + + + {i18n.BUTTON_MANAGE_RULES} + + + + + + + + + {timelinesUi.getLastUpdated({ + updatedAt: updatedAt || 0, + showUpdating: loading, + })} + + + + + + + - - - - + + + + - - + + - - - - ) : ( - - )} + ) : ( diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx index c1d674ce456ff..0c67a19e59e32 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx @@ -45,13 +45,6 @@ jest.mock('../../../../../common/containers/use_global_time', () => ({ setQuery: jest.fn(), }), })); -jest.mock('@kbn/alerts', () => ({ - useGetUserAlertsPermissions: () => ({ - loading: false, - crud: true, - read: true, - }), -})); jest.mock('react-router-dom', () => { const originalModule = jest.requireActual('react-router-dom'); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/translations.ts index 96e423aff1658..fedf119025304 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/translations.ts @@ -143,3 +143,18 @@ export const ML_RULES_UNAVAILABLE = (totalRules: number) => defaultMessage: '{totalRules} {totalRules, plural, =1 {rule requires} other {rules require}} Machine Learning to enable.', }); + +export const FEATURE_NO_PERMISSIONS_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.noPermissionsTitle', + { + defaultMessage: 'Privileges required', + } +); + +export const ALERTS_FEATURE_NO_PERMISSIONS_MSG = i18n.translate( + 'xpack.securitySolution.detectionEngine.noPermissionsMessage', + { + defaultMessage: + 'To view alerts, you must update privileges. For more information, contact your Kibana administrator.', + } +); diff --git a/x-pack/plugins/security_solution/public/overview/pages/overview.tsx b/x-pack/plugins/security_solution/public/overview/pages/overview.tsx index 8deb1b93f9728..ffafe211960d5 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/overview.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/overview.tsx @@ -34,6 +34,7 @@ import { useDeepEqualSelector } from '../../common/hooks/use_selector'; import { ThreatIntelLinkPanel } from '../components/overview_cti_links'; import { useIsThreatIntelModuleEnabled } from '../containers/overview_cti_links/use_is_threat_intel_module_enabled'; import { useUserPrivileges } from '../../common/components/user_privileges'; +import { useAlertsPrivileges } from '../../detections/containers/detection_engine/alerts/use_alerts_privileges'; const SidebarFlexItem = styled(EuiFlexItem)` margin-right: 24px; @@ -72,8 +73,8 @@ const OverviewComponent = () => { }, [addMessage]); const { endpointPrivileges: { canAccessFleet }, - alertsPrivileges, } = useUserPrivileges(); + const { hasIndexRead, hasKibanaREAD } = useAlertsPrivileges(); const isThreatIntelModuleEnabled = useIsThreatIntelModuleEnabled(); return ( <> @@ -98,7 +99,7 @@ const OverviewComponent = () => { - {alertsPrivileges?.read && ( + {hasIndexRead && hasKibanaREAD && ( <> diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index 4a951dfff45d7..93fa70ddd9bfb 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -33,7 +33,6 @@ import { import { Storage } from '../../../../src/plugins/kibana_utils/public'; import { initTelemetry } from './common/lib/telemetry'; import { KibanaServices } from './common/lib/kibana/services'; -import { BASE_RAC_ALERTS_API_PATH } from '../../rule_registry/common/constants'; import { APP_ID, @@ -42,7 +41,7 @@ import { APP_PATH, DEFAULT_INDEX_KEY, APP_ICON_SOLUTION, - SERVER_APP_ID, + DETECTION_ENGINE_INDEX_URL, } from '../common/constants'; import { getDeepLinks, updateGlobalNavigation } from './app/deep_links'; @@ -354,14 +353,17 @@ export class Plugin implements IPlugin ({ - useGetUserAlertsPermissions: () => ({ - loading: false, - crud: true, - read: true, - }), -})); - describe('Details Panel Component', () => { const state: State = { ...mockGlobalState }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx index cad6648cd1f38..5ed9398a621e8 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx @@ -29,14 +29,6 @@ jest.mock( }) ); -jest.mock('@kbn/alerts', () => ({ - useGetUserAlertsPermissions: () => ({ - loading: false, - crud: true, - read: true, - }), -})); - jest.mock('../../../../../common/lib/kibana', () => ({ useKibana: () => ({ services: { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx index d20c62348f07f..404127893b11c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx @@ -24,13 +24,6 @@ import { mockTimelines } from '../../../../../common/mock/mock_timelines_plugin' jest.mock('../../../../../common/hooks/use_experimental_features'); const useIsExperimentalFeatureEnabledMock = useIsExperimentalFeatureEnabled as jest.Mock; -jest.mock('@kbn/alerts', () => ({ - useGetUserAlertsPermissions: () => ({ - loading: false, - crud: true, - read: true, - }), -})); jest.mock('../../../../../common/hooks/use_selector'); jest.mock('../../../../../common/lib/kibana', () => ({ useKibana: () => ({ diff --git a/x-pack/plugins/security_solution/server/features.ts b/x-pack/plugins/security_solution/server/features.ts index 5494270d9ad81..cff1e2482a1ee 100644 --- a/x-pack/plugins/security_solution/server/features.ts +++ b/x-pack/plugins/security_solution/server/features.ts @@ -114,7 +114,7 @@ export const getKibanaPrivilegesFeaturePrivileges = (ruleTypes: string[]): Kiban }, alerting: ruleTypes, cases: [APP_ID], - subFeatures: [{ ...CASES_SUB_FEATURE }, { ...getAlertsSubFeature(ruleTypes) }], + subFeatures: [{ ...CASES_SUB_FEATURE } /* , { ...getAlertsSubFeature(ruleTypes) } */], privileges: { all: { app: [APP_ID, 'kibana'], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/__snapshots__/get_signals_template.test.ts.snap b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/__snapshots__/get_signals_template.test.ts.snap index 833a9084fdac6..3c065ab0ac109 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/__snapshots__/get_signals_template.test.ts.snap +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/__snapshots__/get_signals_template.test.ts.snap @@ -6,11 +6,6 @@ Object { "test-index-*", ], "template": Object { - "aliases": Object { - ".alerts-security.alerts-space-id": Object { - "is_write_index": false, - }, - }, "mappings": Object { "_meta": Object { "aliases_version": 1, @@ -1810,10 +1805,6 @@ Object { "path": "signal.rule.building_block_type", "type": "alias", }, - "kibana.alert.rule.consumer": Object { - "type": "constant_keyword", - "value": "siem", - }, "kibana.alert.rule.created_at": Object { "path": "signal.rule.created_at", "type": "alias", @@ -1870,10 +1861,6 @@ Object { "path": "signal.rule.note", "type": "alias", }, - "kibana.alert.rule.producer": Object { - "type": "constant_keyword", - "value": "siem", - }, "kibana.alert.rule.query": Object { "path": "signal.rule.query", "type": "alias", @@ -1902,10 +1889,6 @@ Object { "path": "signal.rule.rule_name_override", "type": "alias", }, - "kibana.alert.rule.rule_type_id": Object { - "type": "constant_keyword", - "value": "siem.signals", - }, "kibana.alert.rule.saved_id": Object { "path": "signal.rule.saved_id", "type": "alias", @@ -2070,10 +2053,6 @@ Object { "path": "signal.status", "type": "alias", }, - "kibana.space_ids": Object { - "type": "constant_keyword", - "value": "space-id", - }, "labels": Object { "type": "object", }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/create_index_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/create_index_route.ts index ab7ff26d9d875..d65a1ad87b41a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/create_index_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/create_index_route.ts @@ -25,7 +25,6 @@ import { buildSiemResponse } from '../utils'; import { createSignalsFieldAliases, getSignalsTemplate, - getRbacRequiredFields, SIGNALS_TEMPLATE_VERSION, SIGNALS_FIELD_ALIASES_VERSION, ALIAS_VERSION_FIELD, @@ -89,7 +88,7 @@ export const createDetectionIndex = async ( ruleDataService: RuleDataPluginService, ruleRegistryEnabled: boolean ): Promise => { - const esClient = context.core.elasticsearch.client.asInternalUser; + const esClient = context.core.elasticsearch.client.asCurrentUser; const spaceId = siemClient.getSpaceId(); if (!siemClient) { @@ -132,11 +131,11 @@ export const createDetectionIndex = async ( // for BOTH the index AND alias name. However, through 7.14 admins only needed permissions for .siem-signals (the index) // and not .alerts-security.alerts (the alias). From the security solution perspective, all .siem-signals--* // indices should have an alias to .alerts-security.alerts- so it's safe to add those aliases as the internal user. - await addIndexAliases({ - esClient: context.core.elasticsearch.client.asInternalUser, - index, - aadIndexAliasName, - }); + // await addIndexAliases({ + // esClient: context.core.elasticsearch.client.asInternalUser, + // index, + // aadIndexAliasName, + // }); const indexVersion = await getIndexVersion(esClient, index); if (isOutdated({ current: indexVersion, target: SIGNALS_TEMPLATE_VERSION })) { await esClient.indices.rollover({ alias: index }); @@ -166,7 +165,7 @@ const addFieldAliasesToIndices = async ({ properties: { ...signalExtraFields, ...fieldAliases, - ...getRbacRequiredFields(spaceId), + // ...getRbacRequiredFields(spaceId), }, _meta: { version: currentVersion, @@ -181,26 +180,26 @@ const addFieldAliasesToIndices = async ({ } }; -const addIndexAliases = async ({ - esClient, - index, - aadIndexAliasName, -}: { - esClient: ElasticsearchClient; - index: string; - aadIndexAliasName: string; -}) => { - const { body: indices } = await esClient.indices.getAlias({ name: index }); - const aliasActions = { - actions: Object.keys(indices).map((concreteIndexName) => { - return { - add: { - index: concreteIndexName, - alias: aadIndexAliasName, - is_write_index: false, - }, - }; - }), - }; - await esClient.indices.updateAliases({ body: aliasActions }); -}; +// const addIndexAliases = async ({ +// esClient, +// index, +// aadIndexAliasName, +// }: { +// esClient: ElasticsearchClient; +// index: string; +// aadIndexAliasName: string; +// }) => { +// const { body: indices } = await esClient.indices.getAlias({ name: index }); +// const aliasActions = { +// actions: Object.keys(indices).map((concreteIndexName) => { +// return { +// add: { +// index: concreteIndexName, +// alias: aadIndexAliasName, +// is_write_index: false, +// }, +// }; +// }), +// }; +// await esClient.indices.updateAliases({ body: aliasActions }); +// }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template.test.ts index 3355b0659f284..bb67dd1fca6df 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template.test.ts @@ -107,12 +107,13 @@ describe('get_signals_template', () => { } }, []); const constantKeywordsFound = recursiveConstantKeywordFound('', template); - expect(constantKeywordsFound).toEqual([ - 'template.mappings.properties.kibana.space_ids', - 'template.mappings.properties.kibana.alert.rule.consumer', - 'template.mappings.properties.kibana.alert.rule.producer', - 'template.mappings.properties.kibana.alert.rule.rule_type_id', - ]); + expect(constantKeywordsFound).toEqual([]); + // expect(constantKeywordsFound).toEqual([ + // 'template.mappings.properties.kibana.space_ids', + // 'template.mappings.properties.kibana.alert.rule.consumer', + // 'template.mappings.properties.kibana.alert.rule.producer', + // 'template.mappings.properties.kibana.alert.rule.rule_type_id', + // ]); }); test('it should match snapshot', () => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template.ts index 38a3612e5861d..3470f955dbdba 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template.ts @@ -48,11 +48,11 @@ export const getSignalsTemplate = (index: string, spaceId: string, aadIndexAlias const template = { index_patterns: [`${index}-*`], template: { - aliases: { - [aadIndexAliasName]: { - is_write_index: false, - }, - }, + // aliases: { + // [aadIndexAliasName]: { + // is_write_index: false, + // }, + // }, settings: { index: { lifecycle: { @@ -72,7 +72,7 @@ export const getSignalsTemplate = (index: string, spaceId: string, aadIndexAlias ...ecsMapping.mappings.properties, ...otherMapping.mappings.properties, ...fieldAliases, - ...getRbacRequiredFields(spaceId), + // ...getRbacRequiredFields(spaceId), signal: signalsMapping.mappings.properties.signal, }, _meta: { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/read_index_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/read_index_route.ts index c36dade4bb9d0..4cfedd5dcaa01 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/read_index_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/read_index_route.ts @@ -30,7 +30,7 @@ export const readIndexRoute = (router: SecuritySolutionPluginRouter, config: Con const siemResponse = buildSiemResponse(response); try { - const esClient = context.core.elasticsearch.client.asInternalUser; + const esClient = context.core.elasticsearch.client.asCurrentUser; const siemClient = context.securitySolution?.getAppClient(); if (!siemClient) { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/open_close_signals_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/open_close_signals_route.ts index bf21f9de037f4..e54cc94b886f6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/open_close_signals_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/open_close_signals_route.ts @@ -6,6 +6,7 @@ */ import { transformError } from '@kbn/securitysolution-es-utils'; +import { ALERT_WORKFLOW_STATUS } from '@kbn/rule-data-utils'; import { setSignalStatusValidateTypeDependents } from '../../../../../common/detection_engine/schemas/request/set_signal_status_type_dependents'; import { SetSignalsStatusSchemaDecoded, @@ -66,7 +67,12 @@ export const setSignalsStatusRoute = (router: SecuritySolutionPluginRouter) => { refresh: true, body: { script: { - source: `ctx._source.signal.status = '${status}'`, + source: `if (ctx._source['${ALERT_WORKFLOW_STATUS}'] != null) { + ctx._source['${ALERT_WORKFLOW_STATUS}'] = '${status}' + } + if (ctx._source.signal != null && ctx._source.signal.status != null) { + ctx._source.signal.status = '${status}' + }`, lang: 'painless', }, query: queryObject, diff --git a/x-pack/plugins/timelines/common/constants.ts b/x-pack/plugins/timelines/common/constants.ts index 0c03682cc8332..262ab841492e3 100644 --- a/x-pack/plugins/timelines/common/constants.ts +++ b/x-pack/plugins/timelines/common/constants.ts @@ -21,3 +21,4 @@ export const FILTER_IN_PROGRESS: AlertStatus = 'in-progress'; export const FILTER_ACKNOWLEDGED: AlertStatus = 'acknowledged'; export const RAC_ALERTS_BULK_UPDATE_URL = '/internal/rac/alerts/bulk_update'; +export const DETECTION_ENGINE_SIGNALS_STATUS_URL = '/api/detection_engine/signals/status'; diff --git a/x-pack/plugins/timelines/common/types/timeline/actions/index.ts b/x-pack/plugins/timelines/common/types/timeline/actions/index.ts index 281a1fcc91799..e85f2eaa12d72 100644 --- a/x-pack/plugins/timelines/common/types/timeline/actions/index.ts +++ b/x-pack/plugins/timelines/common/types/timeline/actions/index.ts @@ -61,6 +61,7 @@ export interface StatusBulkActionsProps { setEventsDeleted: SetEventsDeleted; onUpdateSuccess?: OnUpdateAlertStatusSuccess; onUpdateFailure?: OnUpdateAlertStatusError; + timelineId?: string; } export interface HeaderActionProps { width: number; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/height_hack.ts b/x-pack/plugins/timelines/public/components/t_grid/body/height_hack.ts index 542be06578d6b..47cd1ed92d661 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/height_hack.ts +++ b/x-pack/plugins/timelines/public/components/t_grid/body/height_hack.ts @@ -40,15 +40,17 @@ export const useDataGridHeightHack = (pageSize: number, rowCount: number) => { gridVirtualized && gridVirtualized.children[0].clientHeight !== gridVirtualized.clientHeight // check if it has vertical scroll ) { - setHeight( - height + + setHeight((currHeight) => { + return ( + currHeight + gridVirtualized.children[0].clientHeight - gridVirtualized.clientHeight + MAGIC_GAP - ); + ); + }); } }, TIME_INTERVAL); - }, [pageSize, rowCount, height]); + }, [pageSize, rowCount]); return height; }; diff --git a/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx index 779fddcad2562..e98d9fff04a0c 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx @@ -41,7 +41,7 @@ import { useDeepEqualSelector } from '../../../hooks/use_selector'; import { defaultHeaders } from '../body/column_headers/default_headers'; import { buildCombinedQuery, getCombinedFilterQuery, resolverIsShowing } from '../helpers'; import { tGridActions, tGridSelectors } from '../../../store/t_grid'; -import { useTimelineEvents } from '../../../container'; +import { useTimelineEvents, InspectResponse, Refetch } from '../../../container'; import { StatefulBody } from '../body'; import { SELECTOR_TIMELINE_GLOBAL_CONTAINER, UpdatedFlexGroup, UpdatedFlexItem } from '../styles'; import { Sort } from '../body/sort'; @@ -114,6 +114,7 @@ export interface TGridIntegratedProps { query: Query; renderCellValue: (props: CellValueElementProps) => React.ReactNode; rowRenderers: RowRenderer[]; + setQuery: (inspect: InspectResponse, loading: boolean, refetch: Refetch) => void; sort: Sort[]; start: string; tGridEventRenderedViewEnabled: boolean; @@ -150,6 +151,7 @@ const TGridIntegratedComponent: React.FC = ({ query, renderCellValue, rowRenderers, + setQuery, sort, start, tGridEventRenderedViewEnabled, @@ -269,6 +271,10 @@ const TGridIntegratedComponent: React.FC = ({ } }, [loading]); + useEffect(() => { + setQuery(inspect, loading, refetch); + }, [inspect, loading, refetch, setQuery]); + return ( = ({ {!resolverIsShowing(graphEventId) && additionalFilters} - {tGridEventRenderedViewEnabled && entityType === 'alerts' && ( - - - - )} + {tGridEventRenderedViewEnabled && + ['detections-page', 'detections-rules-details-page'].includes(id) && ( + + + + )} {!graphEventId && graphOverlay == null && ( diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/bulk_actions/alert_status_bulk_actions.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/bulk_actions/alert_status_bulk_actions.tsx index e4ccf1b72529f..be4a75e443494 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/bulk_actions/alert_status_bulk_actions.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/bulk_actions/alert_status_bulk_actions.tsx @@ -120,6 +120,7 @@ export const AlertStatusBulkActionsComponent = React.memo void; +export type Refetch = () => void; export interface TimelineArgs { consumers: Record; diff --git a/x-pack/plugins/timelines/public/container/use_update_alerts.ts b/x-pack/plugins/timelines/public/container/use_update_alerts.ts index 7cce40b59632d..7f42ddc6e8211 100644 --- a/x-pack/plugins/timelines/public/container/use_update_alerts.ts +++ b/x-pack/plugins/timelines/public/container/use_update_alerts.ts @@ -10,7 +10,10 @@ import { CoreStart } from '../../../../../src/core/public'; import { useKibana } from '../../../../../src/plugins/kibana_react/public'; import { AlertStatus } from '../../../timelines/common'; -import { RAC_ALERTS_BULK_UPDATE_URL } from '../../common/constants'; +import { + DETECTION_ENGINE_SIGNALS_STATUS_URL, + RAC_ALERTS_BULK_UPDATE_URL, +} from '../../common/constants'; /** * Update alert status by query @@ -18,25 +21,35 @@ import { RAC_ALERTS_BULK_UPDATE_URL } from '../../common/constants'; * @param status to update to('open' / 'closed' / 'acknowledged') * @param index index to be updated * @param query optional query object to update alerts by query. - * @param ids optional array of alert ids to update. Ignored if query passed. + * * @throws An error if response is not OK */ -export const useUpdateAlertsStatus = (): { +export const useUpdateAlertsStatus = ( + timelineId: string +): { updateAlertStatus: (params: { status: AlertStatus; index: string; - ids?: string[]; - query?: object; + query: object; }) => Promise; } => { const { http } = useKibana().services; return { - updateAlertStatus: async ({ status, index, ids, query }) => { - const { body } = await http.post(RAC_ALERTS_BULK_UPDATE_URL, { - body: JSON.stringify({ index, status, ...(query ? { query } : { ids }) }), - }); - return body; + updateAlertStatus: async ({ status, index, query }) => { + if (['detections-page', 'detections-rules-details-page'].includes(timelineId)) { + return http!.fetch(DETECTION_ENGINE_SIGNALS_STATUS_URL, { + method: 'POST', + body: JSON.stringify({ status, query }), + }); + } else { + const { body } = await http.post(RAC_ALERTS_BULK_UPDATE_URL, { + body: JSON.stringify({ index, status, query }), + }); + return body; + } }, }; }; + +// diff --git a/x-pack/plugins/timelines/public/hooks/use_status_bulk_action_items.tsx b/x-pack/plugins/timelines/public/hooks/use_status_bulk_action_items.tsx index 8fd637767a387..c9269436646ea 100644 --- a/x-pack/plugins/timelines/public/hooks/use_status_bulk_action_items.tsx +++ b/x-pack/plugins/timelines/public/hooks/use_status_bulk_action_items.tsx @@ -26,8 +26,9 @@ export const useStatusBulkActionItems = ({ setEventsDeleted, onUpdateSuccess, onUpdateFailure, + timelineId, }: StatusBulkActionsProps) => { - const { updateAlertStatus } = useUpdateAlertsStatus(); + const { updateAlertStatus } = useUpdateAlertsStatus(timelineId ?? ''); const { addSuccess, addError, addWarning } = useAppToasts(); const onAlertStatusUpdateSuccess = useCallback( diff --git a/x-pack/plugins/timelines/public/mock/t_grid.tsx b/x-pack/plugins/timelines/public/mock/t_grid.tsx index 6e0b9747186d1..3ae1a1d53c207 100644 --- a/x-pack/plugins/timelines/public/mock/t_grid.tsx +++ b/x-pack/plugins/timelines/public/mock/t_grid.tsx @@ -114,6 +114,7 @@ export const tGridIntegratedProps: TGridIntegratedProps = { }, renderCellValue: () => null, rowRenderers: [], + setQuery: () => null, sort: [ { columnId: '@timestamp', diff --git a/x-pack/plugins/timelines/public/plugin.ts b/x-pack/plugins/timelines/public/plugin.ts index 74e1f2b32844a..4b383ce392147 100644 --- a/x-pack/plugins/timelines/public/plugin.ts +++ b/x-pack/plugins/timelines/public/plugin.ts @@ -48,6 +48,13 @@ export class TimelinesPlugin implements Plugin { return getHoverActions(this._store!); }, getTGrid: (props: TGridProps) => { + if (props.type === 'standalone' && this._store) { + const { getState } = this._store; + const state = getState(); + if (state && state.app) { + this._store = undefined; + } + } return getTGridLazy(props, { store: this._store, storage: this._storage, diff --git a/x-pack/plugins/timelines/server/search_strategy/index_fields/index.ts b/x-pack/plugins/timelines/server/search_strategy/index_fields/index.ts index e20d76bdaf625..907907e978123 100644 --- a/x-pack/plugins/timelines/server/search_strategy/index_fields/index.ts +++ b/x-pack/plugins/timelines/server/search_strategy/index_fields/index.ts @@ -65,7 +65,7 @@ export const requestIndexFieldSearch = async ( }); return get(searchResponse, 'body.hits.total.value', 0) > 0; } else { - if (index.startsWith('.alerts-security.alerts')) { + if (index.startsWith('.alerts-observability')) { return indexPatternsFetcherAsInternalUser.getFieldsForWildcard({ pattern: index, }); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 47043c9776ea0..003f25e38177c 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -20528,7 +20528,6 @@ "xpack.securitySolution.detectionEngine.missingPrivilegesCallOut.cannotEditLists": "これらの権限がない場合は、値リストを作成したり編集したりできません。", "xpack.securitySolution.detectionEngine.missingPrivilegesCallOut.cannotEditRules": "その権限がない場合、検出エンジンルールを作製したり編集したりできません。", "xpack.securitySolution.detectionEngine.missingPrivilegesCallOut.messageBody.essenceDescription": "この機能のすべてにアクセスするには、次の権限が必要です。サポートについては、管理者にお問い合わせください。", - "xpack.securitySolution.detectionEngine.missingPrivilegesCallOut.messageBody.missingFeaturePrivileges": "{index}機能の{privileges}権限が不足しています。{explanation}", "xpack.securitySolution.detectionEngine.missingPrivilegesCallOut.messageBody.missingIndexPrivileges": "{index}インデックスの{privileges}権限が不足しています。{explanation}", "xpack.securitySolution.detectionEngine.missingPrivilegesCallOut.messageTitle": "権限が不十分です", "xpack.securitySolution.detectionEngine.mitreAttack.addSubtechniqueTitle": "サブ手法を追加", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index f67c1e155561e..5f0e3b2ece3c1 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -20842,7 +20842,6 @@ "xpack.securitySolution.detectionEngine.missingPrivilegesCallOut.cannotEditLists": "没有这些权限,将无法创建或编辑值列表。", "xpack.securitySolution.detectionEngine.missingPrivilegesCallOut.cannotEditRules": "没有该权限,将无法创建或编辑检测引擎规则。", "xpack.securitySolution.detectionEngine.missingPrivilegesCallOut.messageBody.essenceDescription": "您需要以下权限,才能完全使用此功能。有关进一步帮助,请联系您的管理员。", - "xpack.securitySolution.detectionEngine.missingPrivilegesCallOut.messageBody.missingFeaturePrivileges": "缺失 {privileges} 权限,无法使用 {index} 功能。{explanation}", "xpack.securitySolution.detectionEngine.missingPrivilegesCallOut.messageBody.missingIndexPrivileges": "缺失 {privileges} 权限,无法使用 {index} 索引。{explanation}", "xpack.securitySolution.detectionEngine.missingPrivilegesCallOut.messageTitle": "权限不足", "xpack.securitySolution.detectionEngine.mitreAttack.addSubtechniqueTitle": "添加子技术", diff --git a/x-pack/test/api_integration/apis/security/privileges.ts b/x-pack/test/api_integration/apis/security/privileges.ts index 40a451ffb5cfe..bbb0fc60cb3ce 100644 --- a/x-pack/test/api_integration/apis/security/privileges.ts +++ b/x-pack/test/api_integration/apis/security/privileges.ts @@ -32,16 +32,7 @@ export default function ({ getService }: FtrProviderContext) { actions: ['all', 'read'], stackAlerts: ['all', 'read'], ml: ['all', 'read'], - siem: [ - 'all', - 'read', - 'minimal_all', - 'minimal_read', - 'cases_all', - 'cases_read', - 'alerts_all', - 'alerts_read', - ], + siem: ['all', 'read', 'minimal_all', 'minimal_read', 'cases_all', 'cases_read'], observabilityCases: ['all', 'read'], uptime: ['all', 'read'], infrastructure: ['all', 'read'], diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_index.ts index 8eacd4231a92e..4748e39cd3a46 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_index.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_index.ts @@ -82,15 +82,15 @@ export default ({ getService }: FtrProviderContext) => { expect(body).to.eql({ message: 'index for this space does not exist', status_code: 404 }); }); - it('should be able to create a signal index when it has not been created yet', async () => { + it('should NOT be able to create a signal index when it has not been created yet. Should return a 403 and error that the user is unauthorized', async () => { const { body } = await supertestWithoutAuth .post(DETECTION_ENGINE_INDEX_URL) .set('kbn-xsrf', 'true') .auth(role, 'changeme') .send() - .expect(200); - - expect(body).to.eql({ acknowledged: true }); + .expect(403); + expect(body.message).to.match(/^security_exception/); + expect(body.status_code).to.eql(403); }); it('should be able to read the index name and status as not being outdated', async () => { @@ -103,7 +103,7 @@ export default ({ getService }: FtrProviderContext) => { .send() .expect(200); expect(body).to.eql({ - index_mapping_outdated: false, + index_mapping_outdated: null, name: `${DEFAULT_SIGNALS_INDEX}-default`, }); }); @@ -129,15 +129,15 @@ export default ({ getService }: FtrProviderContext) => { expect(body).to.eql({ message: 'index for this space does not exist', status_code: 404 }); }); - it('should be able to create a signal index when it has not been created yet.', async () => { + it('should NOT be able to create a signal index when it has not been created yet. Should return a 403 and error that the user is unauthorized', async () => { const { body } = await supertestWithoutAuth .post(DETECTION_ENGINE_INDEX_URL) .set('kbn-xsrf', 'true') .auth(role, 'changeme') .send() - .expect(200); - - expect(body).to.eql({ acknowledged: true }); + .expect(403); + expect(body.message).to.match(/^security_exception/); + expect(body.status_code).to.eql(403); }); it('should be able to read the index name and status as not being outdated', async () => { @@ -150,7 +150,7 @@ export default ({ getService }: FtrProviderContext) => { .send() .expect(200); expect(body).to.eql({ - index_mapping_outdated: false, + index_mapping_outdated: null, name: `${DEFAULT_SIGNALS_INDEX}-default`, }); }); @@ -226,15 +226,15 @@ export default ({ getService }: FtrProviderContext) => { expect(body).to.eql({ message: 'index for this space does not exist', status_code: 404 }); }); - it('should be able to create a signal index when it has not been created yet', async () => { + it('should NOT be able to create a signal index when it has not been created yet. Should return a 403 and error that the user is unauthorized', async () => { const { body } = await supertestWithoutAuth .post(DETECTION_ENGINE_INDEX_URL) .set('kbn-xsrf', 'true') .auth(role, 'changeme') .send() - .expect(200); - - expect(body).to.eql({ acknowledged: true }); + .expect(403); + expect(body.message).to.match(/^security_exception/); + expect(body.status_code).to.eql(403); }); it('should be able to read the index name and status as not being outdated', async () => { @@ -272,16 +272,16 @@ export default ({ getService }: FtrProviderContext) => { .expect(404); expect(body).to.eql({ message: 'index for this space does not exist', status_code: 404 }); }); - // here - it('should be able to create a signal index when it has not been created yet', async () => { + + it('should NOT be able to create a signal index when it has not been created yet. Should return a 403 and error that the user is unauthorized', async () => { const { body } = await supertestWithoutAuth .post(DETECTION_ENGINE_INDEX_URL) .set('kbn-xsrf', 'true') .auth(role, 'changeme') .send() - .expect(200); - - expect(body).to.eql({ acknowledged: true }); + .expect(403); + expect(body.message).to.match(/^security_exception/); + expect(body.status_code).to.eql(403); }); it('should be able to read the index name and status as not being outdated', async () => { @@ -294,7 +294,7 @@ export default ({ getService }: FtrProviderContext) => { .send() .expect(200); expect(body).to.eql({ - index_mapping_outdated: false, + index_mapping_outdated: null, name: `${DEFAULT_SIGNALS_INDEX}-default`, }); }); @@ -370,14 +370,15 @@ export default ({ getService }: FtrProviderContext) => { expect(body).to.eql({ message: 'index for this space does not exist', status_code: 404 }); }); - it('should be able to create a signal index when it has not been created yet', async () => { + it('should NOT be able to create a signal index when it has not been created yet. Should return a 401 unauthorized', async () => { const { body } = await supertestWithoutAuth .post(DETECTION_ENGINE_INDEX_URL) .set('kbn-xsrf', 'true') .auth(role, 'changeme') .send() - .expect(200); - expect(body).to.eql({ acknowledged: true }); + .expect(403); + expect(body.message).to.match(/^security_exception/); + expect(body.status_code).to.eql(403); }); it('should be able to read the index name and status as being outdated.', async () => { @@ -416,14 +417,15 @@ export default ({ getService }: FtrProviderContext) => { expect(body).to.eql({ message: 'index for this space does not exist', status_code: 404 }); }); - it('should be able to create a signal index when it has not been created yet', async () => { + it('should NOT be able to create a signal index when it has not been created yet. Should return a 401 unauthorized', async () => { const { body } = await supertestWithoutAuth .post(DETECTION_ENGINE_INDEX_URL) .set('kbn-xsrf', 'true') .auth(role, 'changeme') .send() - .expect(200); - expect(body).to.eql({ acknowledged: true }); + .expect(403); + expect(body.message).to.match(/^security_exception/); + expect(body.status_code).to.eql(403); }); it('should be able to read the index name and status as being outdated.', async () => {