diff --git a/.buildkite/scripts/lifecycle/build_status.js b/.buildkite/scripts/lifecycle/build_status.js index 2c1d51ecac0a7..f2a5024c96013 100644 --- a/.buildkite/scripts/lifecycle/build_status.js +++ b/.buildkite/scripts/lifecycle/build_status.js @@ -7,11 +7,11 @@ const { BuildkiteClient } = require('kibana-buildkite-library'); console.log(status.success ? 'true' : 'false'); process.exit(0); } catch (ex) { + console.error('Buildkite API Error', ex.message); if (ex.response) { - console.error('HTTP Error Response Body', ex.response.data); console.error('HTTP Error Response Status', ex.response.status); + console.error('HTTP Error Response Body', ex.response.data); } - console.error(ex); process.exit(1); } })(); diff --git a/.buildkite/scripts/lifecycle/ci_stats_complete.js b/.buildkite/scripts/lifecycle/ci_stats_complete.js index d86e2ec7efcae..d9411178799ab 100644 --- a/.buildkite/scripts/lifecycle/ci_stats_complete.js +++ b/.buildkite/scripts/lifecycle/ci_stats_complete.js @@ -4,7 +4,11 @@ const { CiStats } = require('kibana-buildkite-library'); try { await CiStats.onComplete(); } catch (ex) { - console.error(ex); + console.error('CI Stats Error', ex.message); + if (ex.response) { + console.error('HTTP Error Response Status', ex.response.status); + console.error('HTTP Error Response Body', ex.response.data); + } process.exit(1); } })(); diff --git a/.buildkite/scripts/lifecycle/ci_stats_start.js b/.buildkite/scripts/lifecycle/ci_stats_start.js index 115aa9bd23954..ec0e4c713499e 100644 --- a/.buildkite/scripts/lifecycle/ci_stats_start.js +++ b/.buildkite/scripts/lifecycle/ci_stats_start.js @@ -4,7 +4,11 @@ const { CiStats } = require('kibana-buildkite-library'); try { await CiStats.onStart(); } catch (ex) { - console.error(ex); + console.error('CI Stats Error', ex.message); + if (ex.response) { + console.error('HTTP Error Response Status', ex.response.status); + console.error('HTTP Error Response Body', ex.response.data); + } process.exit(1); } })(); diff --git a/docs/apm/correlations.asciidoc b/docs/apm/correlations.asciidoc index 45781228cd200..c0c18433c9021 100644 --- a/docs/apm/correlations.asciidoc +++ b/docs/apm/correlations.asciidoc @@ -12,7 +12,7 @@ piece of hardware, like a host or pod. Or, perhaps a set of users, based on IP address or region, is facing increased latency due to local data center issues. To find correlations, select a service on the *Services* page in the {apm-app} -and click **View correlations**. +then select a transaction group from the *Transactions* tab. NOTE: Queries within the {apm-app} are also applied to the correlations. @@ -20,26 +20,25 @@ NOTE: Queries within the {apm-app} are also applied to the correlations. [[correlations-latency]] ==== Find high transaction latency correlations -The correlations on the *Latency* tab help you discover which attributes are -contributing to increased transaction latency. +The correlations on the *Latency correlations* tab help you discover which +attributes are contributing to increased transaction latency. [role="screenshot"] image::apm/images/correlations-hover.png[Latency correlations] The progress bar indicates the status of the asynchronous analysis, which performs statistical searches across a large number of attributes. For large -time ranges and services with high transaction throughput this might take some -time. To improve performance, reduce the time range on the service overview -page. +time ranges and services with high transaction throughput, this might take some +time. To improve performance, reduce the time range. The latency distribution chart visualizes the overall latency of the -transactions in the service. If there are attributes that have a statistically -significant correlation with slow response times, they are listed in a table -below the chart. The table is sorted by correlation coefficients that range from -0 to 1. Attributes with higher correlation values are more likely to contribute -to high latency transactions. By default, the attribute with the highest -correlation value is added to the chart. To see the latency distribution for -other attributes, hover over their row in the table. +transactions in the transaction group. If there are attributes that have a +statistically significant correlation with slow response times, they are listed +in a table below the chart. The table is sorted by correlation coefficients that +range from 0 to 1. Attributes with higher correlation values are more likely to +contribute to high latency transactions. By default, the attribute with the +highest correlation value is added to the chart. To see the latency distribution +for other attributes, hover over their row in the table. If a correlated attribute seems noteworthy, use the **Filter** quick links: diff --git a/docs/apm/images/correlations-hover.png b/docs/apm/images/correlations-hover.png index c8d5622156b4c..80c1fa41adbdf 100644 Binary files a/docs/apm/images/correlations-hover.png and b/docs/apm/images/correlations-hover.png differ 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/docs/settings/reporting-settings.asciidoc b/docs/settings/reporting-settings.asciidoc index 0d33f80f1a136..2ea5e4f31bf1b 100644 --- a/docs/settings/reporting-settings.asciidoc +++ b/docs/settings/reporting-settings.asciidoc @@ -283,7 +283,7 @@ With Security enabled, Reporting has two forms of access control: each user can [NOTE] ============================================================================ -The `xpack.reporting.roles` settings are for a deprecated system of access control in Reporting. It does not allow API Keys to generate reports, and it doesn't allow {kib} application privileges. We recommend you explicitly turn off reporting's deprecated access control feature by adding `xpack.reporting.roles.enabled: false` in kibana.yml. This will enable application privileges for reporting, as described in <>. +The `xpack.reporting.roles` settings are for a deprecated system of access control in Reporting. It does not allow API Keys to generate reports, and it doesn't allow {kib} application privileges. We recommend you explicitly turn off reporting's deprecated access control feature by adding `xpack.reporting.roles.enabled: false` in kibana.yml. This will enable you to create custom roles that provide application privileges for reporting, as described in <>. ============================================================================ [[xpack-reporting-roles-enabled]] `xpack.reporting.roles.enabled`:: diff --git a/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts b/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts index 7847cad0fd5e7..0584ee27aa5f6 100644 --- a/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts +++ b/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts @@ -221,11 +221,12 @@ export class CiStatsReporter { ? `${error.response.status} response` : 'no response'; + const seconds = attempt * 10; this.log.warning( - `failed to reach ci-stats service [reason=${reason}], retrying in ${attempt} seconds` + `failed to reach ci-stats service, retrying in ${seconds} seconds, [reason=${reason}], [error=${error.message}]` ); - await new Promise((resolve) => setTimeout(resolve, attempt * 1000)); + await new Promise((resolve) => setTimeout(resolve, seconds * 1000)); } } } diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index e96d875f3f024..38e8b7a87296a 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -59522,8 +59522,9 @@ class CiStatsReporter { const reason = error !== null && error !== void 0 && (_error$response = error.response) !== null && _error$response !== void 0 && _error$response.status ? `${error.response.status} response` : 'no response'; - this.log.warning(`failed to reach ci-stats service [reason=${reason}], retrying in ${attempt} seconds`); - await new Promise(resolve => setTimeout(resolve, attempt * 1000)); + const seconds = attempt * 10; + this.log.warning(`failed to reach ci-stats service, retrying in ${seconds} seconds, [reason=${reason}], [error=${error.message}]`); + await new Promise(resolve => setTimeout(resolve, seconds * 1000)); } } } diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index ff0542232e986..2f7d0c5689e8e 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`, @@ -570,6 +571,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 d3f9ce71379b7..b8bd91c609b86 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -607,6 +607,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/alerting/server/health/get_state.test.ts b/x-pack/plugins/alerting/server/health/get_state.test.ts index b58a127941880..9429dcc07d927 100644 --- a/x-pack/plugins/alerting/server/health/get_state.test.ts +++ b/x-pack/plugins/alerting/server/health/get_state.test.ts @@ -216,8 +216,8 @@ describe('getHealthServiceStatusWithRetryAndErrorHandling', () => { }) ).toPromise(); - expect(status.level).toEqual(ServiceStatusLevels.unavailable); - expect(status.summary).toEqual('Alerting framework is unavailable'); + expect(status.level).toEqual(ServiceStatusLevels.degraded); + expect(status.summary).toEqual('Alerting framework is degraded'); expect(status.meta).toBeUndefined(); }); @@ -275,8 +275,8 @@ describe('getHealthServiceStatusWithRetryAndErrorHandling', () => { }), retryDelay ).subscribe((status) => { - expect(status.level).toEqual(ServiceStatusLevels.unavailable); - expect(status.summary).toEqual('Alerting framework is unavailable'); + expect(status.level).toEqual(ServiceStatusLevels.degraded); + expect(status.summary).toEqual('Alerting framework is degraded'); expect(status.meta).toEqual({ error: err }); }); diff --git a/x-pack/plugins/alerting/server/health/get_state.ts b/x-pack/plugins/alerting/server/health/get_state.ts index 255037d7015a2..34f897ad5b73c 100644 --- a/x-pack/plugins/alerting/server/health/get_state.ts +++ b/x-pack/plugins/alerting/server/health/get_state.ts @@ -73,9 +73,7 @@ const getHealthServiceStatus = async ( const level = doc.state?.health_status === HealthStatus.OK ? ServiceStatusLevels.available - : doc.state?.health_status === HealthStatus.Warning - ? ServiceStatusLevels.degraded - : ServiceStatusLevels.unavailable; + : ServiceStatusLevels.degraded; return { level, summary: LEVEL_SUMMARY[level.toString()], @@ -102,10 +100,10 @@ export const getHealthServiceStatusWithRetryAndErrorHandling = ( ); }), catchError((error) => { - logger.warn(`Alerting framework is unavailable due to the error: ${error}`); + logger.warn(`Alerting framework is degraded due to the error: ${error}`); return of({ - level: ServiceStatusLevels.unavailable, - summary: LEVEL_SUMMARY[ServiceStatusLevels.unavailable.toString()], + level: ServiceStatusLevels.degraded, + summary: LEVEL_SUMMARY[ServiceStatusLevels.degraded.toString()], meta: { error }, }); }) diff --git a/x-pack/plugins/alerting/server/plugin.ts b/x-pack/plugins/alerting/server/plugin.ts index 29041af3b4671..bf3eecbb131e1 100644 --- a/x-pack/plugins/alerting/server/plugin.ts +++ b/x-pack/plugins/alerting/server/plugin.ts @@ -239,7 +239,7 @@ export class AlertingPlugin { ); const serviceStatus$ = new BehaviorSubject({ - level: ServiceStatusLevels.unavailable, + level: ServiceStatusLevels.degraded, summary: 'Alerting is initializing', }); core.status.set(serviceStatus$); diff --git a/x-pack/plugins/apm/public/components/app/correlations/latency_correlations_help_popover.tsx b/x-pack/plugins/apm/public/components/app/correlations/latency_correlations_help_popover.tsx index 64adf0e11fca5..97bd13ebe37b5 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/latency_correlations_help_popover.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/latency_correlations_help_popover.tsx @@ -39,7 +39,7 @@ export function LatencyCorrelationsHelpPopover() {

diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/index.tsx index 0c6f03047dc7d..ca4f5941ac845 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/index.tsx @@ -11,6 +11,8 @@ import { useBreadcrumb } from '../../../context/breadcrumbs/use_breadcrumb'; import { ChartPointerEventContextProvider } from '../../../context/chart_pointer_event/chart_pointer_event_context'; import { useApmParams } from '../../../hooks/use_apm_params'; import { useApmRouter } from '../../../hooks/use_apm_router'; +import { useFallbackToTransactionsFetcher } from '../../../hooks/use_fallback_to_transactions_fetcher'; +import { AggregatedTransactionsBadge } from '../../shared/aggregated_transactions_badge'; import { TransactionCharts } from '../../shared/charts/transaction_charts'; import { TransactionDetailsTabs } from './transaction_details_tabs'; @@ -31,8 +33,14 @@ export function TransactionDetails() { }), }); + const { kuery } = query; + const { fallbackToTransactions } = useFallbackToTransactionsFetcher({ + kuery, + }); + return ( <> + {fallbackToTransactions && } 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/public/components/create/form.test.tsx b/x-pack/plugins/cases/public/components/create/form.test.tsx index 9c3071fe27ee5..c9d087a08ba2c 100644 --- a/x-pack/plugins/cases/public/components/create/form.test.tsx +++ b/x-pack/plugins/cases/public/components/create/form.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { mount } from 'enzyme'; -import { act, waitFor } from '@testing-library/react'; +import { act, render, waitFor } from '@testing-library/react'; import { useForm, Form, FormHook } from '../../common/shared_imports'; import { useGetTags } from '../../containers/use_get_tags'; @@ -119,4 +119,14 @@ describe('CreateCaseForm', () => { }); }); }); + + it('hides the sync alerts toggle', () => { + const { queryByText } = render( + + + + ); + + expect(queryByText('Sync alert')).not.toBeInTheDocument(); + }); }); diff --git a/x-pack/plugins/cases/public/components/create/form.tsx b/x-pack/plugins/cases/public/components/create/form.tsx index cbd4fd7654259..d9b9287534601 100644 --- a/x-pack/plugins/cases/public/components/create/form.tsx +++ b/x-pack/plugins/cases/public/components/create/form.tsx @@ -53,7 +53,6 @@ export const CreateCaseForm: React.FC = React.memo( withSteps = true, }) => { const { isSubmitting } = useFormContext(); - const firstStep = useMemo( () => ({ title: i18n.STEP_ONE_TITLE, 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/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx index bdcfb1ebb6534..677ca545029fe 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx @@ -220,7 +220,7 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps const hasUpgrade = !!updatableIntegrationRecord && updatableIntegrationRecord.policiesToUpgrade.some( - ({ id }) => id === packagePolicy.policy_id + ({ pkgPolicyId }) => pkgPolicyId === packagePolicy.id ); return ( diff --git a/x-pack/plugins/maps/public/classes/layers/new_vector_layer_wizard/wizard.tsx b/x-pack/plugins/maps/public/classes/layers/new_vector_layer_wizard/wizard.tsx index 6dac22581efdb..100e9dfa45c1d 100644 --- a/x-pack/plugins/maps/public/classes/layers/new_vector_layer_wizard/wizard.tsx +++ b/x-pack/plugins/maps/public/classes/layers/new_vector_layer_wizard/wizard.tsx @@ -13,13 +13,14 @@ import { RenderWizardArguments } from '../layer_wizard_registry'; import { VectorLayer } from '../vector_layer'; import { ESSearchSource } from '../../sources/es_search_source'; import { ADD_LAYER_STEP_ID } from '../../../connected_components/add_layer_panel/view'; -import { getIndexNameFormComponent } from '../../../kibana_services'; +import { getFileUpload, getIndexNameFormComponent } from '../../../kibana_services'; interface State { indexName: string; indexNameError: string; indexingTriggered: boolean; createIndexError: string; + userHasIndexWritePermissions: boolean; } const DEFAULT_MAPPINGS = { @@ -43,6 +44,7 @@ export class NewVectorLayerEditor extends Component { let indexPatternId: string | undefined; try { + const userHasIndexWritePermissions = await this._checkIndexPermissions(); + if (!userHasIndexWritePermissions) { + this._setCreateIndexError( + i18n.translate('xpack.maps.layers.newVectorLayerWizard.indexPermissionsError', { + defaultMessage: `You must have 'create' and 'create_index' index privileges to create and write data to "{indexName}".`, + values: { + indexName: this.state.indexName, + }, + }), + userHasIndexWritePermissions + ); + return; + } const response = await createNewIndexAndPattern({ indexName: this.state.indexName, defaultMappings: DEFAULT_MAPPINGS, @@ -125,10 +149,23 @@ export class NewVectorLayerEditor extends Component +

{this.state.createIndexError}

+ + ); + } return ( ({ - useKibana: () => ({ - services: { - cases: { - getCreateCase: jest.fn(), - }, - }, - }), -})); const onCloseFlyout = jest.fn(); const onSuccess = jest.fn(); const defaultProps = { @@ -28,8 +25,17 @@ const defaultProps = { }; describe('CreateCaseFlyout', () => { + const mockCreateCase = jest.fn(); + beforeEach(() => { jest.resetAllMocks(); + (useKibana as jest.Mock).mockReturnValue({ + services: { + cases: { + getCreateCase: mockCreateCase, + }, + }, + }); }); it('renders', () => { @@ -52,4 +58,22 @@ describe('CreateCaseFlyout', () => { wrapper.find(`[data-test-subj='euiFlyoutCloseButton']`).first().simulate('click'); expect(onCloseFlyout).toBeCalled(); }); + + it('does not show the sync alerts toggle', () => { + render( + + + + ); + + expect(mockCreateCase).toBeCalledTimes(1); + expect(mockCreateCase).toBeCalledWith({ + onCancel: onCloseFlyout, + onSuccess, + afterCaseCreated: undefined, + withSteps: false, + owner: [CASES_OWNER], + disableAlerts: true, + }); + }); }); diff --git a/x-pack/plugins/observability/public/components/app/cases/create/flyout.tsx b/x-pack/plugins/observability/public/components/app/cases/create/flyout.tsx index df29d02e8d830..896bc27a97674 100644 --- a/x-pack/plugins/observability/public/components/app/cases/create/flyout.tsx +++ b/x-pack/plugins/observability/public/components/app/cases/create/flyout.tsx @@ -68,6 +68,7 @@ function CreateCaseFlyoutComponent({ onSuccess, withSteps: false, owner: [CASES_OWNER], + disableAlerts: true, })} 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/integration/header/navigation.spec.ts b/x-pack/plugins/security_solution/cypress/integration/header/navigation.spec.ts index ca7c89e99719a..d91380a202b7b 100644 --- a/x-pack/plugins/security_solution/cypress/integration/header/navigation.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/header/navigation.spec.ts @@ -26,7 +26,6 @@ import { ALERTS_URL, CASES_URL, HOSTS_URL, - KIBANA_HOME, ENDPOINTS_URL, TRUSTED_APPS_URL, EVENT_FILTERS_URL, @@ -113,7 +112,7 @@ describe('top-level navigation common to all pages in the Security app', () => { describe.skip('Kibana navigation to all pages in the Security app ', () => { before(() => { - loginAndWaitForPage(KIBANA_HOME); + loginAndWaitForPage(TIMELINES_URL); }); beforeEach(() => { openKibanaNavigation(); 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 82c556c3b40d9..b64738c6696fc 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/event_details/alert_summary_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.tsx index da6c091ab069a..03c420914d170 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.tsx @@ -36,7 +36,7 @@ import { AlertSummaryRow, getSummaryColumns, SummaryRow } from './helpers'; import { useRuleWithFallback } from '../../../detections/containers/detection_engine/rules/use_rule_with_fallback'; import { MarkdownRenderer } from '../markdown_editor'; import { LineClamp } from '../line_clamp'; -import { endpointAlertCheck } from '../../utils/endpoint_alert_check'; +import { isAlertFromEndpointEvent } from '../../utils/endpoint_alert_check'; import { getEmptyValue } from '../empty_value'; import { ActionCell } from './table/action_cell'; import { FieldValueCell } from './table/field_value_cell'; @@ -244,7 +244,7 @@ export const getSummaryRows = ({ fieldFromBrowserField: browserField, }; - if (item.id === 'agent.id' && !endpointAlertCheck({ data })) { + if (item.id === 'agent.id' && !isAlertFromEndpointEvent({ data })) { return acc; } 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/page/index.tsx b/x-pack/plugins/security_solution/public/common/components/page/index.tsx index 051c1bd8ae5cb..9cfd0771425ee 100644 --- a/x-pack/plugins/security_solution/public/common/components/page/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/page/index.tsx @@ -31,6 +31,15 @@ export const AppGlobalStyle = createGlobalStyle<{ theme: { eui: { euiColorPrimar z-index: 9950 !important; } + /* + overrides the default styling of EuiDataGrid expand popover footer to + make it a column of actions instead of the default actions row + */ + .euiDataGridRowCell__popover .euiPopoverFooter .euiFlexGroup { + flex-direction: column; + align-items: flex-start; + } + /* overrides the default styling of euiComboBoxOptionsList because it's implemented as a popover, so it's not selectable as a child of the styled component 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/common/lib/cell_actions/default_cell_actions.tsx b/x-pack/plugins/security_solution/public/common/lib/cell_actions/default_cell_actions.tsx index 1481ae3e4248c..ae62c214d7b7a 100644 --- a/x-pack/plugins/security_solution/public/common/lib/cell_actions/default_cell_actions.tsx +++ b/x-pack/plugins/security_solution/public/common/lib/cell_actions/default_cell_actions.tsx @@ -116,6 +116,36 @@ export const defaultCellActions: TGridCellAction[] = [ fieldName: columnId, }); + return ( + <> + {timelines.getHoverActions().getCopyButton({ + Component, + field: columnId, + isHoverAction: false, + ownFocus: false, + showTooltip: false, + value, + })} + + ); + }, + ({ data, pageSize }: { data: TimelineNonEcsData[][]; pageSize: number }) => ({ + rowIndex, + columnId, + Component, + }) => { + const { timelines } = useKibanaServices(); + + const pageRowIndex = getPageRowIndex(rowIndex, pageSize); + if (pageRowIndex >= data.length) { + return null; + } + + const value = getMappedNonEcsValue({ + data: data[pageRowIndex], + fieldName: columnId, + }); + const dataProvider: DataProvider[] = useMemo( () => value?.map((x) => ({ @@ -170,58 +200,30 @@ export const defaultCellActions: TGridCellAction[] = [ fieldName: columnId, }); - return ( - <> - {allowTopN({ + const showButton = useMemo( + () => + allowTopN({ browserField: getAllFieldsByName(browserFields)[columnId], fieldName: columnId, hideTopN: false, - }) && ( - - )} - + }), + [columnId] ); - }, - ({ data, pageSize }: { data: TimelineNonEcsData[][]; pageSize: number }) => ({ - rowIndex, - columnId, - Component, - }) => { - const { timelines } = useKibanaServices(); - - const pageRowIndex = getPageRowIndex(rowIndex, pageSize); - if (pageRowIndex >= data.length) { - return null; - } - const value = getMappedNonEcsValue({ - data: data[pageRowIndex], - fieldName: columnId, - }); - - return ( - <> - {timelines.getHoverActions().getCopyButton({ - Component, - field: columnId, - isHoverAction: false, - ownFocus: false, - showTooltip: false, - value, - })} - - ); + return showButton ? ( + + ) : null; }, ]; diff --git a/x-pack/plugins/security_solution/public/common/utils/endpoint_alert_check.test.ts b/x-pack/plugins/security_solution/public/common/utils/endpoint_alert_check.test.ts index e95f5c15d4ecb..d0a03d62a682b 100644 --- a/x-pack/plugins/security_solution/public/common/utils/endpoint_alert_check.test.ts +++ b/x-pack/plugins/security_solution/public/common/utils/endpoint_alert_check.test.ts @@ -6,10 +6,11 @@ */ import _ from 'lodash'; +import { Ecs } from '../../../common/ecs'; import { generateMockDetailItemData } from '../mock'; -import { endpointAlertCheck } from './endpoint_alert_check'; +import { isAlertFromEndpointAlert, isAlertFromEndpointEvent } from './endpoint_alert_check'; -describe('Endpoint Alert Check Utility', () => { +describe('isAlertFromEndpointEvent', () => { let mockDetailItemData: ReturnType; beforeEach(() => { @@ -38,16 +39,47 @@ describe('Endpoint Alert Check Utility', () => { }); it('should return true if detections data comes from an endpoint rule', () => { - expect(endpointAlertCheck({ data: mockDetailItemData })).toBe(true); + expect(isAlertFromEndpointEvent({ data: mockDetailItemData })).toBe(true); }); it('should return false if it is not an Alert (ex. maybe an event)', () => { _.remove(mockDetailItemData, { field: 'signal.rule.id' }); - expect(endpointAlertCheck({ data: mockDetailItemData })).toBeFalsy(); + expect(isAlertFromEndpointEvent({ data: mockDetailItemData })).toBeFalsy(); }); it('should return false if it is not an endpoint agent', () => { _.remove(mockDetailItemData, { field: 'agent.type' }); - expect(endpointAlertCheck({ data: mockDetailItemData })).toBeFalsy(); + expect(isAlertFromEndpointEvent({ data: mockDetailItemData })).toBeFalsy(); + }); +}); + +describe('isAlertFromEndpointAlert', () => { + it('should return true if detections data comes from an endpoint rule', () => { + const mockEcsData = { + _id: 'mockId', + 'signal.original_event.module': ['endpoint'], + 'signal.original_event.kind': ['alert'], + } as Ecs; + expect(isAlertFromEndpointAlert({ ecsData: mockEcsData })).toBe(true); + }); + + it('should return false if ecsData is undefined', () => { + expect(isAlertFromEndpointAlert({ ecsData: undefined })).toBeFalsy(); + }); + + it('should return false if it is not an Alert', () => { + const mockEcsData = { + _id: 'mockId', + 'signal.original_event.module': ['endpoint'], + } as Ecs; + expect(isAlertFromEndpointAlert({ ecsData: mockEcsData })).toBeFalsy(); + }); + + it('should return false if it is not an endpoint module', () => { + const mockEcsData = { + _id: 'mockId', + 'signal.original_event.kind': ['alert'], + } as Ecs; + expect(isAlertFromEndpointAlert({ ecsData: mockEcsData })).toBeFalsy(); }); }); diff --git a/x-pack/plugins/security_solution/public/common/utils/endpoint_alert_check.ts b/x-pack/plugins/security_solution/public/common/utils/endpoint_alert_check.ts index 30c6e3fdeb672..7e7e7a6bcdd1f 100644 --- a/x-pack/plugins/security_solution/public/common/utils/endpoint_alert_check.ts +++ b/x-pack/plugins/security_solution/public/common/utils/endpoint_alert_check.ts @@ -5,15 +5,20 @@ * 2.0. */ -import { find, some } from 'lodash/fp'; +import { find, getOr, some } from 'lodash/fp'; import { TimelineEventsDetailsItem } from '../../../../timelines/common'; +import { Ecs } from '../../../common/ecs'; /** * Checks to see if the given set of Timeline event detail items includes data that indicates its * an endpoint Alert. Note that it will NOT match on Events - only alerts * @param data */ -export const endpointAlertCheck = ({ data }: { data: TimelineEventsDetailsItem[] }): boolean => { +export const isAlertFromEndpointEvent = ({ + data, +}: { + data: TimelineEventsDetailsItem[]; +}): boolean => { const isAlert = some({ category: 'signal', field: 'signal.rule.id' }, data); if (!isAlert) { @@ -23,3 +28,18 @@ export const endpointAlertCheck = ({ data }: { data: TimelineEventsDetailsItem[] const findEndpointAlert = find({ field: 'agent.type' }, data)?.values; return findEndpointAlert ? findEndpointAlert[0] === 'endpoint' : false; }; + +export const isAlertFromEndpointAlert = ({ + ecsData, +}: { + ecsData: Ecs | null | undefined; +}): boolean => { + if (ecsData == null) { + return false; + } + + const eventModules = getOr([], 'signal.original_event.module', ecsData); + const kinds = getOr([], 'signal.original_event.kind', ecsData); + + return eventModules.includes('endpoint') && kinds.includes('alert'); +}; 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/alert_context_menu.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx index cc719a9999383..f2297b7d567bc 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx @@ -11,7 +11,7 @@ import { EuiButtonIcon, EuiContextMenuPanel, EuiPopover, EuiToolTip } from '@ela import { indexOf } from 'lodash'; import { ExceptionListType } from '@kbn/securitysolution-io-ts-list-types'; -import { get, getOr } from 'lodash/fp'; +import { get } from 'lodash/fp'; import { buildGetAlertByIdQuery } from '../../../../common/components/exceptions/helpers'; import { EventsTdContent } from '../../../../timelines/components/timeline/styles'; import { DEFAULT_ICON_BUTTON_WIDTH } from '../../../../timelines/components/timeline/helpers'; @@ -36,6 +36,7 @@ import { useInvestigateInResolverContextItem } from './investigate_in_resolver'; import { ATTACH_ALERT_TO_CASE_FOR_ROW } from '../../../../timelines/components/timeline/body/translations'; import { useEventFilterAction } from './use_event_filter_action'; import { useAddToCaseActions } from './use_add_to_case_actions'; +import { isAlertFromEndpointAlert } from '../../../../common/utils/endpoint_alert_check'; interface AlertContextMenuProps { ariaLabel?: string; @@ -78,17 +79,6 @@ const AlertContextMenuComponent: React.FC = ({ const isEvent = useMemo(() => indexOf(ecsRowData.event?.kind, 'event') !== -1, [ecsRowData]); - const isEndpointAlert = useMemo((): boolean => { - if (ecsRowData == null) { - return false; - } - - const eventModules = getOr([], 'signal.original_event.module', ecsRowData); - const kinds = getOr([], 'signal.original_event.kind', ecsRowData); - - return eventModules.includes('endpoint') && kinds.includes('alert'); - }, [ecsRowData]); - const onButtonClick = useCallback(() => { setPopover(!isPopoverOpen); }, [isPopoverOpen]); @@ -153,7 +143,7 @@ const AlertContextMenuComponent: React.FC = ({ }, [closePopover, onAddEventFilterClick]); const { exceptionActionItems } = useExceptionActions({ - isEndpointAlert, + isEndpointAlert: isAlertFromEndpointAlert({ ecsData: ecsRowData }), onAddExceptionTypeClick: handleOnAddExceptionTypeClick, }); const investigateInResolverActionItems = useInvestigateInResolverContextItem({ 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/host_isolation/use_host_isolation_action.tsx b/x-pack/plugins/security_solution/public/detections/components/host_isolation/use_host_isolation_action.tsx index e670c000d789a..24bc670a13ec4 100644 --- a/x-pack/plugins/security_solution/public/detections/components/host_isolation/use_host_isolation_action.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/host_isolation/use_host_isolation_action.tsx @@ -10,7 +10,7 @@ import type { TimelineEventsDetailsItem } from '../../../../common'; import { isIsolationSupported } from '../../../../common/endpoint/service/host_isolation/utils'; import { HostStatus } from '../../../../common/endpoint/types'; import { useIsolationPrivileges } from '../../../common/hooks/endpoint/use_isolate_privileges'; -import { endpointAlertCheck } from '../../../common/utils/endpoint_alert_check'; +import { isAlertFromEndpointEvent } from '../../../common/utils/endpoint_alert_check'; import { useHostIsolationStatus } from '../../containers/detection_engine/alerts/use_host_isolation_status'; import { ISOLATE_HOST, UNISOLATE_HOST } from './translations'; import { getFieldValue } from './helpers'; @@ -29,7 +29,7 @@ export const useHostIsolationAction = ({ onAddIsolationStatusClick, }: UseHostIsolationActionProps) => { const isEndpointAlert = useMemo(() => { - return endpointAlertCheck({ data: detailsData || [] }); + return isAlertFromEndpointEvent({ data: detailsData || [] }); }, [detailsData]); const agentId = useMemo( 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..d98b168f209da 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,17 +24,20 @@ 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) }; + return { + isAlertFromEndpointAlert: jest.fn().mockReturnValue(true), + isAlertFromEndpointEvent: jest.fn().mockReturnValue(true), + }; }); jest.mock('../../../../common/endpoint/service/host_isolation/utils', () => { diff --git a/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx b/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx index a6114884b955d..0432e7d353086 100644 --- a/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx @@ -20,7 +20,7 @@ import { useHostIsolationAction } from '../host_isolation/use_host_isolation_act import { getFieldValue } from '../host_isolation/helpers'; import type { Ecs } from '../../../../common/ecs'; import { Status } from '../../../../common/detection_engine/schemas/common/schemas'; -import { endpointAlertCheck } from '../../../common/utils/endpoint_alert_check'; +import { isAlertFromEndpointAlert } from '../../../common/utils/endpoint_alert_check'; import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features'; import { useAddToCaseActions } from '../alerts_table/timeline_actions/use_add_to_case_actions'; @@ -87,13 +87,6 @@ export const TakeActionDropdown = React.memo( ]); const isEvent = actionsData.eventKind === 'event'; - const isEndpointAlert = useMemo((): boolean => { - if (detailsData == null) { - return false; - } - return endpointAlertCheck({ data: detailsData }); - }, [detailsData]); - const togglePopoverHandler = useCallback(() => { setIsPopoverOpen(!isPopoverOpen); }, [isPopoverOpen]); @@ -131,7 +124,7 @@ export const TakeActionDropdown = React.memo( ); const { exceptionActionItems } = useExceptionActions({ - isEndpointAlert, + isEndpointAlert: isAlertFromEndpointAlert({ ecsData }), onAddExceptionTypeClick: handleOnAddExceptionTypeClick, }); 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/management/pages/endpoint_hosts/store/middleware.test.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts index 96314ca154d1f..731859263b07d 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts @@ -270,22 +270,15 @@ describe('endpoint list middleware', () => { it('should set ActivityLog state to loading', async () => { dispatchUserChangedUrl(); - dispatchGetActivityLogLoading(); const loadingDispatched = waitForAction('endpointDetailsActivityLogChanged', { validate(action) { return isLoadingResourceState(action.payload); }, }); + dispatchGetActivityLogLoading(); const loadingDispatchedResponse = await loadingDispatched; - expect(mockedApis.responseProvider.activityLogResponse).toHaveBeenCalledWith({ - path: expect.any(String), - query: { - page: 1, - page_size: 50, - }, - }); expect(loadingDispatchedResponse.payload.type).toEqual('LoadingResourceState'); }); @@ -327,6 +320,25 @@ describe('endpoint list middleware', () => { expect(failedAction.error).toBe(apiError); }); + it('should not call API again if it fails', async () => { + dispatchUserChangedUrl(); + + const apiError = new Error('oh oh'); + const failedDispatched = waitForAction('endpointDetailsActivityLogChanged', { + validate(action) { + return isFailedResourceState(action.payload); + }, + }); + + mockedApis.responseProvider.activityLogResponse.mockImplementation(() => { + throw apiError; + }); + + await failedDispatched; + + expect(mockedApis.responseProvider.activityLogResponse).toHaveBeenCalledTimes(1); + }); + it('should not fetch Activity Log with invalid date ranges', async () => { dispatchUserChangedUrl(); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts index 1be9ff5be55ef..df4361a6048a8 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts @@ -33,11 +33,13 @@ import { getActivityLogData, getActivityLogDataPaging, getLastLoadedActivityLogData, + getActivityLogError, detailsData, getIsEndpointPackageInfoUninitialized, getIsOnEndpointDetailsActivityLog, getMetadataTransformStats, isMetadataTransformStatsLoading, + getActivityLogIsUninitializedOrHasSubsequentAPIError, } from './selectors'; import { AgentIdsPendingActions, @@ -124,7 +126,8 @@ export const endpointMiddlewareFactory: ImmutableMiddlewareFactory { - const pagingOptions = + const updatedActivityLog = action.payload.type === 'LoadedResourceState' ? { ...state.endpointDetails.activityLog, @@ -53,7 +53,7 @@ const handleEndpointDetailsActivityLogChanged: CaseReducer +) => boolean = createSelector(getActivityLogData, (activityLog) => + isUninitialisedResourceState(activityLog) +); + export const getActivityLogRequestLoading: ( state: Immutable ) => boolean = createSelector(getActivityLogData, (activityLog) => @@ -413,6 +419,19 @@ export const getActivityLogError: ( } }); +// returns a true if either lgo is uninitialised +// or if it has failed an api call after having fetched a non empty log list earlier +export const getActivityLogIsUninitializedOrHasSubsequentAPIError: ( + state: Immutable +) => boolean = createSelector( + getActivityLogUninitialized, + getLastLoadedActivityLogData, + getActivityLogError, + (isUninitialized, lastLoadedLogData, isAPIError) => { + return isUninitialized || (!isAPIError && !!lastLoadedLogData?.data.length); + } +); + export const getIsEndpointHostIsolated = createSelector(detailsData, (details) => { return (details && isEndpointHostIsolated(details)) || false; }); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/endpoint_details_tabs.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/endpoint_details_tabs.tsx index a6e571dbd7df9..8f044959f4c90 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/endpoint_details_tabs.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/endpoint_details_tabs.tsx @@ -5,13 +5,10 @@ * 2.0. */ -import { useDispatch } from 'react-redux'; -import React, { memo, useCallback, useMemo } from 'react'; -import { EuiTab, EuiTabs, EuiFlyoutBody, EuiTabbedContentTab, EuiSpacer } from '@elastic/eui'; +import React, { memo, useMemo } from 'react'; +import { EuiTab, EuiTabs, EuiFlyoutBody, EuiSpacer } from '@elastic/eui'; import { EndpointIndexUIQueryParams } from '../../../types'; -import { EndpointAction } from '../../../store/action'; -import { useEndpointSelector } from '../../hooks'; -import { getActivityLogDataPaging } from '../../../store/selectors'; + import { EndpointDetailsFlyoutHeader } from './flyout_header'; import { useNavigateByRouterEventHandler } from '../../../../../../common/hooks/endpoint/use_navigate_by_router_event_handler'; import { useAppUrl } from '../../../../../../common/lib/kibana'; @@ -33,17 +30,9 @@ interface EndpointDetailsTabs { } const EndpointDetailsTab = memo( - ({ - tab, - isSelected, - handleTabClick, - }: { - tab: EndpointDetailsTabs; - isSelected: boolean; - handleTabClick: () => void; - }) => { + ({ tab, isSelected }: { tab: EndpointDetailsTabs; isSelected: boolean }) => { const { getAppUrl } = useAppUrl(); - const onClick = useNavigateByRouterEventHandler(tab.route, handleTabClick); + const onClick = useNavigateByRouterEventHandler(tab.route); return ( { - const dispatch = useDispatch<(action: EndpointAction) => void>(); - const { pageSize } = useEndpointSelector(getActivityLogDataPaging); - - const handleTabClick = useCallback( - (tab: EuiTabbedContentTab) => { - if (tab.id === EndpointDetailsTabsTypes.activityLog) { - dispatch({ - type: 'endpointDetailsActivityLogUpdatePaging', - payload: { - disabled: false, - page: 1, - pageSize, - startDate: undefined, - endDate: undefined, - }, - }); - } - }, - [dispatch, pageSize] - ); - const selectedTab = useMemo(() => tabs.find((tab) => tab.id === show), [tabs, show]); const renderTabs = tabs.map((tab) => ( - handleTabClick(tab)} - isSelected={tab.id === selectedTab?.id} - /> + )); return ( diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_activity_log.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_activity_log.tsx index 121f23fdb3a9e..5172b59450e03 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_activity_log.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_activity_log.tsx @@ -30,6 +30,7 @@ import { getActivityLogRequestLoaded, getLastLoadedActivityLogData, getActivityLogRequestLoading, + getActivityLogUninitialized, } from '../../store/selectors'; const StyledEuiFlexGroup = styled(EuiFlexGroup)<{ isShorter: boolean }>` @@ -42,6 +43,7 @@ const LoadMoreTrigger = styled.div` export const EndpointActivityLog = memo( ({ activityLog }: { activityLog: AsyncResourceState> }) => { + const activityLogUninitialized = useEndpointSelector(getActivityLogUninitialized); const activityLogLoading = useEndpointSelector(getActivityLogRequestLoading); const activityLogLoaded = useEndpointSelector(getActivityLogRequestLoaded); const activityLastLogData = useEndpointSelector(getLastLoadedActivityLogData); @@ -96,7 +98,9 @@ export const EndpointActivityLog = memo( return ( <> - {showEmptyState ? ( + {(activityLogLoading && !activityLastLogData?.data.length) || activityLogUninitialized ? ( + + ) : showEmptyState ? ( { }, [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 b93fec8e99ca5..005fd8905b601 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", @@ -1874,10 +1865,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", @@ -1906,10 +1893,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/task_manager/server/lib/calculate_health_status.ts b/x-pack/plugins/task_manager/server/lib/calculate_health_status.ts index d0e99690066dd..b544674d61c07 100644 --- a/x-pack/plugins/task_manager/server/lib/calculate_health_status.ts +++ b/x-pack/plugins/task_manager/server/lib/calculate_health_status.ts @@ -9,32 +9,42 @@ import { isString } from 'lodash'; import { JsonValue } from '@kbn/utility-types'; import { HealthStatus, RawMonitoringStats } from '../monitoring'; import { TaskManagerConfig } from '../config'; +import { Logger } from '../../../../../src/core/server'; export function calculateHealthStatus( summarizedStats: RawMonitoringStats, - config: TaskManagerConfig + config: TaskManagerConfig, + logger: Logger ): HealthStatus { const now = Date.now(); - // if "hot" health stats are any more stale than monitored_stats_required_freshness (pollInterval +1s buffer by default) - // consider the system unhealthy - const requiredHotStatsFreshness: number = config.monitored_stats_required_freshness; + // if "hot" health stats are any more stale than monitored_stats_required_freshness + // times a multiplier, consider the system unhealthy + const requiredHotStatsFreshness: number = config.monitored_stats_required_freshness * 3; - // if "cold" health stats are any more stale than the configured refresh (+ a buffer), consider the system unhealthy + // if "cold" health stats are any more stale than the configured refresh + // times a multiplier, consider the system unhealthy const requiredColdStatsFreshness: number = config.monitored_aggregated_stats_refresh_rate * 1.5; - /** - * If the monitored stats aren't fresh, return a red status - */ - const healthStatus = - hasStatus(summarizedStats.stats, HealthStatus.Error) || - hasExpiredHotTimestamps(summarizedStats, now, requiredHotStatsFreshness) || - hasExpiredColdTimestamps(summarizedStats, now, requiredColdStatsFreshness) - ? HealthStatus.Error - : hasStatus(summarizedStats.stats, HealthStatus.Warning) - ? HealthStatus.Warning - : HealthStatus.OK; - return healthStatus; + if (hasStatus(summarizedStats.stats, HealthStatus.Error)) { + return HealthStatus.Error; + } + + if (hasExpiredHotTimestamps(summarizedStats, now, requiredHotStatsFreshness)) { + logger.debug('setting HealthStatus.Error because of expired hot timestamps'); + return HealthStatus.Error; + } + + if (hasExpiredColdTimestamps(summarizedStats, now, requiredColdStatsFreshness)) { + logger.debug('setting HealthStatus.Error because of expired cold timestamps'); + return HealthStatus.Error; + } + + if (hasStatus(summarizedStats.stats, HealthStatus.Warning)) { + return HealthStatus.Warning; + } + + return HealthStatus.OK; } function hasStatus(stats: RawMonitoringStats['stats'], status: HealthStatus): boolean { diff --git a/x-pack/plugins/task_manager/server/lib/log_health_metrics.test.ts b/x-pack/plugins/task_manager/server/lib/log_health_metrics.test.ts index b8e3e78925df5..4e17d64870a39 100644 --- a/x-pack/plugins/task_manager/server/lib/log_health_metrics.test.ts +++ b/x-pack/plugins/task_manager/server/lib/log_health_metrics.test.ts @@ -50,12 +50,15 @@ describe('logHealthMetrics', () => { (calculateHealthStatus as jest.Mock).mockImplementation(() => HealthStatus.Error); logHealthMetrics(health, logger, config); - expect((logger as jest.Mocked).warn.mock.calls[0][0] as string).toBe( - `Detected potential performance issue with Task Manager. Set 'xpack.task_manager.monitored_stats_health_verbose_log.enabled: true' in your Kibana.yml to enable debug logging` - ); - expect((logger as jest.Mocked).warn.mock.calls[1][0] as string).toBe( - `Detected potential performance issue with Task Manager. Set 'xpack.task_manager.monitored_stats_health_verbose_log.enabled: true' in your Kibana.yml to enable debug logging` - ); + const debugCalls = (logger as jest.Mocked).debug.mock.calls; + const performanceMessage = /^Task Manager detected a degradation in performance/; + const lastStatsMessage = /^Latest Monitored Stats: \{.*\}$/; + expect(debugCalls[0][0] as string).toMatch(lastStatsMessage); + expect(debugCalls[1][0] as string).toMatch(lastStatsMessage); + expect(debugCalls[2][0] as string).toMatch(performanceMessage); + expect(debugCalls[3][0] as string).toMatch(lastStatsMessage); + expect(debugCalls[4][0] as string).toMatch(lastStatsMessage); + expect(debugCalls[5][0] as string).toMatch(performanceMessage); }); it('should not log a warning message to enable verbose logging when the status goes from Warning to OK', () => { diff --git a/x-pack/plugins/task_manager/server/lib/log_health_metrics.ts b/x-pack/plugins/task_manager/server/lib/log_health_metrics.ts index e8511b1e8c71d..d541ffb5684da 100644 --- a/x-pack/plugins/task_manager/server/lib/log_health_metrics.ts +++ b/x-pack/plugins/task_manager/server/lib/log_health_metrics.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { kibanaPackageJson } from '@kbn/utils'; + import { isEmpty } from 'lodash'; import { Logger } from '../../../../../src/core/server'; import { HealthStatus } from '../monitoring'; @@ -36,7 +38,7 @@ export function logHealthMetrics( capacity_estimation: undefined, }, }; - const statusWithoutCapacity = calculateHealthStatus(healthWithoutCapacity, config); + const statusWithoutCapacity = calculateHealthStatus(healthWithoutCapacity, config, logger); if (statusWithoutCapacity === HealthStatus.Warning) { logLevel = LogLevel.Warn; } else if (statusWithoutCapacity === HealthStatus.Error && !isEmpty(monitoredHealth.stats)) { @@ -44,6 +46,8 @@ export function logHealthMetrics( } const message = `Latest Monitored Stats: ${JSON.stringify(monitoredHealth)}`; + const docLink = `https://www.elastic.co/guide/en/kibana/${kibanaPackageJson.branch}/task-manager-health-monitoring.html`; + const detectedProblemMessage = `Task Manager detected a degradation in performance. This is usually temporary, and Kibana can recover automatically. If the problem persists, check the docs for troubleshooting information: ${docLink} .`; if (enabled) { const driftInSeconds = (monitoredHealth.stats.runtime?.value.drift.p99 ?? 0) / 1000; if ( @@ -80,9 +84,7 @@ export function logHealthMetrics( // This is legacy support - we used to always show this logger.debug(message); if (logLevel !== LogLevel.Debug && lastLogLevel === LogLevel.Debug) { - logger.warn( - `Detected potential performance issue with Task Manager. Set 'xpack.task_manager.monitored_stats_health_verbose_log.enabled: true' in your Kibana.yml to enable debug logging` - ); + logger.debug(detectedProblemMessage); } } diff --git a/x-pack/plugins/task_manager/server/monitoring/capacity_estimation.test.ts b/x-pack/plugins/task_manager/server/monitoring/capacity_estimation.test.ts index 5e2b075415a10..d6ae8024b92f7 100644 --- a/x-pack/plugins/task_manager/server/monitoring/capacity_estimation.test.ts +++ b/x-pack/plugins/task_manager/server/monitoring/capacity_estimation.test.ts @@ -7,11 +7,19 @@ import { CapacityEstimationParams, estimateCapacity } from './capacity_estimation'; import { HealthStatus, RawMonitoringStats } from './monitoring_stats_stream'; +import { mockLogger } from '../test_utils'; describe('estimateCapacity', () => { + const logger = mockLogger(); + + beforeAll(() => { + jest.resetAllMocks(); + }); + test('estimates the max throughput per minute based on the workload and the assumed kibana instances', async () => { expect( estimateCapacity( + logger, mockStats( { max_workers: 10, poll_interval: 3000 }, { @@ -67,6 +75,7 @@ describe('estimateCapacity', () => { test('reduces the available capacity per kibana when average task duration exceeds the poll interval', async () => { expect( estimateCapacity( + logger, mockStats( { max_workers: 10, poll_interval: 3000 }, { @@ -124,6 +133,7 @@ describe('estimateCapacity', () => { test('estimates the max throughput per minute when duration by persistence is empty', async () => { expect( estimateCapacity( + logger, mockStats( { max_workers: 10, poll_interval: 3000 }, { @@ -160,6 +170,7 @@ describe('estimateCapacity', () => { test('estimates the max throughput per minute based on the workload and the assumed kibana instances when there are tasks that repeat each hour or day', async () => { expect( estimateCapacity( + logger, mockStats( { max_workers: 10, poll_interval: 3000 }, { @@ -215,6 +226,7 @@ describe('estimateCapacity', () => { test('estimates the max throughput available when there are no active Kibana', async () => { expect( estimateCapacity( + logger, mockStats( { max_workers: 10, poll_interval: 3000 }, { @@ -271,6 +283,7 @@ describe('estimateCapacity', () => { test('estimates the max throughput available to handle the workload when there are multiple active kibana instances', async () => { expect( estimateCapacity( + logger, mockStats( { max_workers: 10, poll_interval: 3000 }, { @@ -332,6 +345,7 @@ describe('estimateCapacity', () => { expect( estimateCapacity( + logger, mockStats( { max_workers: 10, poll_interval: 3000 }, { @@ -412,6 +426,7 @@ describe('estimateCapacity', () => { expect( estimateCapacity( + logger, mockStats( { max_workers: 10, poll_interval: 3000 }, { @@ -493,6 +508,7 @@ describe('estimateCapacity', () => { test('marks estimated capacity as OK state when workload and load suggest capacity is sufficient', async () => { expect( estimateCapacity( + logger, mockStats( { max_workers: 10, poll_interval: 3000 }, { @@ -557,6 +573,7 @@ describe('estimateCapacity', () => { test('marks estimated capacity as Warning state when capacity is insufficient for recent spikes of non-recurring workload, but sufficient for the recurring workload', async () => { expect( estimateCapacity( + logger, mockStats( { max_workers: 10, poll_interval: 3000 }, { @@ -618,6 +635,7 @@ describe('estimateCapacity', () => { test('marks estimated capacity as Error state when workload and load suggest capacity is insufficient', async () => { expect( estimateCapacity( + logger, mockStats( { max_workers: 10, poll_interval: 3000 }, { @@ -679,6 +697,7 @@ describe('estimateCapacity', () => { test('recommmends a 20% increase in kibana when a spike in non-recurring tasks forces recurring task capacity to zero', async () => { expect( estimateCapacity( + logger, mockStats( { max_workers: 10, poll_interval: 3000 }, { @@ -754,6 +773,7 @@ describe('estimateCapacity', () => { test('recommmends a 20% increase in kibana when a spike in non-recurring tasks in a system with insufficient capacity even for recurring tasks', async () => { expect( estimateCapacity( + logger, mockStats( { max_workers: 10, poll_interval: 3000 }, { diff --git a/x-pack/plugins/task_manager/server/monitoring/capacity_estimation.ts b/x-pack/plugins/task_manager/server/monitoring/capacity_estimation.ts index 9cc223f63b196..49c593d77acec 100644 --- a/x-pack/plugins/task_manager/server/monitoring/capacity_estimation.ts +++ b/x-pack/plugins/task_manager/server/monitoring/capacity_estimation.ts @@ -12,6 +12,7 @@ import { RawMonitoringStats, RawMonitoredStat, HealthStatus } from './monitoring import { AveragedStat } from './task_run_calcultors'; import { TaskPersistenceTypes } from './task_run_statistics'; import { asErr, asOk, map, Result } from '../lib/result_type'; +import { Logger } from '../../../../../src/core/server'; export interface CapacityEstimationStat extends JsonObject { observed: { @@ -44,6 +45,7 @@ function isCapacityEstimationParams( } export function estimateCapacity( + logger: Logger, capacityStats: CapacityEstimationParams ): RawMonitoredStat { const workload = capacityStats.workload.value; @@ -183,13 +185,14 @@ export function estimateCapacity( const assumedRequiredThroughputPerMinutePerKibana = averageCapacityUsedByNonRecurringAndEphemeralTasksPerKibana + averageRecurringRequiredPerMinute / assumedKibanaInstances; + + const status = getHealthStatus(logger, { + assumedRequiredThroughputPerMinutePerKibana, + assumedAverageRecurringRequiredThroughputPerMinutePerKibana, + capacityPerMinutePerKibana, + }); return { - status: - assumedRequiredThroughputPerMinutePerKibana < capacityPerMinutePerKibana - ? HealthStatus.OK - : assumedAverageRecurringRequiredThroughputPerMinutePerKibana < capacityPerMinutePerKibana - ? HealthStatus.Warning - : HealthStatus.Error, + status, timestamp: new Date().toISOString(), value: { observed: mapValues( @@ -220,13 +223,43 @@ export function estimateCapacity( }; } +interface GetHealthStatusParams { + assumedRequiredThroughputPerMinutePerKibana: number; + assumedAverageRecurringRequiredThroughputPerMinutePerKibana: number; + capacityPerMinutePerKibana: number; +} + +function getHealthStatus(logger: Logger, params: GetHealthStatusParams): HealthStatus { + const { + assumedRequiredThroughputPerMinutePerKibana, + assumedAverageRecurringRequiredThroughputPerMinutePerKibana, + capacityPerMinutePerKibana, + } = params; + if (assumedRequiredThroughputPerMinutePerKibana < capacityPerMinutePerKibana) { + return HealthStatus.OK; + } + + if (assumedAverageRecurringRequiredThroughputPerMinutePerKibana < capacityPerMinutePerKibana) { + logger.debug( + `setting HealthStatus.Warning because assumedAverageRecurringRequiredThroughputPerMinutePerKibana (${assumedAverageRecurringRequiredThroughputPerMinutePerKibana}) < capacityPerMinutePerKibana (${capacityPerMinutePerKibana})` + ); + return HealthStatus.Warning; + } + + logger.debug( + `setting HealthStatus.Error because assumedRequiredThroughputPerMinutePerKibana (${assumedRequiredThroughputPerMinutePerKibana}) >= capacityPerMinutePerKibana (${capacityPerMinutePerKibana}) AND assumedAverageRecurringRequiredThroughputPerMinutePerKibana (${assumedAverageRecurringRequiredThroughputPerMinutePerKibana}) >= capacityPerMinutePerKibana (${capacityPerMinutePerKibana})` + ); + return HealthStatus.Error; +} + export function withCapacityEstimate( + logger: Logger, monitoredStats: RawMonitoringStats['stats'] ): RawMonitoringStats['stats'] { if (isCapacityEstimationParams(monitoredStats)) { return { ...monitoredStats, - capacity_estimation: estimateCapacity(monitoredStats), + capacity_estimation: estimateCapacity(logger, monitoredStats), }; } return monitoredStats; diff --git a/x-pack/plugins/task_manager/server/monitoring/monitoring_stats_stream.ts b/x-pack/plugins/task_manager/server/monitoring/monitoring_stats_stream.ts index fdddfc41e590a..08badf8fe1c9d 100644 --- a/x-pack/plugins/task_manager/server/monitoring/monitoring_stats_stream.ts +++ b/x-pack/plugins/task_manager/server/monitoring/monitoring_stats_stream.ts @@ -136,6 +136,7 @@ export function createMonitoringStatsStream( } export function summarizeMonitoringStats( + logger: Logger, { // eslint-disable-next-line @typescript-eslint/naming-convention last_update, @@ -143,7 +144,7 @@ export function summarizeMonitoringStats( }: MonitoringStats, config: TaskManagerConfig ): RawMonitoringStats { - const summarizedStats = withCapacityEstimate({ + const summarizedStats = withCapacityEstimate(logger, { ...(configuration ? { configuration: { @@ -156,7 +157,7 @@ export function summarizeMonitoringStats( ? { runtime: { timestamp: runtime.timestamp, - ...summarizeTaskRunStat(runtime.value, config), + ...summarizeTaskRunStat(logger, runtime.value, config), }, } : {}), diff --git a/x-pack/plugins/task_manager/server/monitoring/task_run_statistics.test.ts b/x-pack/plugins/task_manager/server/monitoring/task_run_statistics.test.ts index 46dc56b2bac4d..32fe9bf60466f 100644 --- a/x-pack/plugins/task_manager/server/monitoring/task_run_statistics.test.ts +++ b/x-pack/plugins/task_manager/server/monitoring/task_run_statistics.test.ts @@ -10,6 +10,7 @@ import { Subject, Observable } from 'rxjs'; import stats from 'stats-lite'; import sinon from 'sinon'; import { take, tap, bufferCount, skip, map } from 'rxjs/operators'; +import { mockLogger } from '../test_utils'; import { ConcreteTaskInstance, TaskStatus } from '../task'; import { @@ -36,9 +37,11 @@ import { configSchema } from '../config'; describe('Task Run Statistics', () => { let fakeTimer: sinon.SinonFakeTimers; + const logger = mockLogger(); beforeAll(() => { fakeTimer = sinon.useFakeTimers(); + jest.resetAllMocks(); }); afterAll(() => fakeTimer.restore()); @@ -77,7 +80,7 @@ describe('Task Run Statistics', () => { // Use 'summarizeTaskRunStat' to receive summarize stats map(({ key, value }: AggregatedStat) => ({ key, - value: summarizeTaskRunStat(value, getTaskManagerConfig()).value, + value: summarizeTaskRunStat(logger, value, getTaskManagerConfig()).value, })), take(runAtDrift.length), bufferCount(runAtDrift.length) @@ -145,7 +148,7 @@ describe('Task Run Statistics', () => { // Use 'summarizeTaskRunStat' to receive summarize stats map(({ key, value }: AggregatedStat) => ({ key, - value: summarizeTaskRunStat(value, getTaskManagerConfig()).value, + value: summarizeTaskRunStat(logger, value, getTaskManagerConfig()).value, })), take(runDurations.length * 2), bufferCount(runDurations.length * 2) @@ -241,7 +244,7 @@ describe('Task Run Statistics', () => { // Use 'summarizeTaskRunStat' to receive summarize stats map(({ key, value }: AggregatedStat) => ({ key, - value: summarizeTaskRunStat(value, getTaskManagerConfig()).value, + value: summarizeTaskRunStat(logger, value, getTaskManagerConfig()).value, })), take(10), bufferCount(10) @@ -321,6 +324,7 @@ describe('Task Run Statistics', () => { map(({ key, value }: AggregatedStat) => ({ key, value: summarizeTaskRunStat( + logger, value, getTaskManagerConfig({ monitored_task_execution_thresholds: { @@ -449,7 +453,7 @@ describe('Task Run Statistics', () => { // Use 'summarizeTaskRunStat' to receive summarize stats map(({ key, value }: AggregatedStat) => ({ key, - value: summarizeTaskRunStat(value, getTaskManagerConfig({})).value, + value: summarizeTaskRunStat(logger, value, getTaskManagerConfig({})).value, })), take(taskEvents.length), bufferCount(taskEvents.length) @@ -590,7 +594,7 @@ describe('Task Run Statistics', () => { // Use 'summarizeTaskRunStat' to receive summarize stats map(({ key, value }: AggregatedStat) => ({ key, - value: summarizeTaskRunStat(value, getTaskManagerConfig({})).value, + value: summarizeTaskRunStat(logger, value, getTaskManagerConfig({})).value, })), take(taskEvents.length), bufferCount(taskEvents.length) @@ -707,7 +711,7 @@ describe('Task Run Statistics', () => { // Use 'summarizeTaskRunStat' to receive summarize stats map(({ key, value }: AggregatedStat) => ({ key, - value: summarizeTaskRunStat(value, getTaskManagerConfig()).value, + value: summarizeTaskRunStat(logger, value, getTaskManagerConfig()).value, })), tap(() => { expectedTimestamp.push(new Date().toISOString()); diff --git a/x-pack/plugins/task_manager/server/monitoring/task_run_statistics.ts b/x-pack/plugins/task_manager/server/monitoring/task_run_statistics.ts index 3946827827fee..44908706aa6ec 100644 --- a/x-pack/plugins/task_manager/server/monitoring/task_run_statistics.ts +++ b/x-pack/plugins/task_manager/server/monitoring/task_run_statistics.ts @@ -38,6 +38,7 @@ import { import { HealthStatus } from './monitoring_stats_stream'; import { TaskPollingLifecycle } from '../polling_lifecycle'; import { TaskExecutionFailureThreshold, TaskManagerConfig } from '../config'; +import { Logger } from '../../../../../src/core/server'; interface FillPoolStat extends JsonObject { duration: number[]; @@ -337,6 +338,7 @@ const DEFAULT_POLLING_FREQUENCIES = { }; export function summarizeTaskRunStat( + logger: Logger, { polling: { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -403,6 +405,7 @@ export function summarizeTaskRunStat( executionResultFrequency, (typedResultFrequencies, taskType) => summarizeTaskExecutionResultFrequencyStat( + logger, { ...DEFAULT_TASK_RUN_FREQUENCIES, ...calculateFrequency(typedResultFrequencies), @@ -418,16 +421,35 @@ export function summarizeTaskRunStat( } function summarizeTaskExecutionResultFrequencyStat( + logger: Logger, resultFrequencySummary: ResultFrequency, executionErrorThreshold: TaskExecutionFailureThreshold ): ResultFrequencySummary { + const status = getHealthStatus(logger, resultFrequencySummary, executionErrorThreshold); return { ...resultFrequencySummary, - status: - resultFrequencySummary.Failed > executionErrorThreshold.warn_threshold - ? resultFrequencySummary.Failed > executionErrorThreshold.error_threshold - ? HealthStatus.Error - : HealthStatus.Warning - : HealthStatus.OK, + status, }; } + +function getHealthStatus( + logger: Logger, + resultFrequencySummary: ResultFrequency, + executionErrorThreshold: TaskExecutionFailureThreshold +): HealthStatus { + if (resultFrequencySummary.Failed > executionErrorThreshold.warn_threshold) { + if (resultFrequencySummary.Failed > executionErrorThreshold.error_threshold) { + logger.debug( + `setting HealthStatus.Error because resultFrequencySummary.Failed (${resultFrequencySummary.Failed}) > error_threshold (${executionErrorThreshold.error_threshold})` + ); + return HealthStatus.Error; + } else { + logger.debug( + `setting HealthStatus.Warning because resultFrequencySummary.Failed (${resultFrequencySummary.Failed}) > warn_threshold (${executionErrorThreshold.warn_threshold})` + ); + return HealthStatus.Warning; + } + } + + return HealthStatus.OK; +} diff --git a/x-pack/plugins/task_manager/server/plugin.ts b/x-pack/plugins/task_manager/server/plugin.ts index 11746e2da2847..35dec38b43df5 100644 --- a/x-pack/plugins/task_manager/server/plugin.ts +++ b/x-pack/plugins/task_manager/server/plugin.ts @@ -95,6 +95,14 @@ export class TaskManagerPlugin this.config! ); + core.status.derivedStatus$.subscribe((status) => + this.logger.debug(`status core.status.derivedStatus now set to ${status.level}`) + ); + serviceStatus$.subscribe((status) => + this.logger.debug(`status serviceStatus now set to ${status.level}`) + ); + + // here is where the system status is updated core.status.set( combineLatest([core.status.derivedStatus$, serviceStatus$]).pipe( map(([derivedStatus, serviceStatus]) => diff --git a/x-pack/plugins/task_manager/server/routes/health.test.ts b/x-pack/plugins/task_manager/server/routes/health.test.ts index 01c1d6a5f3983..f34728cd8ff3c 100644 --- a/x-pack/plugins/task_manager/server/routes/health.test.ts +++ b/x-pack/plugins/task_manager/server/routes/health.test.ts @@ -20,7 +20,7 @@ import { RawMonitoringStats, summarizeMonitoringStats, } from '../monitoring'; -import { ServiceStatusLevels } from 'src/core/server'; +import { ServiceStatusLevels, Logger } from 'src/core/server'; import { configSchema, TaskManagerConfig } from '../config'; import { calculateHealthStatusMock } from '../lib/calculate_health_status.mock'; import { FillPoolResult } from '../lib/fill_pool'; @@ -30,14 +30,14 @@ jest.mock('../lib/log_health_metrics', () => ({ })); describe('healthRoute', () => { + const logger = loggingSystemMock.create().get(); + beforeEach(() => { jest.resetAllMocks(); }); it('registers the route', async () => { const router = httpServiceMock.createRouter(); - - const logger = loggingSystemMock.create().get(); healthRoute(router, of(), logger, uuid.v4(), getTaskManagerConfig()); const [config] = router.get.mock.calls[0]; @@ -47,7 +47,6 @@ describe('healthRoute', () => { it('logs the Task Manager stats at a fixed interval', async () => { const router = httpServiceMock.createRouter(); - const logger = loggingSystemMock.create().get(); const calculateHealthStatus = calculateHealthStatusMock.create(); calculateHealthStatus.mockImplementation(() => HealthStatus.OK); const { logHealthMetrics } = jest.requireMock('../lib/log_health_metrics'); @@ -87,19 +86,22 @@ describe('healthRoute', () => { id, timestamp: expect.any(String), status: expect.any(String), - ...ignoreCapacityEstimation(summarizeMonitoringStats(mockStat, getTaskManagerConfig({}))), + ...ignoreCapacityEstimation( + summarizeMonitoringStats(logger, mockStat, getTaskManagerConfig({})) + ), }); expect(logHealthMetrics.mock.calls[1][0]).toMatchObject({ id, timestamp: expect.any(String), status: expect.any(String), - ...ignoreCapacityEstimation(summarizeMonitoringStats(nextMockStat, getTaskManagerConfig({}))), + ...ignoreCapacityEstimation( + summarizeMonitoringStats(logger, nextMockStat, getTaskManagerConfig({})) + ), }); }); it(`logs at a warn level if the status is warning`, async () => { const router = httpServiceMock.createRouter(); - const logger = loggingSystemMock.create().get(); const calculateHealthStatus = calculateHealthStatusMock.create(); calculateHealthStatus.mockImplementation(() => HealthStatus.Warning); const { logHealthMetrics } = jest.requireMock('../lib/log_health_metrics'); @@ -141,7 +143,7 @@ describe('healthRoute', () => { timestamp: expect.any(String), status: expect.any(String), ...ignoreCapacityEstimation( - summarizeMonitoringStats(warnRuntimeStat, getTaskManagerConfig({})) + summarizeMonitoringStats(logger, warnRuntimeStat, getTaskManagerConfig({})) ), }); expect(logHealthMetrics.mock.calls[1][0]).toMatchObject({ @@ -149,7 +151,7 @@ describe('healthRoute', () => { timestamp: expect.any(String), status: expect.any(String), ...ignoreCapacityEstimation( - summarizeMonitoringStats(warnConfigurationStat, getTaskManagerConfig({})) + summarizeMonitoringStats(logger, warnConfigurationStat, getTaskManagerConfig({})) ), }); expect(logHealthMetrics.mock.calls[2][0]).toMatchObject({ @@ -157,7 +159,7 @@ describe('healthRoute', () => { timestamp: expect.any(String), status: expect.any(String), ...ignoreCapacityEstimation( - summarizeMonitoringStats(warnWorkloadStat, getTaskManagerConfig({})) + summarizeMonitoringStats(logger, warnWorkloadStat, getTaskManagerConfig({})) ), }); expect(logHealthMetrics.mock.calls[3][0]).toMatchObject({ @@ -165,14 +167,13 @@ describe('healthRoute', () => { timestamp: expect.any(String), status: expect.any(String), ...ignoreCapacityEstimation( - summarizeMonitoringStats(warnEphemeralStat, getTaskManagerConfig({})) + summarizeMonitoringStats(logger, warnEphemeralStat, getTaskManagerConfig({})) ), }); }); it(`logs at an error level if the status is error`, async () => { const router = httpServiceMock.createRouter(); - const logger = loggingSystemMock.create().get(); const calculateHealthStatus = calculateHealthStatusMock.create(); calculateHealthStatus.mockImplementation(() => HealthStatus.Error); const { logHealthMetrics } = jest.requireMock('../lib/log_health_metrics'); @@ -214,7 +215,7 @@ describe('healthRoute', () => { timestamp: expect.any(String), status: expect.any(String), ...ignoreCapacityEstimation( - summarizeMonitoringStats(errorRuntimeStat, getTaskManagerConfig({})) + summarizeMonitoringStats(logger, errorRuntimeStat, getTaskManagerConfig({})) ), }); expect(logHealthMetrics.mock.calls[1][0]).toMatchObject({ @@ -222,7 +223,7 @@ describe('healthRoute', () => { timestamp: expect.any(String), status: expect.any(String), ...ignoreCapacityEstimation( - summarizeMonitoringStats(errorConfigurationStat, getTaskManagerConfig({})) + summarizeMonitoringStats(logger, errorConfigurationStat, getTaskManagerConfig({})) ), }); expect(logHealthMetrics.mock.calls[2][0]).toMatchObject({ @@ -230,7 +231,7 @@ describe('healthRoute', () => { timestamp: expect.any(String), status: expect.any(String), ...ignoreCapacityEstimation( - summarizeMonitoringStats(errorWorkloadStat, getTaskManagerConfig({})) + summarizeMonitoringStats(logger, errorWorkloadStat, getTaskManagerConfig({})) ), }); expect(logHealthMetrics.mock.calls[3][0]).toMatchObject({ @@ -238,7 +239,7 @@ describe('healthRoute', () => { timestamp: expect.any(String), status: expect.any(String), ...ignoreCapacityEstimation( - summarizeMonitoringStats(errorEphemeralStat, getTaskManagerConfig({})) + summarizeMonitoringStats(logger, errorEphemeralStat, getTaskManagerConfig({})) ), }); }); @@ -251,7 +252,7 @@ describe('healthRoute', () => { const { serviceStatus$ } = healthRoute( router, stats$, - loggingSystemMock.create().get(), + logger, uuid.v4(), getTaskManagerConfig({ monitored_stats_required_freshness: 1000, @@ -269,7 +270,7 @@ describe('healthRoute', () => { stats$.next( mockHealthStats({ - last_update: new Date(Date.now() - 1500).toISOString(), + last_update: new Date(Date.now() - 3001).toISOString(), }) ); @@ -278,6 +279,7 @@ describe('healthRoute', () => { status: 'error', ...ignoreCapacityEstimation( summarizeMonitoringStats( + logger, mockHealthStats({ last_update: expect.any(String), stats: { @@ -307,9 +309,15 @@ describe('healthRoute', () => { }); expect(await serviceStatus).toMatchObject({ - level: ServiceStatusLevels.unavailable, - summary: 'Task Manager is unavailable', + level: ServiceStatusLevels.degraded, + summary: 'Task Manager is unhealthy', }); + const debugCalls = (logger as jest.Mocked).debug.mock.calls as string[][]; + const warnMessage = /^setting HealthStatus.Warning because assumedAverageRecurringRequiredThroughputPerMinutePerKibana/; + const found = debugCalls + .map((arr) => arr[0]) + .find((message) => message.match(warnMessage) != null); + expect(found).toMatch(warnMessage); }); it('returns a error status if the workload stats have not been updated within the required cold freshness', async () => { @@ -320,7 +328,7 @@ describe('healthRoute', () => { healthRoute( router, stats$, - loggingSystemMock.create().get(), + logger, uuid.v4(), getTaskManagerConfig({ monitored_stats_required_freshness: 5000, @@ -352,6 +360,7 @@ describe('healthRoute', () => { status: 'error', ...ignoreCapacityEstimation( summarizeMonitoringStats( + logger, mockHealthStats({ last_update: expect.any(String), stats: { @@ -388,7 +397,7 @@ describe('healthRoute', () => { healthRoute( router, stats$, - loggingSystemMock.create().get(), + logger, uuid.v4(), getTaskManagerConfig({ monitored_stats_required_freshness: 1000, @@ -399,7 +408,7 @@ describe('healthRoute', () => { await sleep(0); // eslint-disable-next-line @typescript-eslint/naming-convention - const last_successful_poll = new Date(Date.now() - 2000).toISOString(); + const last_successful_poll = new Date(Date.now() - 3001).toISOString(); stats$.next( mockHealthStats({ stats: { @@ -423,6 +432,7 @@ describe('healthRoute', () => { status: 'error', ...ignoreCapacityEstimation( summarizeMonitoringStats( + logger, mockHealthStats({ last_update: expect.any(String), stats: { diff --git a/x-pack/plugins/task_manager/server/routes/health.ts b/x-pack/plugins/task_manager/server/routes/health.ts index fe58ee3490aff..4101662184430 100644 --- a/x-pack/plugins/task_manager/server/routes/health.ts +++ b/x-pack/plugins/task_manager/server/routes/health.ts @@ -62,8 +62,8 @@ export function healthRoute( const requiredHotStatsFreshness: number = config.monitored_stats_required_freshness; function getHealthStatus(monitoredStats: MonitoringStats) { - const summarizedStats = summarizeMonitoringStats(monitoredStats, config); - const status = calculateHealthStatus(summarizedStats, config); + const summarizedStats = summarizeMonitoringStats(logger, monitoredStats, config); + const status = calculateHealthStatus(summarizedStats, config, logger); const now = Date.now(); const timestamp = new Date(now).toISOString(); return { id: taskManagerId, timestamp, status, ...summarizedStats }; @@ -118,9 +118,7 @@ export function withServiceStatus( const level = monitoredHealth.status === HealthStatus.OK ? ServiceStatusLevels.available - : monitoredHealth.status === HealthStatus.Warning - ? ServiceStatusLevels.degraded - : ServiceStatusLevels.unavailable; + : ServiceStatusLevels.degraded; return [ monitoredHealth, { 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/actions/timeline/cases/add_to_case_action.tsx b/x-pack/plugins/timelines/public/components/actions/timeline/cases/add_to_case_action.tsx index b6d581f52cbe5..73be0c13faf51 100644 --- a/x-pack/plugins/timelines/public/components/actions/timeline/cases/add_to_case_action.tsx +++ b/x-pack/plugins/timelines/public/components/actions/timeline/cases/add_to_case_action.tsx @@ -26,6 +26,7 @@ export interface AddToCaseActionProps { } | null; appId: string; onClose?: Function; + disableAlerts?: boolean; } const AddToCaseActionComponent: React.FC = ({ @@ -35,6 +36,7 @@ const AddToCaseActionComponent: React.FC = ({ casePermissions, appId, onClose, + disableAlerts, }) => { const eventId = event?.ecs._id ?? ''; const eventIndex = event?.ecs._index ?? ''; @@ -104,6 +106,7 @@ const AddToCaseActionComponent: React.FC = ({ onSuccess={onCaseSuccess} useInsertTimeline={useInsertTimeline} appId={appId} + disableAlerts={disableAlerts} /> )} {isAllCaseModalOpen && cases.getAllCasesSelectorModal(getAllCasesSelectorModalProps)} diff --git a/x-pack/plugins/timelines/public/components/actions/timeline/cases/create/flyout.tsx b/x-pack/plugins/timelines/public/components/actions/timeline/cases/create/flyout.tsx index 826b9cd8dc4a6..4f189648634d0 100644 --- a/x-pack/plugins/timelines/public/components/actions/timeline/cases/create/flyout.tsx +++ b/x-pack/plugins/timelines/public/components/actions/timeline/cases/create/flyout.tsx @@ -6,7 +6,7 @@ */ import React, { memo, useMemo } from 'react'; -import styled from 'styled-components'; +import styled, { createGlobalStyle } from 'styled-components'; import { EuiFlyout, EuiFlyoutHeader, EuiTitle, EuiFlyoutBody } from '@elastic/eui'; import * as i18n from '../translations'; @@ -20,6 +20,7 @@ export interface CreateCaseModalProps { onSuccess: (theCase: Case) => Promise; useInsertTimeline?: Function; appId: string; + disableAlerts?: boolean; } const StyledFlyout = styled(EuiFlyout)` @@ -27,6 +28,23 @@ const StyledFlyout = styled(EuiFlyout)` z-index: ${theme.eui.euiZModal}; `} `; + +const maskOverlayClassName = 'create-case-flyout-mask-overlay'; + +/** + * We need to target the mask overlay which is a parent element + * of the flyout. + * A global style is needed to target a parent element. + */ + +const GlobalStyle = createGlobalStyle<{ theme: { eui: { euiZModal: number } } }>` + .${maskOverlayClassName} { + ${({ theme }) => ` + z-index: ${theme.eui.euiZModal}; + `} + } +`; + // Adding bottom padding because timeline's // bottom bar gonna hide the submit button. const StyledEuiFlyoutBody = styled(EuiFlyoutBody)` @@ -53,6 +71,7 @@ const CreateCaseFlyoutComponent: React.FC = ({ onCloseFlyout, onSuccess, appId, + disableAlerts, }) => { const { cases } = useKibana().services; const createCaseProps = useMemo(() => { @@ -62,19 +81,27 @@ const CreateCaseFlyoutComponent: React.FC = ({ onSuccess, withSteps: false, owner: [appId], + disableAlerts, }; - }, [afterCaseCreated, onCloseFlyout, onSuccess, appId]); + }, [afterCaseCreated, onCloseFlyout, onSuccess, appId, disableAlerts]); return ( - - - -

      {i18n.CREATE_TITLE}

      -
      -
      - - {cases.getCreateCase(createCaseProps)} - -
      + <> + + + + +

      {i18n.CREATE_TITLE}

      +
      +
      + + {cases.getCreateCase(createCaseProps)} + +
      + ); }; diff --git a/x-pack/plugins/timelines/public/components/hover_actions/actions/copy.tsx b/x-pack/plugins/timelines/public/components/hover_actions/actions/copy.tsx index 1c7fe5c82df85..0c1f8fbacd221 100644 --- a/x-pack/plugins/timelines/public/components/hover_actions/actions/copy.tsx +++ b/x-pack/plugins/timelines/public/components/hover_actions/actions/copy.tsx @@ -69,8 +69,8 @@ const CopyButton: React.FC = React.memo( diff --git a/x-pack/plugins/timelines/public/components/hover_actions/actions/overflow.test.tsx b/x-pack/plugins/timelines/public/components/hover_actions/actions/overflow.test.tsx new file mode 100644 index 0000000000000..e5d53df638829 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/hover_actions/actions/overflow.test.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import OverflowButton from './overflow'; + +describe('OverflowButton', () => { + const props = { + field: 'host.name', + ownFocus: false, + showTooltip: true, + value: 'mac', + closePopOver: jest.fn(), + items: [
      ], + isOverflowPopoverOpen: false, + }; + test('should render a popover', () => { + const wrapper = shallow(); + expect(wrapper.find('EuiPopover').exists()).toBeTruthy(); + }); + + test('the popover always contains a class that hides it when an overlay (e.g. the inspect modal) is displayed', () => { + const wrapper = shallow(); + expect(wrapper.find('EuiPopover').prop('panelClassName')).toEqual('withHoverActions__popover'); + }); + + test('should enable repositionOnScroll', () => { + const wrapper = shallow(); + expect(wrapper.find('EuiPopover').prop('repositionOnScroll')).toEqual(true); + }); + + test('should render a tooltip if showTooltip is true', () => { + const testProps = { + ...props, + showTooltip: true, + }; + const wrapper = shallow(); + expect(wrapper.find('EuiToolTip').exists()).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/timelines/public/components/hover_actions/actions/overflow.tsx b/x-pack/plugins/timelines/public/components/hover_actions/actions/overflow.tsx index a10c96f3aa0ae..aa582504f4f71 100644 --- a/x-pack/plugins/timelines/public/components/hover_actions/actions/overflow.tsx +++ b/x-pack/plugins/timelines/public/components/hover_actions/actions/overflow.tsx @@ -97,6 +97,7 @@ const OverflowButton: React.FC = React.memo( closePopover={closePopOver} panelPaddingSize="none" panelClassName="withHoverActions__popover" + repositionOnScroll={true} anchorPosition="downLeft" > 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/standalone/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/standalone/index.tsx index d1d8662c567cc..ee9b7be48df63 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/standalone/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/standalone/index.tsx @@ -411,7 +411,7 @@ const TGridStandaloneComponent: React.FC = ({ ) : 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 4175539361619..d5c39af9cd67f 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -20609,7 +20609,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 134f4a97779f1..735b41491f9aa 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -20890,7 +20890,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/plugins/uptime/public/components/fleet_package/browser/formatters.ts b/x-pack/plugins/uptime/public/components/fleet_package/browser/formatters.ts index fd90c53dac12a..183ffa53cc339 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/browser/formatters.ts +++ b/x-pack/plugins/uptime/public/components/fleet_package/browser/formatters.ts @@ -6,7 +6,12 @@ */ import { BrowserFields, ConfigKeys } from '../types'; -import { Formatter, commonFormatters, arrayToJsonFormatter } from '../common/formatters'; +import { + Formatter, + commonFormatters, + arrayToJsonFormatter, + stringToJsonFormatter, +} from '../common/formatters'; export type BrowserFormatMap = Record; @@ -15,7 +20,7 @@ export const browserFormatters: BrowserFormatMap = { [ConfigKeys.SOURCE_ZIP_USERNAME]: null, [ConfigKeys.SOURCE_ZIP_PASSWORD]: null, [ConfigKeys.SOURCE_ZIP_FOLDER]: null, - [ConfigKeys.SOURCE_INLINE]: (fields) => JSON.stringify(fields[ConfigKeys.SOURCE_INLINE]), + [ConfigKeys.SOURCE_INLINE]: (fields) => stringToJsonFormatter(fields[ConfigKeys.SOURCE_INLINE]), [ConfigKeys.PARAMS]: null, [ConfigKeys.SCREENSHOTS]: null, [ConfigKeys.SYNTHETICS_ARGS]: (fields) => diff --git a/x-pack/plugins/uptime/public/components/fleet_package/browser/source_field.tsx b/x-pack/plugins/uptime/public/components/fleet_package/browser/source_field.tsx index eca354f30c973..243d709d304ce 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/browser/source_field.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/browser/source_field.tsx @@ -68,6 +68,7 @@ export const SourceField = ({ onChange, defaultConfig = defaultValues }: Props) { id: 'syntheticsBrowserZipURLConfig', name: zipUrlLabel, + 'data-test-subj': `syntheticsSourceTab__zipUrl`, content: ( <> @@ -92,6 +93,7 @@ export const SourceField = ({ onChange, defaultConfig = defaultValues }: Props) setConfig((prevConfig) => ({ ...prevConfig, zipUrl: value })) } value={config.zipUrl} + data-test-subj="syntheticsBrowserZipUrl" /> ({ ...prevConfig, folder: value })) } value={config.folder} + data-test-subj="syntheticsBrowserZipUrlFolder" /> setConfig((prevConfig) => ({ ...prevConfig, params: code }))} value={config.params} + data-test-subj="syntheticsBrowserZipUrlParams" /> ({ ...prevConfig, username: value })) } value={config.username} + data-test-subj="syntheticsBrowserZipUrlUsername" /> ({ ...prevConfig, password: value })) } value={config.password} + data-test-subj="syntheticsBrowserZipUrlPassword" /> @@ -199,6 +205,7 @@ export const SourceField = ({ onChange, defaultConfig = defaultValues }: Props) defaultMessage="Inline script" /> ), + 'data-test-subj': `syntheticsSourceTab__inline`, content: ( { describe('cronToSecondsNormalizer', () => { @@ -33,4 +38,16 @@ describe('formatters', () => { expect(objectToJsonFormatter({})).toEqual(null); }); }); + + describe('stringToJsonFormatter', () => { + it('takes a string and returns an json string', () => { + expect(stringToJsonFormatter('step("test step", () => {})')).toEqual( + '"step(\\"test step\\", () => {})"' + ); + }); + + it('returns null if the string is falsy', () => { + expect(stringToJsonFormatter('')).toEqual(null); + }); + }); }); diff --git a/x-pack/plugins/uptime/public/components/fleet_package/common/formatters.ts b/x-pack/plugins/uptime/public/components/fleet_package/common/formatters.ts index 311fa7da13498..4b30f4b4f0484 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/common/formatters.ts +++ b/x-pack/plugins/uptime/public/components/fleet_package/common/formatters.ts @@ -30,3 +30,5 @@ export const secondsToCronFormatter = (value: string = '') => (value ? `${value} export const objectToJsonFormatter = (value: Record = {}) => Object.keys(value).length ? JSON.stringify(value) : null; + +export const stringToJsonFormatter = (value: string = '') => (value ? JSON.stringify(value) : null); 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 () => { diff --git a/x-pack/test/functional/apps/observability/alerts/index.ts b/x-pack/test/functional/apps/observability/alerts/index.ts new file mode 100644 index 0000000000000..c93c8b0d633ed --- /dev/null +++ b/x-pack/test/functional/apps/observability/alerts/index.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import querystring from 'querystring'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +// Based on the x-pack/test/functional/es_archives/observability/alerts archive. +const DATE_WITH_DATA = { + rangeFrom: '2021-08-31T13:36:22.109Z', + rangeTo: '2021-09-01T13:36:22.109Z', +}; + +export default ({ getPageObjects, getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + + describe('Observability alerts', function () { + this.tags('includeFirefox'); + + const pageObjects = getPageObjects(['common']); + const testSubjects = getService('testSubjects'); + + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/observability/alerts'); + await pageObjects.common.navigateToUrlWithBrowserHistory( + 'observability', + '/alerts', + `?${querystring.stringify(DATE_WITH_DATA)}` + ); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/observability/alerts'); + }); + + describe('Alerts table', () => { + it('Renders the table', async () => { + await testSubjects.existOrFail('events-viewer-panel'); + }); + + it('Renders the correct number of cells', async () => { + // NOTE: This isn't ideal, but EuiDataGrid doesn't really have the concept of "rows" + const cells = await testSubjects.findAll('dataGridRowCell'); + expect(cells.length).to.be(54); + }); + }); + }); +}; diff --git a/x-pack/test/functional/apps/observability/index.ts b/x-pack/test/functional/apps/observability/index.ts index b7f03b5f27bae..fbb401a67b55d 100644 --- a/x-pack/test/functional/apps/observability/index.ts +++ b/x-pack/test/functional/apps/observability/index.ts @@ -11,5 +11,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { describe('Observability specs', function () { this.tags('ciGroup6'); loadTestFile(require.resolve('./feature_controls')); + loadTestFile(require.resolve('./alerts')); }); } diff --git a/x-pack/test/functional/apps/uptime/synthetics_integration.ts b/x-pack/test/functional/apps/uptime/synthetics_integration.ts index 146584d138f22..1fe13227d2546 100644 --- a/x-pack/test/functional/apps/uptime/synthetics_integration.ts +++ b/x-pack/test/functional/apps/uptime/synthetics_integration.ts @@ -72,6 +72,58 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { ], ...config, }, + ...(monitorType === 'browser' + ? [ + { + data_stream: { + dataset: 'browser.network', + type: 'synthetics', + }, + id: `${getSyntheticsPolicy(agentFullPolicy)?.streams?.[1]?.id}`, + processors: [ + { + add_observer_metadata: { + geo: { + name: 'Fleet managed', + }, + }, + }, + { + add_fields: { + fields: { + 'monitor.fleet_managed': true, + }, + target: '', + }, + }, + ], + }, + { + data_stream: { + dataset: 'browser.screenshot', + type: 'synthetics', + }, + id: `${getSyntheticsPolicy(agentFullPolicy)?.streams?.[2]?.id}`, + processors: [ + { + add_observer_metadata: { + geo: { + name: 'Fleet managed', + }, + }, + }, + { + add_fields: { + fields: { + 'monitor.fleet_managed': true, + }, + target: '', + }, + }, + ], + }, + ] + : []), ], type: `synthetics/${monitorType}`, use_output: 'default', @@ -95,6 +147,11 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { host, }); + const generateBrowserConfig = (config: Record): Record => ({ + ...basicConfig, + ...config, + }); + describe('displays custom UI', () => { before(async () => { const version = await uptimeService.syntheticsPackage.getSyntheticsPackageVersion(); @@ -439,6 +496,137 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }) ); }); + + it('allows saving browser monitor', async () => { + // This test ensures that updates made to the Synthetics Policy are carried all the way through + // to the generated Agent Policy that is dispatch down to the Elastic Agent. + const config = generateBrowserConfig({ + zipUrl: 'http://test.zip', + params: JSON.stringify({ url: 'http://localhost:8080' }), + folder: 'folder', + username: 'username', + password: 'password', + }); + + await uptimePage.syntheticsIntegration.createBasicBrowserMonitorDetails(config); + await uptimePage.syntheticsIntegration.confirmAndSave(); + + await uptimePage.syntheticsIntegration.isPolicyCreatedSuccessfully(); + + const [agentPolicy] = await uptimeService.syntheticsPackage.getAgentPolicyList(); + const agentPolicyId = agentPolicy.id; + const agentFullPolicy = await uptimeService.syntheticsPackage.getFullAgentPolicy( + agentPolicyId + ); + + expect(getSyntheticsPolicy(agentFullPolicy)).to.eql( + generatePolicy({ + agentFullPolicy, + version, + name: monitorName, + monitorType: 'browser', + config: { + screenshots: 'on', + schedule: '@every 3m', + timeout: '16s', + tags: [config.tags], + 'service.name': config.apmServiceName, + 'source.zip_url.url': config.zipUrl, + 'source.zip_url.folder': config.folder, + 'source.zip_url.username': config.username, + 'source.zip_url.password': config.password, + params: JSON.parse(config.params), + }, + }) + ); + }); + + it('allows saving browser monitor with inline script', async () => { + // This test ensures that updates made to the Synthetics Policy are carried all the way through + // to the generated Agent Policy that is dispatch down to the Elastic Agent. + const config = generateBrowserConfig({ + inlineScript: + 'step("load homepage", async () => { await page.goto(\'https://www.elastic.co\'); });', + }); + + await uptimePage.syntheticsIntegration.createBasicBrowserMonitorDetails(config, true); + await uptimePage.syntheticsIntegration.confirmAndSave(); + + await uptimePage.syntheticsIntegration.isPolicyCreatedSuccessfully(); + + const [agentPolicy] = await uptimeService.syntheticsPackage.getAgentPolicyList(); + const agentPolicyId = agentPolicy.id; + const agentFullPolicy = await uptimeService.syntheticsPackage.getFullAgentPolicy( + agentPolicyId + ); + + expect(getSyntheticsPolicy(agentFullPolicy)).to.eql( + generatePolicy({ + agentFullPolicy, + version, + name: monitorName, + monitorType: 'browser', + config: { + screenshots: 'on', + schedule: '@every 3m', + timeout: '16s', + tags: [config.tags], + 'service.name': config.apmServiceName, + 'source.inline.script': config.inlineScript, + }, + }) + ); + }); + + it('allows saving browser monitor advanced options', async () => { + // This test ensures that updates made to the Synthetics Policy are carried all the way through + // to the generated Agent Policy that is dispatch down to the Elastic Agent. + const config = generateBrowserConfig({ + zipUrl: 'http://test.zip', + params: JSON.stringify({ url: 'http://localhost:8080' }), + folder: 'folder', + username: 'username', + password: 'password', + }); + const advancedConfig = { + screenshots: 'off', + syntheticsArgs: '-ssBlocks', + }; + + await uptimePage.syntheticsIntegration.createBasicBrowserMonitorDetails(config); + await uptimePage.syntheticsIntegration.configureBrowserAdvancedOptions(advancedConfig); + await uptimePage.syntheticsIntegration.confirmAndSave(); + + await uptimePage.syntheticsIntegration.isPolicyCreatedSuccessfully(); + + const [agentPolicy] = await uptimeService.syntheticsPackage.getAgentPolicyList(); + const agentPolicyId = agentPolicy.id; + const agentFullPolicy = await uptimeService.syntheticsPackage.getFullAgentPolicy( + agentPolicyId + ); + + expect(getSyntheticsPolicy(agentFullPolicy)).to.eql( + generatePolicy({ + agentFullPolicy, + version, + name: monitorName, + monitorType: 'browser', + config: { + screenshots: advancedConfig.screenshots, + schedule: '@every 3m', + timeout: '16s', + tags: [config.tags], + 'service.name': config.apmServiceName, + 'source.zip_url.url': config.zipUrl, + 'source.zip_url.folder': config.folder, + 'source.zip_url.username': config.username, + 'source.zip_url.password': config.password, + params: JSON.parse(config.params), + synthetics_args: [advancedConfig.syntheticsArgs], + }, + }) + ); + }); }); }); } diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.js index 02b492418e4fc..126c6d025f225 100644 --- a/x-pack/test/functional/config.js +++ b/x-pack/test/functional/config.js @@ -98,6 +98,7 @@ export default async function ({ readConfigFile }) { '--timelion.ui.enabled=true', '--savedObjects.maxImportPayloadBytes=10485760', // for OSS test management/_import_objects '--xpack.observability.unsafe.cases.enabled=true', + '--xpack.observability.unsafe.alertingExperience.enabled=true', // NOTE: Can be removed once enabled by default ], }, uiSettings: { @@ -211,6 +212,9 @@ export default async function ({ readConfigFile }) { securitySolution: { pathname: '/app/security', }, + observability: { + pathname: '/app/observability', + }, }, // choose where screenshots should be saved diff --git a/x-pack/test/functional/es_archives/observability/alerts/data.json.gz b/x-pack/test/functional/es_archives/observability/alerts/data.json.gz new file mode 100644 index 0000000000000..45da368188284 Binary files /dev/null and b/x-pack/test/functional/es_archives/observability/alerts/data.json.gz differ diff --git a/x-pack/test/functional/es_archives/observability/alerts/mappings.json b/x-pack/test/functional/es_archives/observability/alerts/mappings.json new file mode 100644 index 0000000000000..88d12b7d797bb --- /dev/null +++ b/x-pack/test/functional/es_archives/observability/alerts/mappings.json @@ -0,0 +1,731 @@ +{ + "type": "index", + "value": { + "aliases": { + ".alerts-observability.apm.alerts-default": { + "is_write_index": true + } + }, + "index": ".internal.alerts-observability.apm.alerts-default-000001", + "mappings": { + "_meta": { + "kibana": { + "version": "8.0.0" + }, + "namespace": "default" + }, + "dynamic": "strict", + "properties": { + "@timestamp": { + "type": "date" + }, + "ecs": { + "properties": { + "version": { + "type": "keyword" + } + } + }, + "event": { + "properties": { + "action": { + "type": "keyword" + }, + "kind": { + "type": "keyword" + } + } + }, + "kibana": { + "properties": { + "alert": { + "properties": { + "action_group": { + "type": "keyword" + }, + "duration": { + "properties": { + "us": { + "type": "long" + } + } + }, + "end": { + "type": "date" + }, + "evaluation": { + "properties": { + "threshold": { + "scaling_factor": 100, + "type": "scaled_float" + }, + "value": { + "scaling_factor": 100, + "type": "scaled_float" + } + } + }, + "id": { + "type": "keyword" + }, + "reason": { + "type": "keyword" + }, + "rule": { + "properties": { + "author": { + "type": "keyword" + }, + "category": { + "type": "keyword" + }, + "consumer": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "type": "keyword" + }, + "description": { + "type": "keyword" + }, + "enabled": { + "type": "keyword" + }, + "from": { + "type": "date" + }, + "interval": { + "type": "keyword" + }, + "license": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "note": { + "type": "keyword" + }, + "producer": { + "type": "keyword" + }, + "references": { + "type": "keyword" + }, + "risk_score": { + "type": "float" + }, + "risk_score_mapping": { + "properties": { + "field": { + "type": "keyword" + }, + "operator": { + "type": "keyword" + }, + "value": { + "type": "keyword" + } + } + }, + "rule_id": { + "type": "keyword" + }, + "rule_name_override": { + "type": "keyword" + }, + "rule_type_id": { + "type": "keyword" + }, + "severity": { + "type": "keyword" + }, + "severity_mapping": { + "properties": { + "field": { + "type": "keyword" + }, + "operator": { + "type": "keyword" + }, + "severity": { + "type": "keyword" + }, + "value": { + "type": "keyword" + } + } + }, + "tags": { + "type": "keyword" + }, + "to": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "type": "keyword" + }, + "uuid": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "severity": { + "type": "keyword" + }, + "start": { + "type": "date" + }, + "status": { + "type": "keyword" + }, + "system_status": { + "type": "keyword" + }, + "uuid": { + "type": "keyword" + }, + "workflow_reason": { + "type": "keyword" + }, + "workflow_status": { + "type": "keyword" + }, + "workflow_user": { + "type": "keyword" + } + } + }, + "space_ids": { + "type": "keyword" + }, + "version": { + "type": "version" + } + } + }, + "processor": { + "properties": { + "event": { + "type": "keyword" + } + } + }, + "service": { + "properties": { + "environment": { + "type": "keyword" + }, + "name": { + "type": "keyword" + } + } + }, + "tags": { + "type": "keyword" + }, + "transaction": { + "properties": { + "type": { + "type": "keyword" + } + } + } + } + }, + "settings": { + "index": { + "lifecycle": { + "name": ".alerts-ilm-policy", + "rollover_alias": ".alerts-observability.apm.alerts-default" + }, + "number_of_replicas": "1", + "number_of_shards": "1" + } + } + } +} + +{ + "type": "index", + "value": { + "aliases": { + ".alerts-observability.logs.alerts-default": { + "is_write_index": true + } + }, + "index": ".internal.alerts-observability.logs.alerts-default-000001", + "mappings": { + "_meta": { + "kibana": { + "version": "8.0.0" + }, + "namespace": "default" + }, + "dynamic": "strict", + "properties": { + "@timestamp": { + "type": "date" + }, + "ecs": { + "properties": { + "version": { + "type": "keyword" + } + } + }, + "event": { + "properties": { + "action": { + "type": "keyword" + }, + "kind": { + "type": "keyword" + } + } + }, + "kibana": { + "properties": { + "alert": { + "properties": { + "action_group": { + "type": "keyword" + }, + "duration": { + "properties": { + "us": { + "type": "long" + } + } + }, + "end": { + "type": "date" + }, + "evaluation": { + "properties": { + "threshold": { + "scaling_factor": 100, + "type": "scaled_float" + }, + "value": { + "scaling_factor": 100, + "type": "scaled_float" + } + } + }, + "id": { + "type": "keyword" + }, + "reason": { + "type": "keyword" + }, + "rule": { + "properties": { + "author": { + "type": "keyword" + }, + "category": { + "type": "keyword" + }, + "consumer": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "type": "keyword" + }, + "description": { + "type": "keyword" + }, + "enabled": { + "type": "keyword" + }, + "from": { + "type": "date" + }, + "interval": { + "type": "keyword" + }, + "license": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "note": { + "type": "keyword" + }, + "producer": { + "type": "keyword" + }, + "references": { + "type": "keyword" + }, + "risk_score": { + "type": "float" + }, + "risk_score_mapping": { + "properties": { + "field": { + "type": "keyword" + }, + "operator": { + "type": "keyword" + }, + "value": { + "type": "keyword" + } + } + }, + "rule_id": { + "type": "keyword" + }, + "rule_name_override": { + "type": "keyword" + }, + "rule_type_id": { + "type": "keyword" + }, + "severity": { + "type": "keyword" + }, + "severity_mapping": { + "properties": { + "field": { + "type": "keyword" + }, + "operator": { + "type": "keyword" + }, + "severity": { + "type": "keyword" + }, + "value": { + "type": "keyword" + } + } + }, + "tags": { + "type": "keyword" + }, + "to": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "type": "keyword" + }, + "uuid": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "severity": { + "type": "keyword" + }, + "start": { + "type": "date" + }, + "status": { + "type": "keyword" + }, + "system_status": { + "type": "keyword" + }, + "uuid": { + "type": "keyword" + }, + "workflow_reason": { + "type": "keyword" + }, + "workflow_status": { + "type": "keyword" + }, + "workflow_user": { + "type": "keyword" + } + } + }, + "space_ids": { + "type": "keyword" + }, + "version": { + "type": "version" + } + } + }, + "tags": { + "type": "keyword" + } + } + }, + "settings": { + "index": { + "lifecycle": { + "name": ".alerts-ilm-policy", + "rollover_alias": ".alerts-observability.logs.alerts-default" + }, + "number_of_replicas": "1", + "number_of_shards": "1" + } + } + } +} + +{ + "type": "index", + "value": { + "aliases": { + ".alerts-observability.metrics.alerts-default": { + "is_write_index": true + } + }, + "index": ".internal.alerts-observability.metrics.alerts-default-000001", + "mappings": { + "_meta": { + "kibana": { + "version": "8.0.0" + }, + "namespace": "default" + }, + "dynamic": "strict", + "properties": { + "@timestamp": { + "type": "date" + }, + "ecs": { + "properties": { + "version": { + "type": "keyword" + } + } + }, + "event": { + "properties": { + "action": { + "type": "keyword" + }, + "kind": { + "type": "keyword" + } + } + }, + "kibana": { + "properties": { + "alert": { + "properties": { + "action_group": { + "type": "keyword" + }, + "duration": { + "properties": { + "us": { + "type": "long" + } + } + }, + "end": { + "type": "date" + }, + "evaluation": { + "properties": { + "threshold": { + "scaling_factor": 100, + "type": "scaled_float" + }, + "value": { + "scaling_factor": 100, + "type": "scaled_float" + } + } + }, + "id": { + "type": "keyword" + }, + "reason": { + "type": "keyword" + }, + "rule": { + "properties": { + "author": { + "type": "keyword" + }, + "category": { + "type": "keyword" + }, + "consumer": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "type": "keyword" + }, + "description": { + "type": "keyword" + }, + "enabled": { + "type": "keyword" + }, + "from": { + "type": "date" + }, + "interval": { + "type": "keyword" + }, + "license": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "note": { + "type": "keyword" + }, + "producer": { + "type": "keyword" + }, + "references": { + "type": "keyword" + }, + "risk_score": { + "type": "float" + }, + "risk_score_mapping": { + "properties": { + "field": { + "type": "keyword" + }, + "operator": { + "type": "keyword" + }, + "value": { + "type": "keyword" + } + } + }, + "rule_id": { + "type": "keyword" + }, + "rule_name_override": { + "type": "keyword" + }, + "rule_type_id": { + "type": "keyword" + }, + "severity": { + "type": "keyword" + }, + "severity_mapping": { + "properties": { + "field": { + "type": "keyword" + }, + "operator": { + "type": "keyword" + }, + "severity": { + "type": "keyword" + }, + "value": { + "type": "keyword" + } + } + }, + "tags": { + "type": "keyword" + }, + "to": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "type": "keyword" + }, + "uuid": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "severity": { + "type": "keyword" + }, + "start": { + "type": "date" + }, + "status": { + "type": "keyword" + }, + "system_status": { + "type": "keyword" + }, + "uuid": { + "type": "keyword" + }, + "workflow_reason": { + "type": "keyword" + }, + "workflow_status": { + "type": "keyword" + }, + "workflow_user": { + "type": "keyword" + } + } + }, + "space_ids": { + "type": "keyword" + }, + "version": { + "type": "version" + } + } + }, + "tags": { + "type": "keyword" + } + } + }, + "settings": { + "index": { + "lifecycle": { + "name": ".alerts-ilm-policy", + "rollover_alias": ".alerts-observability.metrics.alerts-default" + }, + "number_of_replicas": "1", + "number_of_shards": "1" + } + } + } +} \ No newline at end of file diff --git a/x-pack/test/functional/page_objects/synthetics_integration_page.ts b/x-pack/test/functional/page_objects/synthetics_integration_page.ts index daebb1d2a2f99..cfb6e1dac980e 100644 --- a/x-pack/test/functional/page_objects/synthetics_integration_page.ts +++ b/x-pack/test/functional/page_objects/synthetics_integration_page.ts @@ -205,6 +205,16 @@ export function SyntheticsIntegrationPageProvider({ */ async configureRequestBody(testSubj: string, value: string) { await testSubjects.click(`syntheticsRequestBodyTab__${testSubj}`); + await this.fillCodeEditor(value); + }, + + /** + * + * Fills the monaco code editor + * @params value {string} value of code input + * + */ + async fillCodeEditor(value: string) { const codeEditorContainer = await testSubjects.find('codeEditorContainer'); const textArea = await codeEditorContainer.findByCssSelector('textarea'); await textArea.clearValue(); @@ -273,6 +283,40 @@ export function SyntheticsIntegrationPageProvider({ await this.fillTextInputByTestSubj('syntheticsICMPHostField', host); }, + /** + * Creates a basic browser monitor + * @params name {string} the name of the monitor + * @params zipUrl {string} the zip url of the synthetics suites + */ + async createBasicBrowserMonitorDetails( + { + name, + inlineScript, + zipUrl, + folder, + params, + username, + password, + apmServiceName, + tags, + }: Record, + isInline: boolean = false + ) { + await this.selectMonitorType('browser'); + await this.fillTextInputByTestSubj('packagePolicyNameInput', name); + await this.createBasicMonitorDetails({ name, apmServiceName, tags }); + if (isInline) { + await testSubjects.click('syntheticsSourceTab__inline'); + await this.fillCodeEditor(inlineScript); + return; + } + await this.fillTextInputByTestSubj('syntheticsBrowserZipUrl', zipUrl); + await this.fillTextInputByTestSubj('syntheticsBrowserZipUrlFolder', folder); + await this.fillTextInputByTestSubj('syntheticsBrowserZipUrlUsername', username); + await this.fillTextInputByTestSubj('syntheticsBrowserZipUrlPassword', password); + await this.fillCodeEditor(params); + }, + /** * Enables TLS */ @@ -376,5 +420,16 @@ export function SyntheticsIntegrationPageProvider({ await label.click(); } }, + + /** + * Configure browser advanced settings + * @params name {string} the name of the monitor + * @params zipUrl {string} the zip url of the synthetics suites + */ + async configureBrowserAdvancedOptions({ screenshots, syntheticsArgs }: Record) { + await testSubjects.click('syntheticsBrowserAdvancedFieldsAccordion'); + await testSubjects.selectValue('syntheticsBrowserScreenshots', screenshots); + await this.setComboBox('syntheticsBrowserSyntheticsArgs', syntheticsArgs); + }, }; } diff --git a/x-pack/test/functional/page_objects/tag_management_page.ts b/x-pack/test/functional/page_objects/tag_management_page.ts index bce8912f7fef8..8b85bc89b1181 100644 --- a/x-pack/test/functional/page_objects/tag_management_page.ts +++ b/x-pack/test/functional/page_objects/tag_management_page.ts @@ -22,6 +22,7 @@ type TagFormValidation = FillTagFormFields; * Sub page object to manipulate the create/edit tag modal. */ class TagModal extends FtrService { + private readonly browser = this.ctx.getService('browser'); private readonly testSubjects = this.ctx.getService('testSubjects'); private readonly retry = this.ctx.getService('retry'); private readonly header = this.ctx.getPageObject('header'); @@ -57,8 +58,14 @@ class TagModal extends FtrService { } if (fields.color !== undefined) { await this.testSubjects.setValue('~createModalField-color', fields.color); - // Wait for the popover to be closable before moving to the next input - await new Promise((res) => setTimeout(res, 200)); + // Close the popover before moving to the next input, as it can get in the way of interacting with other elements + await this.testSubjects.existOrFail('euiSaturation'); + await this.retry.try(async () => { + if (await this.testSubjects.exists('euiSaturation', { timeout: 10 })) { + await this.browser.pressKeys(this.browser.keys.ENTER); + } + await this.testSubjects.missingOrFail('euiSaturation', { timeout: 250 }); + }); } if (fields.description !== undefined) { await this.testSubjects.click('createModalField-description'); diff --git a/x-pack/test/functional/services/ml/data_frame_analytics.ts b/x-pack/test/functional/services/ml/data_frame_analytics.ts index 9998a52a3044e..aafe96c2c4967 100644 --- a/x-pack/test/functional/services/ml/data_frame_analytics.ts +++ b/x-pack/test/functional/services/ml/data_frame_analytics.ts @@ -16,6 +16,7 @@ export function MachineLearningDataFrameAnalyticsProvider( { getService }: FtrProviderContext, mlApi: MlApi ) { + const retry = getService('retry'); const testSubjects = getService('testSubjects'); return { @@ -50,12 +51,14 @@ export function MachineLearningDataFrameAnalyticsProvider( }, async startAnalyticsCreation() { - if (await testSubjects.exists('mlNoDataFrameAnalyticsFound')) { - await testSubjects.click('mlAnalyticsCreateFirstButton'); - } else { - await testSubjects.click('mlAnalyticsButtonCreate'); - } - await testSubjects.existOrFail('analyticsCreateSourceIndexModal'); + await retry.tryForTime(20 * 1000, async () => { + if (await testSubjects.exists('mlNoDataFrameAnalyticsFound', { timeout: 1000 })) { + await testSubjects.click('mlAnalyticsCreateFirstButton'); + } else { + await testSubjects.click('mlAnalyticsButtonCreate'); + } + await testSubjects.existOrFail('analyticsCreateSourceIndexModal'); + }); }, async waitForAnalyticsCompletion(analyticsId: string) { diff --git a/x-pack/test/functional/services/ml/job_table.ts b/x-pack/test/functional/services/ml/job_table.ts index c711ff0ac8909..4a38aa4efe4dd 100644 --- a/x-pack/test/functional/services/ml/job_table.ts +++ b/x-pack/test/functional/services/ml/job_table.ts @@ -234,13 +234,17 @@ export function MachineLearningJobTableProvider( } public async assertJobRowFields(jobId: string, expectedRow: object) { - await this.refreshJobList(); - const rows = await this.parseJobTable(); - const jobRow = rows.filter((row) => row.id === jobId)[0]; - expect(jobRow).to.eql( - expectedRow, - `Expected job row to be '${JSON.stringify(expectedRow)}' (got '${JSON.stringify(jobRow)}')` - ); + await retry.tryForTime(5000, async () => { + await this.refreshJobList(); + const rows = await this.parseJobTable(); + const jobRow = rows.filter((row) => row.id === jobId)[0]; + expect(jobRow).to.eql( + expectedRow, + `Expected job row to be '${JSON.stringify(expectedRow)}' (got '${JSON.stringify( + jobRow + )}')` + ); + }); } public async assertJobRowDetailsCounts( @@ -585,9 +589,11 @@ export function MachineLearningJobTableProvider( } // Save custom URL - await testSubjects.click('mlJobAddCustomUrl'); - const expectedIndex = existingCustomUrls.length; - await customUrls.assertCustomUrlLabel(expectedIndex, customUrl.label); + await retry.tryForTime(5000, async () => { + await testSubjects.click('mlJobAddCustomUrl'); + const expectedIndex = existingCustomUrls.length; + await customUrls.assertCustomUrlLabel(expectedIndex, customUrl.label); + }); // Save the job await this.saveEditJobFlyoutChanges(); diff --git a/x-pack/test/functional/services/transform/wizard.ts b/x-pack/test/functional/services/transform/wizard.ts index 275002155d7e0..8f8d6d896db41 100644 --- a/x-pack/test/functional/services/transform/wizard.ts +++ b/x-pack/test/functional/services/transform/wizard.ts @@ -130,7 +130,7 @@ export function TransformWizardProvider({ getService, getPageObjects }: FtrProvi column: number, expectedColumnValues: string[] ) { - await retry.tryForTime(2000, async () => { + await retry.tryForTime(20 * 1000, async () => { // get a 2D array of rows and cell values // only parse columns up to the one we want to assert const rows = await this.parseEuiDataGrid(tableSubj, column + 1); @@ -152,7 +152,7 @@ export function TransformWizardProvider({ getService, getPageObjects }: FtrProvi }, async assertEuiDataGridColumnValuesNotEmpty(tableSubj: string, column: number) { - await retry.tryForTime(2000, async () => { + await retry.tryForTime(20 * 1000, async () => { // get a 2D array of rows and cell values // only parse columns up to the one we want to assert const rows = await this.parseEuiDataGrid(tableSubj, column + 1); @@ -171,7 +171,7 @@ export function TransformWizardProvider({ getService, getPageObjects }: FtrProvi }, async assertIndexPreview(columns: number, expectedNumberOfRows: number) { - await retry.tryForTime(2000, async () => { + await retry.tryForTime(20 * 1000, async () => { // get a 2D array of rows and cell values // only parse the first column as this is sufficient to get assert the row count const rowsData = await this.parseEuiDataGrid('transformIndexPreview', 1); diff --git a/x-pack/test/rule_registry/security_and_spaces/tests/basic/bulk_update_alerts.ts b/x-pack/test/rule_registry/security_and_spaces/tests/basic/bulk_update_alerts.ts index d9f04f206cb37..04b451fa5097a 100644 --- a/x-pack/test/rule_registry/security_and_spaces/tests/basic/bulk_update_alerts.ts +++ b/x-pack/test/rule_registry/security_and_spaces/tests/basic/bulk_update_alerts.ts @@ -169,7 +169,7 @@ export default ({ getService }: FtrProviderContext) => { // Alert - Update - RBAC - spaces Security Solution superuser should bulk update alerts which match query in space1/.alerts-security.alerts // Alert - Update - RBAC - spaces superuser should bulk update alert with given id 020202 in space1/.alerts-security.alerts - describe('Security Solution', () => { + describe.skip('Security Solution', () => { const authorizedInAllSpaces = [superUser, secOnlySpacesAll, obsSecSpacesAll]; const authorizedOnlyInSpace1 = [secOnly, obsSec]; const authorizedOnlyInSpace2 = [secOnlySpace2, obsSecAllSpace2]; diff --git a/x-pack/test/rule_registry/security_and_spaces/tests/basic/find_alerts.ts b/x-pack/test/rule_registry/security_and_spaces/tests/basic/find_alerts.ts index 9b8c334c7f9da..c580a5fb6839c 100644 --- a/x-pack/test/rule_registry/security_and_spaces/tests/basic/find_alerts.ts +++ b/x-pack/test/rule_registry/security_and_spaces/tests/basic/find_alerts.ts @@ -202,7 +202,7 @@ export default ({ getService }: FtrProviderContext) => { // Alert - Update - RBAC - spaces Security Solution superuser should bulk update alerts which match query in space1/.alerts-security.alerts // Alert - Update - RBAC - spaces superuser should bulk update alert with given id 020202 in space1/.alerts-security.alerts - describe('Security Solution', () => { + describe.skip('Security Solution', () => { const authorizedInAllSpaces = [superUser, globalRead, secOnlySpacesAll, obsSecSpacesAll]; const authorizedOnlyInSpace1 = [secOnly, secOnlyRead, obsSecRead, obsSec]; const authorizedOnlyInSpace2 = [ diff --git a/x-pack/test/rule_registry/security_and_spaces/tests/basic/get_alert_by_id.ts b/x-pack/test/rule_registry/security_and_spaces/tests/basic/get_alert_by_id.ts index b968f8db176c8..cfb3539eb93d8 100644 --- a/x-pack/test/rule_registry/security_and_spaces/tests/basic/get_alert_by_id.ts +++ b/x-pack/test/rule_registry/security_and_spaces/tests/basic/get_alert_by_id.ts @@ -161,7 +161,7 @@ export default ({ getService }: FtrProviderContext) => { }); } - describe('Security Solution', () => { + describe.skip('Security Solution', () => { const authorizedInAllSpaces = [superUser, globalRead, secOnlySpacesAll, obsSecSpacesAll]; const authorizedOnlyInSpace1 = [secOnly, secOnlyRead, obsSec, obsSecRead]; const authorizedOnlyInSpace2 = [ diff --git a/x-pack/test/rule_registry/security_and_spaces/tests/basic/get_alerts_index.ts b/x-pack/test/rule_registry/security_and_spaces/tests/basic/get_alerts_index.ts index 69d35c70dcf9e..a184c99faaa1b 100644 --- a/x-pack/test/rule_registry/security_and_spaces/tests/basic/get_alerts_index.ts +++ b/x-pack/test/rule_registry/security_and_spaces/tests/basic/get_alerts_index.ts @@ -75,7 +75,7 @@ export default ({ getService }: FtrProviderContext) => { expect(indexNames?.index_name?.length).to.eql(0); }); - it(`${secOnlyRead.username} should be able to access the security solution alert in ${SPACE1}`, async () => { + it.skip(`${secOnlyRead.username} should be able to access the security solution alert in ${SPACE1}`, async () => { const indexNames = await getSecuritySolutionIndexName(secOnlyRead, SPACE1); const securitySolution = indexNames?.index_name?.find((indexName) => indexName.startsWith(SECURITY_SOLUTION_ALERT_INDEX) diff --git a/x-pack/test/rule_registry/security_and_spaces/tests/basic/update_alert.ts b/x-pack/test/rule_registry/security_and_spaces/tests/basic/update_alert.ts index df4bdf08d6850..02399f178f4ff 100644 --- a/x-pack/test/rule_registry/security_and_spaces/tests/basic/update_alert.ts +++ b/x-pack/test/rule_registry/security_and_spaces/tests/basic/update_alert.ts @@ -164,7 +164,7 @@ export default ({ getService }: FtrProviderContext) => { }); } - describe('Security Solution', () => { + describe.skip('Security Solution', () => { const authorizedInAllSpaces = [superUser, secOnlySpacesAll, obsSecSpacesAll]; const authorizedOnlyInSpace1 = [secOnly, obsSec]; const authorizedOnlyInSpace2 = [secOnlySpace2, obsSecAllSpace2];