From 1d3e791c6b1fd63158e9f0c8a0bb9452a656ae39 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Thu, 17 Nov 2022 19:40:05 +0000 Subject: [PATCH 01/78] [ML] Fixing js types after allowJs change (#144340) Fixes types and enables `allowJs` Closes https://github.com/elastic/kibana/issues/144288 Related to https://github.com/elastic/kibana/pull/144281 --- .../job_selector/id_badges/id_badges.js | 4 +- .../components/job_selector/job_selector.tsx | 1 - .../public/application/explorer/explorer.tsx | 3 +- .../explorer_anomalies_container.tsx | 1 - .../timeseries_search_service.ts | 6 +- ...fig_builder.js => chart_config_builder.ts} | 15 +- .../bucket_span_estimator.js | 9 +- .../polled_data_checker.js | 185 ++++--- .../single_series_checker.js | 506 +++++++++--------- .../models/job_validation/job_validation.ts | 5 +- x-pack/plugins/ml/tsconfig.json | 5 - 11 files changed, 365 insertions(+), 375 deletions(-) rename x-pack/plugins/ml/public/application/util/{chart_config_builder.js => chart_config_builder.ts} (88%) diff --git a/x-pack/plugins/ml/public/application/components/job_selector/id_badges/id_badges.js b/x-pack/plugins/ml/public/application/components/job_selector/id_badges/id_badges.js index 9e373c3017d0d..b0fac87389e44 100644 --- a/x-pack/plugins/ml/public/application/components/job_selector/id_badges/id_badges.js +++ b/x-pack/plugins/ml/public/application/components/job_selector/id_badges/id_badges.js @@ -66,7 +66,7 @@ export function IdBadges({ limit, maps, onLinkClick, selectedIds, showAllBarBadg ); } - return badges; + return <>{badges}; } else { const overFlow = badges.length - limit; @@ -82,7 +82,7 @@ export function IdBadges({ limit, maps, onLinkClick, selectedIds, showAllBarBadg ); - return badges; + return <>{badges}; } } IdBadges.propTypes = { diff --git a/x-pack/plugins/ml/public/application/components/job_selector/job_selector.tsx b/x-pack/plugins/ml/public/application/components/job_selector/job_selector.tsx index 16fbc81b23f12..2ca5320572da2 100644 --- a/x-pack/plugins/ml/public/application/components/job_selector/job_selector.tsx +++ b/x-pack/plugins/ml/public/application/components/job_selector/job_selector.tsx @@ -20,7 +20,6 @@ import './_index.scss'; import { Dictionary } from '../../../../common/types/common'; import { useUrlState } from '../../util/url_state'; -// @ts-ignore import { IdBadges } from './id_badges'; import { BADGE_LIMIT, diff --git a/x-pack/plugins/ml/public/application/explorer/explorer.tsx b/x-pack/plugins/ml/public/application/explorer/explorer.tsx index 35fa1baf5460b..6e224b25f506f 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer.tsx +++ b/x-pack/plugins/ml/public/application/explorer/explorer.tsx @@ -535,7 +535,7 @@ export const Explorer: FC = ({ <> = ({ {showCharts ? ( + // @ts-ignore inferred js types are incorrect f.fieldName); ml.getCardinalityOfFields({ - index: chartConfig.datafeedConfig.indices, + index: chartConfig.datafeedConfig.indices.join(','), fieldNames: entityFieldNames, query: chartConfig.datafeedConfig.query, timeFieldName: chartConfig.timeField, diff --git a/x-pack/plugins/ml/public/application/util/chart_config_builder.js b/x-pack/plugins/ml/public/application/util/chart_config_builder.ts similarity index 88% rename from x-pack/plugins/ml/public/application/util/chart_config_builder.js rename to x-pack/plugins/ml/public/application/util/chart_config_builder.ts index ae49a2d9a9839..b4ef66a2c0936 100644 --- a/x-pack/plugins/ml/public/application/util/chart_config_builder.js +++ b/x-pack/plugins/ml/public/application/util/chart_config_builder.ts @@ -11,6 +11,8 @@ */ import { get } from 'lodash'; +import { SeriesConfig } from '../../../common/types/results'; +import { Job } from '../../../common/types/anomaly_detection_jobs'; import { ES_AGGREGATION, ML_JOB_AGGREGATION } from '../../../common/constants/aggregation_types'; import { DOC_COUNT, _DOC_COUNT } from '../../../common/constants/field_types'; @@ -18,20 +20,21 @@ import { mlFunctionToESAggregation } from '../../../common/util/job_utils'; // Builds the basic configuration to plot a chart of the source data // analyzed by the the detector at the given index from the specified ML job. -export function buildConfigFromDetector(job, detectorIndex) { +export function buildConfigFromDetector(job: Job, detectorIndex: number) { const analysisConfig = job.analysis_config; const detector = analysisConfig.detectors[detectorIndex]; - const config = { + const config: SeriesConfig = { jobId: job.job_id, - detectorIndex: detectorIndex, + detectorIndex, metricFunction: detector.function === ML_JOB_AGGREGATION.LAT_LONG ? ML_JOB_AGGREGATION.LAT_LONG : mlFunctionToESAggregation(detector.function), - timeField: job.data_description.time_field, + timeField: job.data_description.time_field!, + // @ts-expect-error bucket_span is of type estypes.Duration interval: job.analysis_config.bucket_span, - datafeedConfig: job.datafeed_config, + datafeedConfig: job.datafeed_config!, summaryCountFieldName: job.analysis_config.summary_count_field_name, }; @@ -51,7 +54,7 @@ export function buildConfigFromDetector(job, detectorIndex) { // The cardinality field will be in: // aggregations//aggregations//cardinality/field // or aggs//aggs//cardinality/field - let cardinalityField = undefined; + let cardinalityField; const topAgg = get(job.datafeed_config, 'aggregations') || get(job.datafeed_config, 'aggs'); if (topAgg !== undefined && Object.values(topAgg).length > 0) { cardinalityField = diff --git a/x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.js b/x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.js index 5560f2ad22827..710139c5b9f9e 100644 --- a/x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.js +++ b/x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.js @@ -10,13 +10,11 @@ import { cloneDeep, each, remove, sortBy, get } from 'lodash'; import { mlLog } from '../../lib/log'; import { INTERVALS } from './intervals'; -import { singleSeriesCheckerFactory } from './single_series_checker'; -import { polledDataCheckerFactory } from './polled_data_checker'; +import { SingleSeriesChecker } from './single_series_checker'; +import { PolledDataChecker } from './polled_data_checker'; export function estimateBucketSpanFactory(client) { const { asCurrentUser, asInternalUser } = client; - const PolledDataChecker = polledDataCheckerFactory(client); - const SingleSeriesChecker = singleSeriesCheckerFactory(client); class BucketSpanEstimator { constructor( @@ -79,6 +77,7 @@ export function estimateBucketSpanFactory(client) { }); this.polledDataChecker = new PolledDataChecker( + asCurrentUser, this.index, this.timeField, this.duration, @@ -93,6 +92,7 @@ export function estimateBucketSpanFactory(client) { // either a single metric job or no data split this.checkers.push({ check: new SingleSeriesChecker( + asCurrentUser, this.index, this.timeField, this.aggTypes[i], @@ -117,6 +117,7 @@ export function estimateBucketSpanFactory(client) { }); this.checkers.push({ check: new SingleSeriesChecker( + asCurrentUser, this.index, this.timeField, this.aggTypes[i], diff --git a/x-pack/plugins/ml/server/models/bucket_span_estimator/polled_data_checker.js b/x-pack/plugins/ml/server/models/bucket_span_estimator/polled_data_checker.js index 7ccd3d4a30c64..35a70b69eabe4 100644 --- a/x-pack/plugins/ml/server/models/bucket_span_estimator/polled_data_checker.js +++ b/x-pack/plugins/ml/server/models/bucket_span_estimator/polled_data_checker.js @@ -13,111 +13,108 @@ import { get } from 'lodash'; -export function polledDataCheckerFactory({ asCurrentUser }) { - class PolledDataChecker { - constructor(index, timeField, duration, query, runtimeMappings, indicesOptions) { - this.index = index; - this.timeField = timeField; - this.duration = duration; - this.query = query; - this.runtimeMappings = runtimeMappings; - this.indicesOptions = indicesOptions; - - this.isPolled = false; - this.minimumBucketSpan = 0; - } +export class PolledDataChecker { + constructor(asCurrentUser, index, timeField, duration, query, runtimeMappings, indicesOptions) { + this.index = index; + this.timeField = timeField; + this.duration = duration; + this.query = query; + this.runtimeMappings = runtimeMappings; + this.indicesOptions = indicesOptions; + this.asCurrentUser = asCurrentUser; + + this.isPolled = false; + this.minimumBucketSpan = 0; + } - run() { - return new Promise((resolve, reject) => { - const interval = { name: '1m', ms: 60000 }; - this.performSearch(interval.ms) - .then((resp) => { - const fullBuckets = get(resp, 'aggregations.non_empty_buckets.buckets', []); - const result = this.isPolledData(fullBuckets, interval); - if (result.pass) { - // data is polled, return a flag and the minimumBucketSpan which should be - // used as a minimum bucket span for all subsequent tests. - this.isPolled = true; - this.minimumBucketSpan = result.meanTimeDiff; - } - resolve({ - isPolled: this.isPolled, - minimumBucketSpan: this.minimumBucketSpan, - }); - }) - .catch((resp) => { - reject(resp); + run() { + return new Promise((resolve, reject) => { + const interval = { name: '1m', ms: 60000 }; + this.performSearch(interval.ms) + .then((resp) => { + const fullBuckets = get(resp, 'aggregations.non_empty_buckets.buckets', []); + const result = this.isPolledData(fullBuckets, interval); + if (result.pass) { + // data is polled, return a flag and the minimumBucketSpan which should be + // used as a minimum bucket span for all subsequent tests. + this.isPolled = true; + this.minimumBucketSpan = result.meanTimeDiff; + } + resolve({ + isPolled: this.isPolled, + minimumBucketSpan: this.minimumBucketSpan, }); - }); - } + }) + .catch((resp) => { + reject(resp); + }); + }); + } - createSearch(intervalMs) { - const search = { - query: this.query, - aggs: { - non_empty_buckets: { - date_histogram: { - min_doc_count: 1, - field: this.timeField, - fixed_interval: `${intervalMs}ms`, - }, + createSearch(intervalMs) { + const search = { + query: this.query, + aggs: { + non_empty_buckets: { + date_histogram: { + min_doc_count: 1, + field: this.timeField, + fixed_interval: `${intervalMs}ms`, }, }, - ...this.runtimeMappings, - }; + }, + ...this.runtimeMappings, + }; - return search; + return search; + } + + async performSearch(intervalMs) { + const searchBody = this.createSearch(intervalMs); + + const body = await this.asCurrentUser.search( + { + index: this.index, + size: 0, + body: searchBody, + ...(this.indicesOptions ?? {}), + }, + { maxRetries: 0 } + ); + return body; + } + + // test that the coefficient of variation of time difference between non-empty buckets is small + isPolledData(fullBuckets, intervalMs) { + let pass = false; + + const timeDiffs = []; + let sumOfTimeDiffs = 0; + for (let i = 1; i < fullBuckets.length; i++) { + const diff = fullBuckets[i].key - fullBuckets[i - 1].key; + sumOfTimeDiffs += diff; + timeDiffs.push(diff); } - async performSearch(intervalMs) { - const searchBody = this.createSearch(intervalMs); + const meanTimeDiff = sumOfTimeDiffs / (fullBuckets.length - 1); - const body = await asCurrentUser.search( - { - index: this.index, - size: 0, - body: searchBody, - ...(this.indicesOptions ?? {}), - }, - { maxRetries: 0 } - ); - return body; + let sumSquareTimeDiffResiduals = 0; + for (let i = 0; i < fullBuckets.length - 1; i++) { + sumSquareTimeDiffResiduals += Math.pow(timeDiffs[i] - meanTimeDiff, 2); } - // test that the coefficient of variation of time difference between non-empty buckets is small - isPolledData(fullBuckets, intervalMs) { - let pass = false; - - const timeDiffs = []; - let sumOfTimeDiffs = 0; - for (let i = 1; i < fullBuckets.length; i++) { - const diff = fullBuckets[i].key - fullBuckets[i - 1].key; - sumOfTimeDiffs += diff; - timeDiffs.push(diff); - } - - const meanTimeDiff = sumOfTimeDiffs / (fullBuckets.length - 1); - - let sumSquareTimeDiffResiduals = 0; - for (let i = 0; i < fullBuckets.length - 1; i++) { - sumSquareTimeDiffResiduals += Math.pow(timeDiffs[i] - meanTimeDiff, 2); - } - - const vari = sumSquareTimeDiffResiduals / (fullBuckets.length - 1); - - const cov = Math.sqrt(vari) / meanTimeDiff; - - if (cov < 0.1 && intervalMs < meanTimeDiff) { - pass = false; - } else { - pass = true; - } - return { - pass, - meanTimeDiff, - }; + const vari = sumSquareTimeDiffResiduals / (fullBuckets.length - 1); + + const cov = Math.sqrt(vari) / meanTimeDiff; + + if (cov < 0.1 && intervalMs < meanTimeDiff) { + pass = false; + } else { + pass = true; } + return { + pass, + meanTimeDiff, + }; } - - return PolledDataChecker; } diff --git a/x-pack/plugins/ml/server/models/bucket_span_estimator/single_series_checker.js b/x-pack/plugins/ml/server/models/bucket_span_estimator/single_series_checker.js index bc8077e44c72c..ea883f8b5a8ac 100644 --- a/x-pack/plugins/ml/server/models/bucket_span_estimator/single_series_checker.js +++ b/x-pack/plugins/ml/server/models/bucket_span_estimator/single_series_checker.js @@ -14,301 +14,299 @@ import { mlLog } from '../../lib/log'; import { INTERVALS, LONG_INTERVALS } from './intervals'; -export function singleSeriesCheckerFactory({ asCurrentUser }) { - const REF_DATA_INTERVAL = { name: '1h', ms: 3600000 }; +const REF_DATA_INTERVAL = { name: '1h', ms: 3600000 }; + +export class SingleSeriesChecker { + constructor( + asCurrentUser, + index, + timeField, + aggType, + field, + duration, + query, + thresholds, + runtimeMappings, + indicesOptions + ) { + this.index = index; + this.timeField = timeField; + this.aggType = aggType; + this.field = field; + this.duration = duration; + this.query = query; + this.thresholds = thresholds; + this.refMetricData = { + varValue: 0, + varDiff: 0, + created: false, + }; + this.runtimeMappings = runtimeMappings; + this.indicesOptions = indicesOptions; + this.asCurrentUser = asCurrentUser; + this.interval = null; + } - class SingleSeriesChecker { - constructor( - index, - timeField, - aggType, - field, - duration, - query, - thresholds, - runtimeMappings, - indicesOptions - ) { - this.index = index; - this.timeField = timeField; - this.aggType = aggType; - this.field = field; - this.duration = duration; - this.query = query; - this.thresholds = thresholds; - this.refMetricData = { - varValue: 0, - varDiff: 0, - created: false, + run() { + return new Promise((resolve, reject) => { + const start = () => { + // run all tests, returns a suggested interval + this.runTests() + .then((interval) => { + this.interval = interval; + resolve(this.interval); + }) + .catch((resp) => { + reject(resp); + }); }; - this.runtimeMappings = runtimeMappings; - this.indicesOptions = indicesOptions; - this.interval = null; - } - run() { - return new Promise((resolve, reject) => { - const start = () => { - // run all tests, returns a suggested interval - this.runTests() - .then((interval) => { - this.interval = interval; - resolve(this.interval); - }) - .catch((resp) => { - reject(resp); - }); - }; + // if a field has been selected, first create ref data used in metric check + if (this.field === null) { + start(); + } else { + this.createRefMetricData(REF_DATA_INTERVAL.ms) + .then(() => { + start(); + }) + .catch((resp) => { + mlLog.warn('SingleSeriesChecker: Could not load metric reference data'); + reject(resp); + }); + } + }); + } - // if a field has been selected, first create ref data used in metric check - if (this.field === null) { - start(); - } else { - this.createRefMetricData(REF_DATA_INTERVAL.ms) - .then(() => { - start(); - }) - .catch((resp) => { - mlLog.warn('SingleSeriesChecker: Could not load metric reference data'); - reject(resp); - }); + runTests() { + return new Promise((resolve, reject) => { + let count = 0; + + // create filtered copy of INTERVALS + // not including any buckets spans lower that the min threshold + // if the data has been detected as being polled, the min threshold + // is set to that poll interval + const intervals = []; + for (let i = 0; i < INTERVALS.length; i++) { + if (INTERVALS[i].ms >= this.thresholds.minimumBucketSpanMS) { + intervals.push(INTERVALS[i]); } - }); - } - - runTests() { - return new Promise((resolve, reject) => { - let count = 0; + } - // create filtered copy of INTERVALS - // not including any buckets spans lower that the min threshold - // if the data has been detected as being polled, the min threshold - // is set to that poll interval - const intervals = []; - for (let i = 0; i < INTERVALS.length; i++) { - if (INTERVALS[i].ms >= this.thresholds.minimumBucketSpanMS) { - intervals.push(INTERVALS[i]); + // if none of the normal intervals fit + // check the poll interval against longer bucket spans + // if any of these match, call resolve and skip all other tests + if (intervals.length === 0) { + let interval = null; + for (let i = 1; i < LONG_INTERVALS.length; i++) { + const int1 = LONG_INTERVALS[i - 1]; + const int2 = LONG_INTERVALS[i]; + if ( + this.thresholds.minimumBucketSpanMS > int1.ms && + this.thresholds.minimumBucketSpanMS <= int2.ms + ) { + // value is between two intervals, choose the highest + interval = int2; + break; } } - - // if none of the normal intervals fit - // check the poll interval against longer bucket spans - // if any of these match, call resolve and skip all other tests - if (intervals.length === 0) { - let interval = null; - for (let i = 1; i < LONG_INTERVALS.length; i++) { - const int1 = LONG_INTERVALS[i - 1]; - const int2 = LONG_INTERVALS[i]; - if ( - this.thresholds.minimumBucketSpanMS > int1.ms && - this.thresholds.minimumBucketSpanMS <= int2.ms - ) { - // value is between two intervals, choose the highest - interval = int2; - break; - } - } - if (interval !== null) { - resolve(interval); - return; - } + if (interval !== null) { + resolve(interval); + return; } + } - // recursive function called with the index of the INTERVALS array - // each time one of the checks fails, the index is increased and - // the tests are repeated. - const runTest = (i) => { - const interval = intervals[i]; - this.performSearch(interval.ms) - .then((resp) => { - const buckets = resp.aggregations.non_empty_buckets.buckets; - const fullBuckets = this.getFullBuckets(buckets); - if (fullBuckets.length) { - let pass = true; + // recursive function called with the index of the INTERVALS array + // each time one of the checks fails, the index is increased and + // the tests are repeated. + const runTest = (i) => { + const interval = intervals[i]; + this.performSearch(interval.ms) + .then((resp) => { + const buckets = resp.aggregations.non_empty_buckets.buckets; + const fullBuckets = this.getFullBuckets(buckets); + if (fullBuckets.length) { + let pass = true; - // test that the more than 20% of the buckets contain data - if (pass && this.testBucketPercentage(fullBuckets, buckets) === false) { - pass = false; - } + // test that the more than 20% of the buckets contain data + if (pass && this.testBucketPercentage(fullBuckets, buckets) === false) { + pass = false; + } - // test that the full buckets contain at least 5 documents - if (this.aggType === 'sum' || this.aggType === 'count') { - if (pass && this.testSumCountBuckets(fullBuckets) === false) { - pass = false; - } + // test that the full buckets contain at least 5 documents + if (this.aggType === 'sum' || this.aggType === 'count') { + if (pass && this.testSumCountBuckets(fullBuckets) === false) { + pass = false; } + } - // scale variation test - // only run this test for bucket spans less than 1 hour - if (this.refMetricData.created && this.field !== null && interval.ms < 3600000) { - if (pass && this.testMetricData(fullBuckets) === false) { - pass = false; - } + // scale variation test + // only run this test for bucket spans less than 1 hour + if (this.refMetricData.created && this.field !== null && interval.ms < 3600000) { + if (pass && this.testMetricData(fullBuckets) === false) { + pass = false; } + } - if (pass) { + if (pass) { + resolve(interval); + } else { + count++; + if (count === intervals.length) { resolve(interval); } else { - count++; - if (count === intervals.length) { - resolve(interval); - } else { - runTest(count); - } + runTest(count); } - } else { - mlLog.warn('SingleSeriesChecker: runTest stopped because fullBuckets is empty'); - reject('runTest stopped because fullBuckets is empty'); } - }) - .catch((resp) => { - // do something better with this - reject(resp); - }); - }; + } else { + mlLog.warn('SingleSeriesChecker: runTest stopped because fullBuckets is empty'); + reject('runTest stopped because fullBuckets is empty'); + } + }) + .catch((resp) => { + // do something better with this + reject(resp); + }); + }; - runTest(count); - }); - } + runTest(count); + }); + } - createSearch(intervalMs) { - const search = { - query: this.query, - aggs: { - non_empty_buckets: { - date_histogram: { - field: this.timeField, - fixed_interval: `${intervalMs}ms`, - }, + createSearch(intervalMs) { + const search = { + query: this.query, + aggs: { + non_empty_buckets: { + date_histogram: { + field: this.timeField, + fixed_interval: `${intervalMs}ms`, }, }, - ...this.runtimeMappings, - }; - - if (this.field !== null) { - search.aggs.non_empty_buckets.aggs = { - fieldValue: { - [this.aggType]: { - field: this.field, - }, + }, + ...this.runtimeMappings, + }; + + if (this.field !== null) { + search.aggs.non_empty_buckets.aggs = { + fieldValue: { + [this.aggType]: { + field: this.field, }, - }; - } - return search; - } - - async performSearch(intervalMs) { - const searchBody = this.createSearch(intervalMs); - - const body = await asCurrentUser.search( - { - index: this.index, - size: 0, - body: searchBody, - ...(this.indicesOptions ?? {}), }, - { maxRetries: 0 } - ); - return body; + }; } + return search; + } - getFullBuckets(buckets) { - const fullBuckets = []; - for (let i = 0; i < buckets.length; i++) { - if (buckets[i].doc_count > 0) { - fullBuckets.push(buckets[i]); - } + async performSearch(intervalMs) { + const searchBody = this.createSearch(intervalMs); + + const body = await this.asCurrentUser.search( + { + index: this.index, + size: 0, + body: searchBody, + ...(this.indicesOptions ?? {}), + }, + { maxRetries: 0 } + ); + return body; + } + + getFullBuckets(buckets) { + const fullBuckets = []; + for (let i = 0; i < buckets.length; i++) { + if (buckets[i].doc_count > 0) { + fullBuckets.push(buckets[i]); } - return fullBuckets; } + return fullBuckets; + } - // test that the more than 20% of the buckets contain data - testBucketPercentage(fullBuckets, buckets) { - const pcnt = fullBuckets.length / buckets.length; - return pcnt > 0.2; + // test that the more than 20% of the buckets contain data + testBucketPercentage(fullBuckets, buckets) { + const pcnt = fullBuckets.length / buckets.length; + return pcnt > 0.2; + } + + // test that the full buckets contain at least 5 documents + testSumCountBuckets(fullBuckets) { + let totalCount = 0; + for (let i = 0; i < fullBuckets.length; i++) { + totalCount += fullBuckets[i].doc_count; } + const mean = totalCount / fullBuckets.length; + return mean >= 5; + } - // test that the full buckets contain at least 5 documents - testSumCountBuckets(fullBuckets) { - let totalCount = 0; - for (let i = 0; i < fullBuckets.length; i++) { - totalCount += fullBuckets[i].doc_count; - } - const mean = totalCount / fullBuckets.length; - return mean >= 5; + // create the metric data used for the metric test and the metric test 1hr reference data + createMetricData(fullBuckets) { + const valueDiffs = []; + let sumOfValues = fullBuckets[0].fieldValue.value; + let sumOfValueDiffs = 0; + for (let i = 1; i < fullBuckets.length; i++) { + const value = fullBuckets[i].fieldValue.value; + const diff = value - fullBuckets[i - 1].fieldValue.value; + sumOfValueDiffs += diff; + valueDiffs.push(diff); + sumOfValues += value; } - // create the metric data used for the metric test and the metric test 1hr reference data - createMetricData(fullBuckets) { - const valueDiffs = []; - let sumOfValues = fullBuckets[0].fieldValue.value; - let sumOfValueDiffs = 0; - for (let i = 1; i < fullBuckets.length; i++) { - const value = fullBuckets[i].fieldValue.value; - const diff = value - fullBuckets[i - 1].fieldValue.value; - sumOfValueDiffs += diff; - valueDiffs.push(diff); - sumOfValues += value; - } + const meanValue = sumOfValues / fullBuckets.length; + const meanValueDiff = sumOfValueDiffs / (fullBuckets.length - 1); - const meanValue = sumOfValues / fullBuckets.length; - const meanValueDiff = sumOfValueDiffs / (fullBuckets.length - 1); + let sumOfSquareValueResiduals = 0; + let sumOfSquareValueDiffResiduals = 0; + for (let i = 0; i < fullBuckets.length - 1; i++) { + sumOfSquareValueResiduals += Math.pow(fullBuckets[i].fieldValue.value - meanValue, 2); + sumOfSquareValueDiffResiduals += Math.pow(valueDiffs[i] - meanValueDiff, 2); + } + sumOfSquareValueResiduals += Math.pow( + fullBuckets[fullBuckets.length - 1].fieldValue.value - meanValue, + 2 + ); + + const varValue = sumOfSquareValueResiduals / fullBuckets.length; + const varDiff = sumOfSquareValueDiffResiduals / (fullBuckets.length - 1); + + return { + varValue, + varDiff, + }; + } - let sumOfSquareValueResiduals = 0; - let sumOfSquareValueDiffResiduals = 0; - for (let i = 0; i < fullBuckets.length - 1; i++) { - sumOfSquareValueResiduals += Math.pow(fullBuckets[i].fieldValue.value - meanValue, 2); - sumOfSquareValueDiffResiduals += Math.pow(valueDiffs[i] - meanValueDiff, 2); + // create reference data for the scale variation test + createRefMetricData(intervalMs) { + return new Promise((resolve, reject) => { + if (this.field === null) { + resolve(); + return; } - sumOfSquareValueResiduals += Math.pow( - fullBuckets[fullBuckets.length - 1].fieldValue.value - meanValue, - 2 - ); - - const varValue = sumOfSquareValueResiduals / fullBuckets.length; - const varDiff = sumOfSquareValueDiffResiduals / (fullBuckets.length - 1); - return { - varValue, - varDiff, - }; - } + this.performSearch(intervalMs) // 1h + .then((resp) => { + const buckets = resp.aggregations.non_empty_buckets.buckets; + const fullBuckets = this.getFullBuckets(buckets); + if (fullBuckets.length) { + this.refMetricData = this.createMetricData(fullBuckets); + this.refMetricData.created = true; + } - // create reference data for the scale variation test - createRefMetricData(intervalMs) { - return new Promise((resolve, reject) => { - if (this.field === null) { resolve(); - return; - } - - this.performSearch(intervalMs) // 1h - .then((resp) => { - const buckets = resp.aggregations.non_empty_buckets.buckets; - const fullBuckets = this.getFullBuckets(buckets); - if (fullBuckets.length) { - this.refMetricData = this.createMetricData(fullBuckets); - this.refMetricData.created = true; - } - - resolve(); - }) - .catch((resp) => { - reject(resp); - }); - }); - } - - // scale variation test - testMetricData(fullBuckets) { - const metricData = this.createMetricData(fullBuckets); - const stat = - metricData.varDiff / - metricData.varValue / - (this.refMetricData.varDiff / this.refMetricData.varValue); - return stat <= 5; - } + }) + .catch((resp) => { + reject(resp); + }); + }); } - return SingleSeriesChecker; + // scale variation test + testMetricData(fullBuckets) { + const metricData = this.createMetricData(fullBuckets); + const stat = + metricData.varDiff / + metricData.varValue / + (this.refMetricData.varDiff / this.refMetricData.varValue); + return stat <= 5; + } } diff --git a/x-pack/plugins/ml/server/models/job_validation/job_validation.ts b/x-pack/plugins/ml/server/models/job_validation/job_validation.ts index 9f3cce388dc9d..9ee8bd350365c 100644 --- a/x-pack/plugins/ml/server/models/job_validation/job_validation.ts +++ b/x-pack/plugins/ml/server/models/job_validation/job_validation.ts @@ -13,7 +13,6 @@ import { getMessages, MessageId, JobValidationMessage } from '../../../common/co import { VALIDATION_STATUS } from '../../../common/constants/validation'; import { basicJobValidation, uniqWithIsEqual } from '../../../common/util/job_utils'; -// @ts-expect-error importing js file import { validateBucketSpan } from './validate_bucket_span'; import { validateCardinality } from './validate_cardinality'; import { validateInfluencers } from './validate_influencers'; @@ -86,9 +85,7 @@ export async function validateJob( return messages[m.id as MessageId].status === VALIDATION_STATUS.ERROR; }); - validationMessages.push( - ...(await validateBucketSpan(client, job, duration, isSecurityDisabled)) - ); + validationMessages.push(...(await validateBucketSpan(client, job, duration))); validationMessages.push(...(await validateTimeRange(client, job, duration))); // only run the influencer and model memory limit checks diff --git a/x-pack/plugins/ml/tsconfig.json b/x-pack/plugins/ml/tsconfig.json index ff4bd0825cea9..21897ae7ba4f4 100644 --- a/x-pack/plugins/ml/tsconfig.json +++ b/x-pack/plugins/ml/tsconfig.json @@ -4,11 +4,6 @@ "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, - // there is still a decent amount of JS in this plugin and we are taking - // advantage of the fact that TS doesn't know the types of that code and - // gives us `any`. Once that code is converted to .ts we can remove this - // and allow TS to infer types from any JS file imported. - "allowJs": false }, "include": [ "common/**/*", From d5ed16a86e105e3cd1418fa1a42a3745a46e0a9c Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Thu, 17 Nov 2022 13:24:47 -0700 Subject: [PATCH 02/78] [controls] fix Time Slider text is not working properly with Dark Mode (#145612) Fixes https://github.com/elastic/kibana/issues/145594 TimeSlider component is not wrapped by KibanaThemeProvider and therefore does not properly use kibana themeing. This PR resolves the issues by wrapping TimeSlider by KibanaThemeProvider. To test * set advanced setting `theme:darkMode` to true * open dashboard * add time slider * verify timeslider is using dark theme Screen Shot 2022-11-17 at 12 04 40 PM --- .../embeddable/time_slider_embeddable.tsx | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/plugins/controls/public/time_slider/embeddable/time_slider_embeddable.tsx b/src/plugins/controls/public/time_slider/embeddable/time_slider_embeddable.tsx index 8122bbfda7813..ffb7bd2610fc5 100644 --- a/src/plugins/controls/public/time_slider/embeddable/time_slider_embeddable.tsx +++ b/src/plugins/controls/public/time_slider/embeddable/time_slider_embeddable.tsx @@ -12,6 +12,7 @@ import moment from 'moment-timezone'; import { Embeddable, IContainer } from '@kbn/embeddable-plugin/public'; import { ReduxEmbeddableTools, ReduxEmbeddablePackage } from '@kbn/presentation-util-plugin/public'; import type { TimeRange } from '@kbn/es-query'; +import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; import React from 'react'; import ReactDOM from 'react-dom'; import { Subscription } from 'rxjs'; @@ -270,16 +271,18 @@ export class TimeSliderControlEmbeddable extends Embeddable< const { Wrapper: TimeSliderControlReduxWrapper } = this.reduxEmbeddableTools; ReactDOM.render( - - { - this.onTimesliceChange(value); - const range = value ? value[TO_INDEX] - value[FROM_INDEX] : undefined; - this.onRangeChange(range); - }} - /> - , + + + { + this.onTimesliceChange(value); + const range = value ? value[TO_INDEX] - value[FROM_INDEX] : undefined; + this.onRangeChange(range); + }} + /> + + , node ); }; From 8c26375dfeb5c67d7398493d91d034ca84d0dc20 Mon Sep 17 00:00:00 2001 From: Marshall Main <55718608+marshallmain@users.noreply.github.com> Date: Thu, 17 Nov 2022 13:02:10 -0800 Subject: [PATCH 03/78] [Security Solution][Alerts] Sort alert results to fix flaky test (#145501) ## Summary Fixes https://github.com/elastic/kibana/issues/143992 Alert results can sometimes come back in different order if the sort is not specified, causing occasional test failures. --- .../security_and_spaces/rule_execution_logic/threshold.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/threshold.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/threshold.ts index e3294ae9a8156..45b24902e8bc5 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/threshold.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/threshold.ts @@ -373,7 +373,7 @@ export default ({ getService }: FtrProviderContext) => { }, }; const { previewId } = await previewRule({ supertest, rule }); - const previewAlerts = await getPreviewAlerts({ es, previewId }); + const previewAlerts = await getPreviewAlerts({ es, previewId, sort: ['host.name'] }); expect(previewAlerts[0]?._source?.host?.risk?.calculated_level).to.eql('Low'); expect(previewAlerts[0]?._source?.host?.risk?.calculated_score_norm).to.eql(20); From 2f68c14b148f00a9a7b64c28dc86295ec055a3a7 Mon Sep 17 00:00:00 2001 From: Marshall Main <55718608+marshallmain@users.noreply.github.com> Date: Thu, 17 Nov 2022 13:02:27 -0800 Subject: [PATCH 04/78] [Security Solution][Alerts] Unskip ML rule test suite (#145503) ## Summary Fixes https://github.com/elastic/kibana/issues/142993 The failure is in ES archiver, not the test, and there's seemingly nothing wrong with the ES archive. Passed 100x here: https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/1564 --- .../rule_execution_logic/machine_learning.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/machine_learning.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/machine_learning.ts index 0949d8255bed2..5a0f8fbf747b7 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/machine_learning.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/machine_learning.ts @@ -64,8 +64,7 @@ export default ({ getService }: FtrProviderContext) => { rule_id: 'ml-rule-id', }; - // FLAKY: https://github.com/elastic/kibana/issues/142993 - describe.skip('Machine learning type rules', () => { + describe('Machine learning type rules', () => { before(async () => { // Order is critical here: auditbeat data must be loaded before attempting to start the ML job, // as the job looks for certain indices on start From e33392bff0a022bb246f8d560a336ebdac0555f9 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Thu, 17 Nov 2022 22:32:39 +0100 Subject: [PATCH 05/78] [Discover] Fix theme for Alerts popover (#145390) Closes https://github.com/elastic/kibana/issues/143070 ## Summary This PR adds theme provider to the Alerts popover. Dark theme: Screenshot 2022-11-16 at 16 37 49 Light theme: Screenshot 2022-11-16 at 16 38 17 Co-authored-by: Matthias Wilhelm --- .../components/top_nav/get_top_nav_links.tsx | 1 + .../top_nav/open_alerts_popover.tsx | 29 +++++++++++-------- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx b/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx index c72c0539fc593..23e7176cb2845 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx @@ -81,6 +81,7 @@ export const getTopNavLinks = ({ run: async (anchorElement: HTMLElement) => { openAlertsPopover({ I18nContext: services.core.i18n.Context, + theme$: services.core.theme.theme$, anchorElement, searchSource: savedSearch.searchSource, services, diff --git a/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx b/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx index bf484452b731f..18a4922a908b5 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx @@ -8,11 +8,12 @@ import React, { useCallback, useState, useMemo } from 'react'; import ReactDOM from 'react-dom'; -import { I18nStart } from '@kbn/core/public'; +import type { Observable } from 'rxjs'; +import type { CoreTheme, I18nStart } from '@kbn/core/public'; import { EuiWrappingPopover, EuiContextMenu } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import type { DataView, ISearchSource } from '@kbn/data-plugin/common'; -import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; +import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; import { DiscoverServices } from '../../../../build_services'; import { updateSearchSource } from '../../utils/update_search_source'; @@ -177,6 +178,7 @@ function closeAlertsPopover() { export function openAlertsPopover({ I18nContext, + theme$, anchorElement, searchSource, services, @@ -185,6 +187,7 @@ export function openAlertsPopover({ updateDataViewList, }: { I18nContext: I18nStart['Context']; + theme$: Observable; anchorElement: HTMLElement; searchSource: ISearchSource; services: DiscoverServices; @@ -203,16 +206,18 @@ export function openAlertsPopover({ const element = ( - + + + ); From 7e5bf37e794127f028bed8efc595c3b4d5282850 Mon Sep 17 00:00:00 2001 From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com> Date: Thu, 17 Nov 2022 16:47:59 -0500 Subject: [PATCH 06/78] [Security Solution][Endpoint] Enhance the Endpoint Authz service so that it also calculates access to delete Host Isolation Exceptions (#145123) ## Summary - Enhances the Endpoint Authz services (both UI and Server) so that it correctly sets access to the Host Isolation Exceptions in a license downgrade use case. - Deletes the `useCanSeeHostIsolationExceptionsMenu()` hook - no longer needed. `authz.canReadHostIsolationExceptions` should be used instead - Consolidates the server side to using only `endpointAppContextServices.getEndpointAuthz()` for determining authz (removed implementation an implementation from `RequestContextFactory`) - Updated `ArtifactListPage` Empty list component to only show the `Create` button if user is allowed to create - Converted `RequestContextFactory` `endpointAuth` to an async method (`getEndpiontAuthz()` - The default Security Solution `QueryClient` is now exposed and can be used to create non-hook utilities that utilize react-query for http requests ### Testing The following scenarios were tested manually to ensure continued proper behaviour: > **NOTE:** only the `endpointRbacV1Enabled` FF was enabled for these to mimic v8.6 runtime 1. Downgrade license and ensure HIE can be still be accessed from both Fleet (cards) and Security Solution **IF** HIE entries exist 2. Ensure HIE is not accessible if License is not Platinum++ 3. Created a non-security solution user (ex. only access to observability) and logged in: 1. With Enterprise+ license: ensured no browser API failures to the Lists plugin (normally needed to validate access to HIC 1. With Basic license: ensured no browser API failures to the Lists plugin (normally needed to validate access to HIC Co-authored-by: Ashokaditya <1849116+ashokaditya@users.noreply.github.com> --- .../endpoint/service/authz/authz.test.ts | 15 +++ .../common/endpoint/service/authz/authz.ts | 91 ++++++++++++----- .../common/endpoint/service/authz/mocks.ts | 2 +- .../common/endpoint/types/authz.ts | 6 ++ .../index.test.tsx | 19 ++-- .../use_navigation_items.tsx | 14 ++- .../endpoint/use_endpoint_privileges.test.ts | 74 ++++++++++++-- .../endpoint/use_endpoint_privileges.ts | 98 ++++++++++--------- .../query_client/query_client_provider.tsx | 10 +- .../common/lib/kibana/__mocks__/index.ts | 9 +- .../artifact_list_page/artifact_list_page.tsx | 1 + .../components/no_data_empty_state.tsx | 28 +++--- .../public/management/links.test.ts | 32 +++--- .../public/management/links.ts | 93 +++++++++--------- .../mocks/exceptions_list_http_mocks.ts | 26 ++++- .../view/hooks.test.ts | 93 ------------------ .../host_isolation_exceptions/view/hooks.ts | 46 --------- .../view/host_isolation_exceptions_list.tsx | 9 +- .../host_isolation_exceptions_list.test.tsx | 5 +- .../public/management/pages/index.tsx | 6 +- .../pages/integration_tests/index.test.tsx | 4 - ...endpoint_package_custom_extension.test.tsx | 3 +- .../endpoint_package_custom_extension.tsx | 9 +- .../check_artifact_has_data.ts | 38 +++++++ .../endpoint/endpoint_app_context_services.ts | 19 +++- .../actions/file_download_handler.test.ts | 6 +- .../routes/actions/file_info_handler.test.ts | 6 +- .../endpoint/routes/actions/list.test.ts | 14 ++- .../routes/actions/response_actions.test.ts | 8 +- .../routes/with_endpoint_authz.test.ts | 11 ++- .../endpoint/routes/with_endpoint_authz.ts | 2 +- .../does_artifact_have_data.ts | 40 ++++++++ .../artifacts_exception_list/index.ts | 8 ++ .../server/endpoint/services/index.ts | 1 + .../routes/__mocks__/request_context.ts | 4 +- .../server/request_context_factory.ts | 54 ++-------- .../plugins/security_solution/server/types.ts | 3 +- 37 files changed, 501 insertions(+), 406 deletions(-) delete mode 100644 x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/hooks.test.ts delete mode 100644 x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/hooks.ts create mode 100644 x-pack/plugins/security_solution/public/management/services/exceptions_list/check_artifact_has_data.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/services/artifacts_exception_list/does_artifact_have_data.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/services/artifacts_exception_list/index.ts diff --git a/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.test.ts b/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.test.ts index 7ee477e3076c8..8305d2aa08ae1 100644 --- a/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.test.ts +++ b/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.test.ts @@ -83,6 +83,20 @@ describe('Endpoint Authz service', () => { true ); }); + + it(`should allow Host Isolation Exception read/delete when license is not Platinum+, but entries exist`, () => { + licenseService.isPlatinumPlus.mockReturnValue(false); + + expect( + calculateEndpointAuthz(licenseService, fleetAuthz, userRoles, false, undefined, true) + ).toEqual( + expect.objectContaining({ + canWriteHostIsolationExceptions: false, + canReadHostIsolationExceptions: true, + canDeleteHostIsolationExceptions: true, + }) + ); + }); }); describe('and `fleet.all` access is false', () => { @@ -206,6 +220,7 @@ describe('Endpoint Authz service', () => { canAccessFleet: false, canAccessEndpointManagement: false, canCreateArtifactsByPolicy: false, + canDeleteHostIsolationExceptions: false, canWriteEndpointList: false, canReadEndpointList: false, canWritePolicyManagement: false, diff --git a/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.ts b/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.ts index 0bf21e4734ba2..f4a2c9894108d 100644 --- a/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.ts +++ b/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.ts @@ -19,14 +19,26 @@ export function defaultEndpointPermissions(): EndpointPermissions { }; } -function hasPermission( +/** + * Checks to see if a given Kibana privilege was granted. + * Note that this only checks if the user has the privilege as part of their role. That + * does not indicate that the user has the granted functionality behind that privilege + * (ex. due to license level). To get an accurate representation of user's authorization + * level, use `calculateEndpointAuthz()` + * + * @param fleetAuthz + * @param isEndpointRbacEnabled + * @param isSuperuser + * @param privilege + */ +export function hasKibanaPrivilege( fleetAuthz: FleetAuthz, isEndpointRbacEnabled: boolean, - hasEndpointManagementAccess: boolean, + isSuperuser: boolean, privilege: typeof ENDPOINT_PRIVILEGES[number] ): boolean { // user is superuser, always return true - if (hasEndpointManagementAccess) { + if (isSuperuser) { return true; } @@ -46,19 +58,24 @@ function hasPermission( * @param licenseService * @param fleetAuthz * @param userRoles + * @param isEndpointRbacEnabled + * @param permissions + * @param hasHostIsolationExceptionsItems if set to `true`, then Host Isolation Exceptions related authz properties + * may be adjusted to account for a license downgrade scenario */ export const calculateEndpointAuthz = ( licenseService: LicenseService, fleetAuthz: FleetAuthz, userRoles: MaybeImmutable, isEndpointRbacEnabled: boolean = false, - permissions: Partial = defaultEndpointPermissions() + permissions: Partial = defaultEndpointPermissions(), + hasHostIsolationExceptionsItems: boolean = false ): EndpointAuthz => { const isPlatinumPlusLicense = licenseService.isPlatinumPlus(); const isEnterpriseLicense = licenseService.isEnterprise(); const hasEndpointManagementAccess = userRoles.includes('superuser'); const { canWriteSecuritySolution = false, canReadSecuritySolution = false } = permissions; - const canWriteEndpointList = hasPermission( + const canWriteEndpointList = hasKibanaPrivilege( fleetAuthz, isEndpointRbacEnabled, hasEndpointManagementAccess, @@ -66,13 +83,13 @@ export const calculateEndpointAuthz = ( ); const canReadEndpointList = canWriteEndpointList || - hasPermission( + hasKibanaPrivilege( fleetAuthz, isEndpointRbacEnabled, hasEndpointManagementAccess, 'readEndpointList' ); - const canWritePolicyManagement = hasPermission( + const canWritePolicyManagement = hasKibanaPrivilege( fleetAuthz, isEndpointRbacEnabled, hasEndpointManagementAccess, @@ -80,13 +97,13 @@ export const calculateEndpointAuthz = ( ); const canReadPolicyManagement = canWritePolicyManagement || - hasPermission( + hasKibanaPrivilege( fleetAuthz, isEndpointRbacEnabled, hasEndpointManagementAccess, 'readPolicyManagement' ); - const canWriteActionsLogManagement = hasPermission( + const canWriteActionsLogManagement = hasKibanaPrivilege( fleetAuthz, isEndpointRbacEnabled, hasEndpointManagementAccess, @@ -94,25 +111,25 @@ export const calculateEndpointAuthz = ( ); const canReadActionsLogManagement = canWriteActionsLogManagement || - hasPermission( + hasKibanaPrivilege( fleetAuthz, isEndpointRbacEnabled, hasEndpointManagementAccess, 'readActionsLogManagement' ); - const canIsolateHost = hasPermission( + const canIsolateHost = hasKibanaPrivilege( fleetAuthz, isEndpointRbacEnabled, hasEndpointManagementAccess, 'writeHostIsolation' ); - const canWriteProcessOperations = hasPermission( + const canWriteProcessOperations = hasKibanaPrivilege( fleetAuthz, isEndpointRbacEnabled, hasEndpointManagementAccess, 'writeProcessOperations' ); - const canWriteTrustedApplications = hasPermission( + const canWriteTrustedApplications = hasKibanaPrivilege( fleetAuthz, isEndpointRbacEnabled, hasEndpointManagementAccess, @@ -120,27 +137,46 @@ export const calculateEndpointAuthz = ( ); const canReadTrustedApplications = canWriteTrustedApplications || - hasPermission( + hasKibanaPrivilege( fleetAuthz, isEndpointRbacEnabled, hasEndpointManagementAccess, 'readTrustedApplications' ); - const canWriteHostIsolationExceptions = hasPermission( + + const hasWriteHostIsolationExceptionsPermission = hasKibanaPrivilege( fleetAuthz, isEndpointRbacEnabled, hasEndpointManagementAccess, 'writeHostIsolationExceptions' ); - const canReadHostIsolationExceptions = - canWriteHostIsolationExceptions || - hasPermission( + const canWriteHostIsolationExceptions = + hasWriteHostIsolationExceptionsPermission && isPlatinumPlusLicense; + + const hasReadHostIsolationExceptionsPermission = + hasWriteHostIsolationExceptionsPermission || + hasKibanaPrivilege( fleetAuthz, isEndpointRbacEnabled, hasEndpointManagementAccess, 'readHostIsolationExceptions' ); - const canWriteBlocklist = hasPermission( + // Calculate the Host Isolation Exceptions Authz. Some of these authz properties could be + // set to `true` in cases where license was downgraded, but entries still exist. + const canReadHostIsolationExceptions = + canWriteHostIsolationExceptions || + (hasReadHostIsolationExceptionsPermission && + // We still allow `read` if not Platinum license, but entries exists for HIE + (isPlatinumPlusLicense || hasHostIsolationExceptionsItems)); + + const canDeleteHostIsolationExceptions = + canWriteHostIsolationExceptions || + // Should be able to delete if host isolation exceptions exists and license is not platinum+ + (hasWriteHostIsolationExceptionsPermission && + !isPlatinumPlusLicense && + hasHostIsolationExceptionsItems); + + const canWriteBlocklist = hasKibanaPrivilege( fleetAuthz, isEndpointRbacEnabled, hasEndpointManagementAccess, @@ -148,8 +184,13 @@ export const calculateEndpointAuthz = ( ); const canReadBlocklist = canWriteBlocklist || - hasPermission(fleetAuthz, isEndpointRbacEnabled, hasEndpointManagementAccess, 'readBlocklist'); - const canWriteEventFilters = hasPermission( + hasKibanaPrivilege( + fleetAuthz, + isEndpointRbacEnabled, + hasEndpointManagementAccess, + 'readBlocklist' + ); + const canWriteEventFilters = hasKibanaPrivilege( fleetAuthz, isEndpointRbacEnabled, hasEndpointManagementAccess, @@ -157,13 +198,13 @@ export const calculateEndpointAuthz = ( ); const canReadEventFilters = canWriteEventFilters || - hasPermission( + hasKibanaPrivilege( fleetAuthz, isEndpointRbacEnabled, hasEndpointManagementAccess, 'readEventFilters' ); - const canWriteFileOperations = hasPermission( + const canWriteFileOperations = hasKibanaPrivilege( fleetAuthz, isEndpointRbacEnabled, hasEndpointManagementAccess, @@ -195,8 +236,9 @@ export const calculateEndpointAuthz = ( // artifacts canWriteTrustedApplications, canReadTrustedApplications, - canWriteHostIsolationExceptions: canWriteHostIsolationExceptions && isPlatinumPlusLicense, + canWriteHostIsolationExceptions, canReadHostIsolationExceptions, + canDeleteHostIsolationExceptions, canWriteBlocklist, canReadBlocklist, canWriteEventFilters, @@ -227,6 +269,7 @@ export const getEndpointAuthzInitialState = (): EndpointAuthz => { canReadTrustedApplications: false, canWriteHostIsolationExceptions: false, canReadHostIsolationExceptions: false, + canDeleteHostIsolationExceptions: false, canWriteBlocklist: false, canReadBlocklist: false, canWriteEventFilters: false, diff --git a/x-pack/plugins/security_solution/common/endpoint/service/authz/mocks.ts b/x-pack/plugins/security_solution/common/endpoint/service/authz/mocks.ts index a1ea4c9c4847b..0abfe82b3f4d2 100644 --- a/x-pack/plugins/security_solution/common/endpoint/service/authz/mocks.ts +++ b/x-pack/plugins/security_solution/common/endpoint/service/authz/mocks.ts @@ -16,7 +16,7 @@ export const getEndpointAuthzInitialStateMock = ( Object.entries(getEndpointAuthzInitialState()) as Array<[keyof EndpointAuthz, boolean]> ).reduce((mockPrivileges, [key, value]) => { // Invert the initial values (from `false` to `true`) so that everything is authorized - mockPrivileges[key] = !value; + mockPrivileges[key] = true; return mockPrivileges; }, {} as EndpointAuthz), diff --git a/x-pack/plugins/security_solution/common/endpoint/types/authz.ts b/x-pack/plugins/security_solution/common/endpoint/types/authz.ts index 838edc695c540..fbfa97ad73328 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/authz.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/authz.ts @@ -60,6 +60,12 @@ export interface EndpointAuthz extends EndpointPermissions { canWriteHostIsolationExceptions: boolean; /** if user has read permissions for host isolation exceptions */ canReadHostIsolationExceptions: boolean; + /** + * if user has permissions to delete host isolation exceptions. This could be set to true, while + * `canWriteHostIsolationExceptions` is false in cases where the license might have been downgraded. + * In that use case, users should still be allowed to ONLY delete entries. + */ + canDeleteHostIsolationExceptions: boolean; /** if user has write permissions for blocklist entries */ canWriteBlocklist: boolean; /** if user has read permissions for blocklist entries */ 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 1d13d100b4d88..280d56d82e228 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 @@ -15,7 +15,6 @@ import { useRouteSpy } from '../../../utils/route/use_route_spy'; import { useIsExperimentalFeatureEnabled } from '../../../hooks/use_experimental_features'; import { TestProviders } from '../../../mock'; import { CASES_FEATURE_ID } from '../../../../../common/constants'; -import { useCanSeeHostIsolationExceptionsMenu } from '../../../../management/pages/host_isolation_exceptions/view/hooks'; import { useTourContext } from '../../guided_onboarding_tour'; import { useUserPrivileges } from '../../user_privileges'; import { @@ -24,6 +23,8 @@ import { readCasesPermissions, } from '../../../../cases_test_utils'; import { mockCasesContract } from '@kbn/cases-plugin/public/mocks'; +import { getEndpointAuthzInitialStateMock } from '../../../../../common/endpoint/service/authz/mocks'; +import { getUserPrivilegesMockDefaultValue } from '../../user_privileges/__mocks__'; jest.mock('../../../lib/kibana/kibana_react'); jest.mock('../../../lib/kibana'); @@ -37,7 +38,6 @@ mockUseGetUserCasesPermissions.mockImplementation(originalKibanaLib.useGetUserCa jest.mock('../../../hooks/use_selector'); jest.mock('../../../hooks/use_experimental_features'); jest.mock('../../../utils/route/use_route_spy'); -jest.mock('../../../../management/pages/host_isolation_exceptions/view/hooks'); jest.mock('../../guided_onboarding_tour'); jest.mock('../../user_privileges'); @@ -59,10 +59,7 @@ describe('useSecuritySolutionNavigation', () => { beforeEach(() => { (useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(false); (useRouteSpy as jest.Mock).mockReturnValue(mockRouteSpy); - (useCanSeeHostIsolationExceptionsMenu as jest.Mock).mockReturnValue(true); - mockUseUserPrivileges.mockImplementation(() => ({ - endpointPrivileges: { canReadActionsLogManagement: true }, - })); + mockUseUserPrivileges.mockImplementation(getUserPrivilegesMockDefaultValue); (useTourContext as jest.Mock).mockReturnValue({ isTourShown: false }); const cases = mockCasesContract(); @@ -113,8 +110,12 @@ describe('useSecuritySolutionNavigation', () => { expect(result?.current?.items?.[1].items?.[4].id).toEqual(SecurityPageName.kubernetes); }); - it('should omit host isolation exceptions if hook reports false', () => { - (useCanSeeHostIsolationExceptionsMenu as jest.Mock).mockReturnValue(false); + it('should omit host isolation exceptions if no authz', () => { + mockUseUserPrivileges.mockImplementation(() => ({ + endpointPrivileges: getEndpointAuthzInitialStateMock({ + canReadHostIsolationExceptions: false, + }), + })); const { result } = renderHook<{}, KibanaPageTemplateProps['solutionNav']>( () => useSecuritySolutionNavigation(), { wrapper: TestProviders } @@ -130,7 +131,7 @@ describe('useSecuritySolutionNavigation', () => { it('should omit response actions history if hook reports false', () => { mockUseUserPrivileges.mockImplementation(() => ({ - endpointPrivileges: { canReadActionsLogManagement: false }, + endpointPrivileges: getEndpointAuthzInitialStateMock({ canReadActionsLogManagement: false }), })); const { result } = renderHook<{}, KibanaPageTemplateProps['solutionNav']>( () => useSecuritySolutionNavigation(), 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 3c043950b758c..3bc1e9c66205e 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 @@ -18,7 +18,6 @@ import { useNavigation } from '../../../lib/kibana/hooks'; import type { NavTab } from '../types'; import { SecurityNavGroupKey } from '../types'; import { SecurityPageName } from '../../../../../common/constants'; -import { useCanSeeHostIsolationExceptionsMenu } from '../../../../management/pages/host_isolation_exceptions/view/hooks'; import { useIsExperimentalFeatureEnabled } from '../../../hooks/use_experimental_features'; import { useGlobalQueryString } from '../../../utils/global_query_string'; import { useUserPrivileges } from '../../user_privileges'; @@ -71,9 +70,8 @@ export const usePrimaryNavigationItems = ({ function usePrimaryNavigationItemsToDisplay(navTabs: Record) { const hasCasesReadPermissions = useGetUserCasesPermissions().read; - const canSeeHostIsolationExceptions = useCanSeeHostIsolationExceptionsMenu(); - const canSeeResponseActionsHistory = - useUserPrivileges().endpointPrivileges.canReadActionsLogManagement; + const { canReadActionsLogManagement, canReadHostIsolationExceptions } = + useUserPrivileges().endpointPrivileges; const isPolicyListEnabled = useIsExperimentalFeatureEnabled('policyListEnabled'); const uiCapabilities = useKibana().services.application.capabilities; @@ -138,11 +136,11 @@ function usePrimaryNavigationItemsToDisplay(navTabs: Record) { ...(isPolicyListEnabled ? [navTabs[SecurityPageName.policies]] : []), navTabs[SecurityPageName.trustedApps], navTabs[SecurityPageName.eventFilters], - ...(canSeeHostIsolationExceptions + ...(canReadHostIsolationExceptions ? [navTabs[SecurityPageName.hostIsolationExceptions]] : []), navTabs[SecurityPageName.blocklist], - ...(canSeeResponseActionsHistory + ...(canReadActionsLogManagement ? [navTabs[SecurityPageName.responseActionsHistory]] : []), navTabs[SecurityPageName.cloudSecurityPostureBenchmarks], @@ -161,8 +159,8 @@ function usePrimaryNavigationItemsToDisplay(navTabs: Record) { uiCapabilities.siem.show, navTabs, hasCasesReadPermissions, - canSeeHostIsolationExceptions, - canSeeResponseActionsHistory, + canReadHostIsolationExceptions, + canReadActionsLogManagement, isPolicyListEnabled, ] ); diff --git a/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/use_endpoint_privileges.test.ts b/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/use_endpoint_privileges.test.ts index b9df2e976966a..ba3bb55afedc0 100644 --- a/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/use_endpoint_privileges.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/use_endpoint_privileges.test.ts @@ -13,13 +13,16 @@ import type { AuthenticatedUser } from '@kbn/security-plugin/common'; import { createFleetAuthzMock } from '@kbn/fleet-plugin/common'; import type { EndpointPrivileges } from '../../../../../common/endpoint/types'; -import { useCurrentUser, useKibana } from '../../../lib/kibana'; +import { useCurrentUser, useKibana, useHttp as _useHttp } from '../../../lib/kibana'; import { licenseService } from '../../../hooks/use_license'; import { useEndpointPrivileges } from './use_endpoint_privileges'; import { getEndpointPrivilegesInitialStateMock } from './mocks'; import { getEndpointPrivilegesInitialState } from './utils'; +import { exceptionsListAllHttpMocks } from '../../../../management/mocks'; +import { getDeferred } from '../../../../management/mocks/utils'; +import { waitFor } from '@testing-library/react'; +import type { HttpFetchOptionsWithPath, HttpSetup } from '@kbn/core-http-browser'; -const useKibanaMock = useKibana as jest.Mocked; jest.mock('../../../lib/kibana'); jest.mock('../../../hooks/use_license', () => { const licenseServiceInstance = { @@ -37,6 +40,8 @@ jest.mock('../../../hooks/use_experimental_features', () => ({ useIsExperimentalFeatureEnabled: jest.fn((feature: string) => feature === 'endpointRbacEnabled'), })); +const useKibanaMock = useKibana as jest.Mocked; +const useHttpMock = _useHttp as jest.Mock; const licenseServiceMock = licenseService as jest.Mocked; describe('When using useEndpointPrivileges hook', () => { @@ -72,7 +77,7 @@ describe('When using useEndpointPrivileges hook', () => { }); afterEach(() => { - jest.resetAllMocks(); + jest.clearAllMocks(); unmount(); }); @@ -80,18 +85,69 @@ describe('When using useEndpointPrivileges hook', () => { (useCurrentUser as jest.Mock).mockReturnValue(null); const { rerender } = render(); + expect(result.current).toEqual(getEndpointPrivilegesInitialState()); // Make user service available (useCurrentUser as jest.Mock).mockReturnValue(authenticatedUser); rerender(); - expect(result.current).toEqual(getEndpointPrivilegesInitialState()); - - // Release the API response - await act(async () => { - await useKibana().services.fleet!.authz; - }); expect(result.current).toEqual(getEndpointPrivilegesInitialStateMock()); }); + + it.each([ + ['HIE exist', true], + ['No HIE exist', false], + ])( + `should check if Host Isolation Exceptions exist when license is not Platinum+ (%s)`, + async (_, hasHIE) => { + licenseServiceMock.isPlatinumPlus.mockReturnValue(false); + const http = useKibanaMock().services.http as jest.Mocked; + const deferred = getDeferred(); + const apiMock = exceptionsListAllHttpMocks(http); + + useHttpMock.mockReturnValue(http); + + const apiResponse = apiMock.responseProvider.exceptionsFind({ + query: {}, + } as HttpFetchOptionsWithPath); + apiMock.responseProvider.exceptionsFind.mockImplementation(() => { + if (hasHIE) { + return apiResponse; + } + return { + ...apiResponse, + total: 0, + data: [], + }; + }); + + // Hold on to the Host Isolation Exceptions API all + apiMock.responseProvider.exceptionsFind.mockDelay.mockReturnValue(deferred.promise); + + const { rerender } = render(); + + expect(result.current).toEqual(getEndpointPrivilegesInitialState()); + + // release HIE api call + act(() => { + deferred.resolve(); + }); + rerender(); + + await waitFor(() => { + expect(apiMock.responseProvider.exceptionsFind).toHaveBeenCalled(); + }); + + expect(result.current).toEqual( + getEndpointPrivilegesInitialStateMock({ + canCreateArtifactsByPolicy: false, + canIsolateHost: false, + canWriteHostIsolationExceptions: false, + canReadHostIsolationExceptions: hasHIE, + canDeleteHostIsolationExceptions: hasHIE, + }) + ); + } + ); }); diff --git a/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/use_endpoint_privileges.ts b/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/use_endpoint_privileges.ts index 483bf2192efc5..c74deb9dec00e 100644 --- a/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/use_endpoint_privileges.ts +++ b/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/use_endpoint_privileges.ts @@ -5,9 +5,11 @@ * 2.0. */ -import { useEffect, useMemo, useRef, useState } from 'react'; -import type { FleetAuthz } from '@kbn/fleet-plugin/common'; -import { useCurrentUser, useKibana } from '../../../lib/kibana'; +import { useEffect, useMemo, useState } from 'react'; +import { useIsMounted } from '@kbn/securitysolution-hook-utils'; +import { checkArtifactHasData } from '../../../../management/services/exceptions_list/check_artifact_has_data'; +import { HostIsolationExceptionsApiClient } from '../../../../management/pages/host_isolation_exceptions/host_isolation_exceptions_api_client'; +import { useCurrentUser, useHttp, useKibana } from '../../../lib/kibana'; import { useLicense } from '../../../hooks/use_license'; import type { EndpointPrivileges, @@ -29,95 +31,99 @@ import { useIsExperimentalFeatureEnabled } from '../../../hooks/use_experimental * to keep API calls to a minimum. */ export const useEndpointPrivileges = (): Immutable => { + const isMounted = useIsMounted(); + const http = useHttp(); const user = useCurrentUser(); + const fleetServicesFromUseKibana = useKibana().services.fleet; // The `fleetServicesFromPluginStart` will be defined when this hooks called from a component // that is being rendered under the Fleet context (UI extensions). The `fleetServicesFromUseKibana` // above will be `undefined` in this case. const fleetServicesFromPluginStart = useSecuritySolutionStartDependencies()?.fleet; - const isMounted = useRef(true); + const fleetAuthz = fleetServicesFromUseKibana?.authz ?? fleetServicesFromPluginStart?.authz; + const licenseService = useLicense(); - const [fleetCheckDone, setFleetCheckDone] = useState(false); - const [fleetAuthz, setFleetAuthz] = useState(null); + const isPlatinumPlus = licenseService.isPlatinumPlus(); + const [userRolesCheckDone, setUserRolesCheckDone] = useState(false); const [userRoles, setUserRoles] = useState>([]); - const fleetServices = fleetServicesFromUseKibana ?? fleetServicesFromPluginStart; const isEndpointRbacEnabled = useIsExperimentalFeatureEnabled('endpointRbacEnabled'); const isEndpointRbacV1Enabled = useIsExperimentalFeatureEnabled('endpointRbacV1Enabled'); - const endpointPermissions = calculatePermissionsFromCapabilities( + const [checkHostIsolationExceptionsDone, setCheckHostIsolationExceptionsDone] = + useState(false); + const [hasHostIsolationExceptionsItems, setHasHostIsolationExceptionsItems] = + useState(false); + + const securitySolutionPermissions = calculatePermissionsFromCapabilities( useKibana().services.application.capabilities ); const privileges = useMemo(() => { + const loading = !userRolesCheckDone || !user || !checkHostIsolationExceptionsDone; + const privilegeList: EndpointPrivileges = Object.freeze({ - loading: !fleetCheckDone || !userRolesCheckDone || !user, - ...(fleetAuthz + loading, + ...(!loading && fleetAuthz ? calculateEndpointAuthz( licenseService, fleetAuthz, userRoles, isEndpointRbacEnabled || isEndpointRbacV1Enabled, - endpointPermissions + securitySolutionPermissions, + hasHostIsolationExceptionsItems ) : getEndpointAuthzInitialState()), }); return privilegeList; }, [ - fleetCheckDone, userRolesCheckDone, user, + checkHostIsolationExceptionsDone, fleetAuthz, licenseService, userRoles, isEndpointRbacEnabled, isEndpointRbacV1Enabled, - endpointPermissions, + securitySolutionPermissions, + hasHostIsolationExceptionsItems, ]); - // Check if user can access fleet - useEffect(() => { - if (!fleetServices) { - setFleetCheckDone(true); - return; - } - - setFleetCheckDone(false); - - (async () => { - try { - const fleetAuthzForCurrentUser = await fleetServices.authz; - - if (isMounted.current) { - setFleetAuthz(fleetAuthzForCurrentUser); - } - } finally { - if (isMounted.current) { - setFleetCheckDone(true); - } - } - })(); - }, [fleetServices]); - // get user roles useEffect(() => { (async () => { - if (user && isMounted.current) { + if (user && isMounted()) { setUserRoles(user?.roles); setUserRolesCheckDone(true); } })(); - }, [user]); + }, [isMounted, user]); - // Capture if component is unmounted - useEffect( - () => () => { - isMounted.current = false; - }, - [] - ); + // Check if Host Isolation Exceptions exist if license is not Platinum+ + useEffect(() => { + if (!isPlatinumPlus) { + // Reset these back to false. Case license is changed while the user is logged in. + setHasHostIsolationExceptionsItems(false); + setCheckHostIsolationExceptionsDone(false); + + checkArtifactHasData(HostIsolationExceptionsApiClient.getInstance(http)) + .then((hasData) => { + if (isMounted()) { + setHasHostIsolationExceptionsItems(hasData); + } + }) + .finally(() => { + if (isMounted()) { + setCheckHostIsolationExceptionsDone(true); + } + }); + } else { + setHasHostIsolationExceptionsItems(true); + setCheckHostIsolationExceptionsDone(true); + } + }, [http, isMounted, isPlatinumPlus]); return privileges; }; diff --git a/x-pack/plugins/security_solution/public/common/containers/query_client/query_client_provider.tsx b/x-pack/plugins/security_solution/public/common/containers/query_client/query_client_provider.tsx index 7feaf9c8653ef..016d0d494fae0 100644 --- a/x-pack/plugins/security_solution/public/common/containers/query_client/query_client_provider.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/query_client/query_client_provider.tsx @@ -33,6 +33,14 @@ export class SecuritySolutionQueryClient extends QueryClient { } } +/** + * The default Security Solution Query Client. Can be imported and used from outside of React hooks + * and still benefit from ReactQuery features (like caching, etc) + * + * @see https://tanstack.com/query/v4/docs/reference/QueryClient + */ +export const securitySolutionQueryClient = new SecuritySolutionQueryClient(); + export type ReactQueryClientProviderProps = PropsWithChildren<{ queryClient?: SecuritySolutionQueryClient; }>; @@ -40,7 +48,7 @@ export type ReactQueryClientProviderProps = PropsWithChildren<{ export const ReactQueryClientProvider = memo( ({ queryClient, children }) => { const client = useMemo(() => { - return queryClient || new SecuritySolutionQueryClient(); + return queryClient || securitySolutionQueryClient; }, [queryClient]); return {children}; } diff --git a/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts b/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts index 28b3a85386a94..dc790b99c13d1 100644 --- a/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts +++ b/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts @@ -21,7 +21,14 @@ import { APP_UI_ID } from '../../../../../common/constants'; import { mockCasesContract } from '@kbn/cases-plugin/public/mocks'; const mockStartServicesMock = createStartServicesMock(); -export const KibanaServices = { get: jest.fn(), getKibanaVersion: jest.fn(() => '8.0.0') }; +export const KibanaServices = { + get: jest.fn(() => { + const { http, uiSettings, notifications, data, unifiedSearch } = mockStartServicesMock; + + return { http, uiSettings, notifications, data, unifiedSearch }; + }), + getKibanaVersion: jest.fn(() => '8.0.0'), +}; export const useKibana = jest.fn().mockReturnValue({ services: { ...mockStartServicesMock, diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_list_page/artifact_list_page.tsx b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/artifact_list_page.tsx index 344dbd6cd8349..6f83fa9d41c95 100644 --- a/x-pack/plugins/security_solution/public/management/components/artifact_list_page/artifact_list_page.tsx +++ b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/artifact_list_page.tsx @@ -321,6 +321,7 @@ export const ArtifactListPage = memo( backComponent={backButtonEmptyComponent} data-test-subj={getTestId('emptyState')} secondaryAboutInfo={secondaryPageInfo} + canCreateItems={allowCardCreateAction} /> ) : ( <> diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_list_page/components/no_data_empty_state.tsx b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/components/no_data_empty_state.tsx index 87fb9414b894a..bd40f7031aac2 100644 --- a/x-pack/plugins/security_solution/public/management/components/artifact_list_page/components/no_data_empty_state.tsx +++ b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/components/no_data_empty_state.tsx @@ -22,6 +22,7 @@ export const NoDataEmptyState = memo<{ titleLabel: string; aboutInfo: string; primaryButtonLabel: string; + canCreateItems?: boolean; /** Should the Add button be disabled */ isAddDisabled?: boolean; backComponent?: React.ReactNode; @@ -37,6 +38,7 @@ export const NoDataEmptyState = memo<{ aboutInfo, primaryButtonLabel, secondaryAboutInfo, + canCreateItems = true, }) => { const getTestId = useTestIdGenerator(dataTestSubj); @@ -57,17 +59,21 @@ export const NoDataEmptyState = memo<{ ) : undefined} } - actions={[ - - {primaryButtonLabel} - , - ...(backComponent ? [backComponent] : []), - ]} + actions={ + canCreateItems + ? [ + + {primaryButtonLabel} + , + ...(backComponent ? [backComponent] : []), + ] + : [] + } /> ); diff --git a/x-pack/plugins/security_solution/public/management/links.test.ts b/x-pack/plugins/security_solution/public/management/links.test.ts index 4992b32cef81b..73834013a493d 100644 --- a/x-pack/plugins/security_solution/public/management/links.test.ts +++ b/x-pack/plugins/security_solution/public/management/links.test.ts @@ -15,6 +15,7 @@ import type { StartPlugins } from '../types'; import { links, getManagementFilteredLinks } from './links'; import { allowedExperimentalValues } from '../../common/experimental_features'; import { ExperimentalFeaturesService } from '../common/experimental_features_service'; +import { getEndpointAuthzInitialStateMock } from '../../common/endpoint/service/authz/mocks'; jest.mock('../../common/endpoint/service/authz', () => { const originalModule = jest.requireActual('../../common/endpoint/service/authz'); @@ -24,6 +25,8 @@ jest.mock('../../common/endpoint/service/authz', () => { }; }); +jest.mock('../common/lib/kibana'); + describe('links', () => { let coreMockStarted: ReturnType; let getPlugins: (roles: string[]) => StartPlugins; @@ -57,12 +60,7 @@ describe('links', () => { }); it('should return all links without filtering when having isolate permission', async () => { - (calculateEndpointAuthz as jest.Mock).mockReturnValue({ - canIsolateHost: true, - canUnIsolateHost: true, - canAccessEndpointManagement: true, - canReadActionsLogManagement: true, - }); + (calculateEndpointAuthz as jest.Mock).mockReturnValue(getEndpointAuthzInitialStateMock()); const filteredLinks = await getManagementFilteredLinks( coreMockStarted, @@ -73,12 +71,11 @@ describe('links', () => { describe('Action Logs', () => { it('should return all but response actions link when no actions log access', async () => { - (calculateEndpointAuthz as jest.Mock).mockReturnValue({ - canIsolateHost: true, - canUnIsolateHost: true, - canAccessEndpointManagement: true, - canReadActionsLogManagement: false, - }); + (calculateEndpointAuthz as jest.Mock).mockReturnValue( + getEndpointAuthzInitialStateMock({ + canReadActionsLogManagement: false, + }) + ); fakeHttpServices.get.mockResolvedValue({ total: 0 }); const filteredLinks = await getManagementFilteredLinks( @@ -150,12 +147,11 @@ describe('links', () => { }); it('should return all when NO isolation permission due to license but HAS at least one host isolation exceptions entry', async () => { - (calculateEndpointAuthz as jest.Mock).mockReturnValue({ - canIsolateHost: false, - canUnIsolateHost: true, - canAccessEndpointManagement: true, - canReadActionsLogManagement: true, - }); + (calculateEndpointAuthz as jest.Mock).mockReturnValue( + getEndpointAuthzInitialStateMock({ + canIsolateHost: false, + }) + ); fakeHttpServices.get.mockResolvedValue({ total: 1 }); const filteredLinks = await getManagementFilteredLinks( diff --git a/x-pack/plugins/security_solution/public/management/links.ts b/x-pack/plugins/security_solution/public/management/links.ts index d2e8b6b708948..b168d88215a3a 100644 --- a/x-pack/plugins/security_solution/public/management/links.ts +++ b/x-pack/plugins/security_solution/public/management/links.ts @@ -8,6 +8,8 @@ import type { CoreStart } from '@kbn/core/public'; import { i18n } from '@kbn/i18n'; +import { hasKibanaPrivilege } from '../../common/endpoint/service/authz/authz'; +import { checkArtifactHasData } from './services/exceptions_list/check_artifact_has_data'; import { calculateEndpointAuthz, getEndpointAuthzInitialState, @@ -59,6 +61,7 @@ import { IconSiemRules } from './icons/siem_rules'; import { IconTrustedApplications } from './icons/trusted_applications'; import { HostIsolationExceptionsApiClient } from './pages/host_isolation_exceptions/host_isolation_exceptions_api_client'; import { ExperimentalFeaturesService } from '../common/experimental_features_service'; +import { KibanaServices } from '../common/lib/kibana'; const categories = [ { @@ -232,62 +235,60 @@ const excludeLinks = (linkIds: SecurityPageName[]) => ({ links: links.links?.filter((link) => !linkIds.includes(link.id)), }); -const getHostIsolationExceptionTotal = async (http: CoreStart['http']) => { - const hostIsolationExceptionsApiClientInstance = - HostIsolationExceptionsApiClient.getInstance(http); - const summaryResponse = await hostIsolationExceptionsApiClientInstance.summary(); - return summaryResponse.total; -}; - export const getManagementFilteredLinks = async ( core: CoreStart, plugins: StartPlugins ): Promise => { const fleetAuthz = plugins.fleet?.authz; - const { endpointRbacV1Enabled } = ExperimentalFeaturesService.get(); - const hasPermissionsForSecuritySolution = calculatePermissionsFromCapabilities( - core.application.capabilities - ); + + const { endpointRbacEnabled, endpointRbacV1Enabled } = ExperimentalFeaturesService.get(); + const isEndpointRbacEnabled = endpointRbacEnabled || endpointRbacV1Enabled; + const endpointPermissions = calculatePermissionsFromCapabilities(core.application.capabilities); + const linksToExclude: SecurityPageName[] = []; - try { - const currentUserResponse = await plugins.security.authc.getCurrentUser(); - const { - canReadActionsLogManagement, - canUnIsolateHost, - canIsolateHost, - canAccessEndpointManagement, - } = fleetAuthz - ? calculateEndpointAuthz( - licenseService, - fleetAuthz, - currentUserResponse.roles, - endpointRbacV1Enabled, - hasPermissionsForSecuritySolution - ) - : getEndpointAuthzInitialState(); + const currentUser = await plugins.security.authc.getCurrentUser(); - if (!canReadActionsLogManagement) { - linksToExclude.push(SecurityPageName.responseActionsHistory); - } + const isPlatinumPlus = licenseService.isPlatinumPlus(); + let hasHostIsolationExceptions: boolean = isPlatinumPlus; - if (!canIsolateHost && canUnIsolateHost) { - let shouldBeAbleToDeleteEntries: boolean; - try { - const hostExceptionCount = await getHostIsolationExceptionTotal(core.http); - // has an HIE entry and is a super user then set to TRUE - shouldBeAbleToDeleteEntries = hostExceptionCount !== 0 && canAccessEndpointManagement; - } catch { - shouldBeAbleToDeleteEntries = false; - } + // If not Platinum+ license and user has read permissions to security solution + // then check if Host Isolation Exceptions exist. + // *** IT IS IMPORTANT *** that this HTTP call only be made if the user has access to the + // Lists plugin, else non-security solution users, especially when license is not Platinum, + // may see failed HTTP requests in the browser console. This is the reason that + // `hasKibanaPrivilege()` is used below. + if ( + !isPlatinumPlus && + fleetAuthz && + hasKibanaPrivilege( + fleetAuthz, + isEndpointRbacEnabled, + currentUser.roles.includes('superuser'), + 'readHostIsolationExceptions' + ) + ) { + hasHostIsolationExceptions = await checkArtifactHasData( + HostIsolationExceptionsApiClient.getInstance(KibanaServices.get().http) + ); + } + + const { canReadActionsLogManagement, canReadHostIsolationExceptions } = fleetAuthz + ? calculateEndpointAuthz( + licenseService, + fleetAuthz, + currentUser.roles, + isEndpointRbacEnabled, + endpointPermissions, + hasHostIsolationExceptions + ) + : getEndpointAuthzInitialState(); + + if (!canReadActionsLogManagement) { + linksToExclude.push(SecurityPageName.responseActionsHistory); + } - if (!shouldBeAbleToDeleteEntries) { - linksToExclude.push(SecurityPageName.hostIsolationExceptions); - } - } else if (!canIsolateHost || !canAccessEndpointManagement) { - linksToExclude.push(SecurityPageName.hostIsolationExceptions); - } - } catch { + if (!canReadHostIsolationExceptions) { linksToExclude.push(SecurityPageName.hostIsolationExceptions); } diff --git a/x-pack/plugins/security_solution/public/management/mocks/exceptions_list_http_mocks.ts b/x-pack/plugins/security_solution/public/management/mocks/exceptions_list_http_mocks.ts index 5629052137753..505c1d35ded1b 100644 --- a/x-pack/plugins/security_solution/public/management/mocks/exceptions_list_http_mocks.ts +++ b/x-pack/plugins/security_solution/public/management/mocks/exceptions_list_http_mocks.ts @@ -6,7 +6,11 @@ */ import type { HttpFetchOptionsWithPath } from '@kbn/core/public'; -import { EXCEPTION_LIST_ITEM_URL, EXCEPTION_LIST_URL } from '@kbn/securitysolution-list-constants'; +import { + EXCEPTION_LIST_ITEM_URL, + EXCEPTION_LIST_URL, + INTERNAL_EXCEPTIONS_LIST_ENSURE_CREATED_URL, +} from '@kbn/securitysolution-list-constants'; import type { ExceptionListItemSchema, FoundExceptionListItemSchema, @@ -216,6 +220,24 @@ export const exceptionsPostCreateListHttpMock = }, ]); +export type ExceptionsPostInternalCreateListHttpMockInterface = ResponseProvidersInterface<{ + exceptionInternalCreateList: (options: HttpFetchOptionsWithPath) => ExceptionListSchema; +}>; +/** + * HTTP mock that support creating the list via the internal Lists route + */ +export const exceptionsPostInternalCreateListHttpMock = + httpHandlerMockFactory([ + { + id: 'exceptionInternalCreateList', + path: INTERNAL_EXCEPTIONS_LIST_ENSURE_CREATED_URL, + method: 'post', + handler: (): ExceptionListSchema => { + return getExceptionListSchemaMock(); + }, + }, + ]); + export type ExceptionsGetSummaryHttpMockInterface = ResponseProvidersInterface<{ exceptionsSummary: (options: HttpFetchOptionsWithPath) => ExceptionListSummarySchema; }>; @@ -245,6 +267,7 @@ export type ExceptionsListAllHttpMocksInterface = ExceptionsFindHttpMocksInterfa ExceptionsDeleteOneHttpMocksInterface & ExceptionsPostHttpMocksInterface & ExceptionsPostCreateListHttpMockInterface & + ExceptionsPostInternalCreateListHttpMockInterface & ExceptionsGetSummaryHttpMockInterface; /** Use this HTTP mock when wanting to mock the API calls done by the Exception Http service */ export const exceptionsListAllHttpMocks = @@ -254,6 +277,7 @@ export const exceptionsListAllHttpMocks = exceptionPutHttpMocks, exceptionPostHttpMocks, exceptionsPostCreateListHttpMock, + exceptionsPostInternalCreateListHttpMock, exceptionsDeleteOneHttpMocks, exceptionsGetSummaryHttpMock, ]); diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/hooks.test.ts b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/hooks.test.ts deleted file mode 100644 index e20c17ad60809..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/hooks.test.ts +++ /dev/null @@ -1,93 +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. - */ -import { useCanSeeHostIsolationExceptionsMenu } from './hooks'; -import { renderHook as _renderHook } from '@testing-library/react-hooks'; -import { TestProviders } from '../../../../common/mock'; -import { useEndpointPrivileges } from '../../../../common/components/user_privileges/endpoint'; -import { createAppRootMockRenderer } from '../../../../common/mock/endpoint'; -import { exceptionsGetSummaryHttpMock } from '../../../mocks/exceptions_list_http_mocks'; - -jest.mock('../../../../common/hooks/use_license'); -jest.mock('../../../../common/components/user_privileges/endpoint/use_endpoint_privileges'); - -describe('host isolation exceptions hooks', () => { - const useEndpointPrivilegesMock = useEndpointPrivileges as jest.Mock; - let renderHook: typeof _renderHook; - let mockedApis: ReturnType; - - describe('useCanSeeHostIsolationExceptionsMenu', () => { - beforeEach(() => { - const mockedContext = createAppRootMockRenderer(); - - mockedApis = exceptionsGetSummaryHttpMock(mockedContext.coreStart.http); - renderHook = (callback, options = {}) => { - return _renderHook(callback, { - ...options, - wrapper: mockedContext.AppWrapper, - }); - }; - }); - - afterEach(() => { - useEndpointPrivilegesMock.mockReset(); - }); - - it('should return TRUE if IS superuser AND canIsolateHost', () => { - useEndpointPrivilegesMock.mockReturnValue({ - canIsolateHost: true, - canAccessEndpointManagement: true, - }); - const { result } = renderHook(() => useCanSeeHostIsolationExceptionsMenu(), { - wrapper: TestProviders, - }); - expect(result.current).toBe(true); - }); - - it('should return FALSE if NOT superuser AND canIsolateHost', () => { - useEndpointPrivilegesMock.mockReturnValue({ - canIsolateHost: true, - canAccessEndpointManagement: false, - }); - const { result } = renderHook(() => useCanSeeHostIsolationExceptionsMenu(), { - wrapper: TestProviders, - }); - expect(result.current).toBe(false); - }); - - it('should return FALSE if IS superuser AND and !canIsolateHost and there are no existing host isolation items', () => { - useEndpointPrivilegesMock.mockReturnValue({ - canIsolateHost: false, - canAccessEndpointManagement: true, - }); - mockedApis.responseProvider.exceptionsSummary.mockReturnValue({ - total: 0, - linux: 0, - macos: 0, - windows: 0, - }); - const { result } = renderHook(() => useCanSeeHostIsolationExceptionsMenu(), { - wrapper: TestProviders, - }); - expect(result.current).toBe(false); - }); - - it('should return TRUE if IS superuser AND and !canIsolateHost and there are existing host isolation items', async () => { - useEndpointPrivilegesMock.mockReturnValue({ - canIsolateHost: false, - canAccessEndpointManagement: true, - }); - const { result, waitForNextUpdate } = renderHook( - () => useCanSeeHostIsolationExceptionsMenu(), - { - wrapper: TestProviders, - } - ); - await waitForNextUpdate(); - expect(result.current).toBe(true); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/hooks.ts b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/hooks.ts deleted file mode 100644 index 09bc1f27593c4..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/hooks.ts +++ /dev/null @@ -1,46 +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. - */ -import { useEffect, useMemo } from 'react'; -import { useEndpointPrivileges } from '../../../../common/components/user_privileges/endpoint'; -import { useHttp } from '../../../../common/lib/kibana/hooks'; -import { useSummaryArtifact } from '../../../hooks/artifacts'; -import { HostIsolationExceptionsApiClient } from '../host_isolation_exceptions_api_client'; - -/** - * Checks if the current user should be able to see the host isolation exceptions - * menu item based on their current privileges - */ -export function useCanSeeHostIsolationExceptionsMenu(): boolean { - const http = useHttp(); - // TODO: why doesn't this use useUserPrivileges? - const privileges = useEndpointPrivileges(); - const apiQuery = useSummaryArtifact( - HostIsolationExceptionsApiClient.getInstance(http), - undefined, - undefined, - { - enabled: false, - } - ); - - const { data: summary, isFetching, refetch: checkIfHasExceptions, isFetched } = apiQuery; - - const canSeeMenu = useMemo(() => { - return ( - privileges.canAccessEndpointManagement && - (privileges.canIsolateHost || Boolean(summary?.total)) - ); - }, [privileges.canIsolateHost, privileges.canAccessEndpointManagement, summary?.total]); - - useEffect(() => { - if (!privileges.canIsolateHost && !privileges.loading && !isFetched && !isFetching) { - checkIfHasExceptions(); - } - }, [checkIfHasExceptions, isFetched, isFetching, privileges.canIsolateHost, privileges.loading]); - - return canSeeMenu; -} diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx index e8963d5111bc8..3d2857f3ca6d9 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx @@ -112,10 +112,7 @@ const HOST_ISOLATION_EXCEPTIONS_LABELS: ArtifactListPageProps['labels'] = Object export const HostIsolationExceptionsList = memo(() => { const http = useHttp(); const apiClient = HostIsolationExceptionsApiClient.getInstance(http); - // There is a flow when the Host Isolation Exceptions page is accessible to the user, even - // though they might not have authz to isolate hosts - in a downgrade scenario when entries - // still exist. The only thing the user can do is view and delete entries. - const canIsolate = useUserPrivileges().endpointPrivileges.canIsolateHost; + const { canWriteHostIsolationExceptions } = useUserPrivileges().endpointPrivileges; return ( { labels={HOST_ISOLATION_EXCEPTIONS_LABELS} data-test-subj="hostIsolationExceptionsListPage" searchableFields={SEARCHABLE_FIELDS} - allowCardCreateAction={canIsolate} - allowCardEditAction={canIsolate} + allowCardCreateAction={canWriteHostIsolationExceptions} + allowCardEditAction={canWriteHostIsolationExceptions} /> ); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/integration_tests/host_isolation_exceptions_list.test.tsx b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/integration_tests/host_isolation_exceptions_list.test.tsx index 699b0eb900685..c747766c73c3a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/integration_tests/host_isolation_exceptions_list.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/integration_tests/host_isolation_exceptions_list.test.tsx @@ -77,7 +77,7 @@ describe('When on the host isolation exceptions page', () => { ); }); - it('should hide the Create and Edit actions when host isolation authz is not allowed', async () => { + it('should hide the Create and Edit actions when host isolation exceptions write authz is not allowed, but HIE entries exist', async () => { // Use case: license downgrade scenario, where user still has entries defined, but no longer // able to create or edit them (only Delete them) const existingPrivileges = useUserPrivilegesMock(); @@ -86,6 +86,9 @@ describe('When on the host isolation exceptions page', () => { endpointPrivileges: { ...existingPrivileges.endpointPrivileges, canIsolateHost: false, + canWriteHostIsolationExceptions: false, + canReadHostIsolationExceptions: true, + canDeleteHostIsolationExceptions: true, }, }); diff --git a/x-pack/plugins/security_solution/public/management/pages/index.tsx b/x-pack/plugins/security_solution/public/management/pages/index.tsx index 600683fce9025..ccb9adbb14565 100644 --- a/x-pack/plugins/security_solution/public/management/pages/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/index.tsx @@ -31,7 +31,6 @@ import { useUserPrivileges } from '../../common/components/user_privileges'; import { HostIsolationExceptionsContainer } from './host_isolation_exceptions'; import { BlocklistContainer } from './blocklist'; import { ResponseActionsContainer } from './response_actions'; -import { useCanSeeHostIsolationExceptionsMenu } from './host_isolation_exceptions/view/hooks'; import { PrivilegedRoute } from '../components/privileged_route'; const EndpointTelemetry = () => ( @@ -85,10 +84,9 @@ export const ManagementContainer = memo(() => { canReadEventFilters, canReadActionsLogManagement, canReadEndpointList, + canReadHostIsolationExceptions, } = useUserPrivileges().endpointPrivileges; - const canSeeHostIsolationExceptionsPage = useCanSeeHostIsolationExceptionsMenu(); - // Lets wait until we can verify permissions if (loading) { return ; @@ -119,7 +117,7 @@ export const ManagementContainer = memo(() => { { let render: () => ReturnType; @@ -33,7 +30,6 @@ describe('when in the Administration tab', () => { afterEach(() => { useUserPrivilegesMock.mockReset(); - useCanSeeHostIsolationExceptionsMenuMock.mockReset(); }); describe('when the user has no permissions', () => { diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/endpoint_package_custom_extension.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/endpoint_package_custom_extension.test.tsx index 4c8bfac78d5d3..7372f98e21056 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/endpoint_package_custom_extension.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/endpoint_package_custom_extension.test.tsx @@ -88,8 +88,7 @@ describe('When displaying the EndpointPackageCustomExtension fleet UI extension' it('should NOT show Host Isolation Exceptions if user has no authz and no entries exist', async () => { useEndpointPrivilegesMock.mockReturnValue({ - ...getEndpointPrivilegesInitialStateMock(), - canIsolateHost: false, + ...getEndpointPrivilegesInitialStateMock({ canReadHostIsolationExceptions: false }), }); render(); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/endpoint_package_custom_extension.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/endpoint_package_custom_extension.tsx index 479b2290a1b00..6410c14deec61 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/endpoint_package_custom_extension.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/endpoint_package_custom_extension.tsx @@ -9,7 +9,6 @@ import React, { memo, useMemo } from 'react'; import { EuiSpacer, EuiLoadingSpinner } from '@elastic/eui'; import type { PackageCustomExtensionComponentProps } from '@kbn/fleet-plugin/public'; import { useHttp } from '../../../../../../common/lib/kibana'; -import { useCanSeeHostIsolationExceptionsMenu } from '../../../../host_isolation_exceptions/view/hooks'; import { TrustedAppsApiClient } from '../../../../trusted_apps/service/api_client'; import { EventFiltersApiClient } from '../../../../event_filters/service/api_client'; import { HostIsolationExceptionsApiClient } from '../../../../host_isolation_exceptions/host_isolation_exceptions_api_client'; @@ -33,8 +32,8 @@ import { NoPermissions } from '../../../../../components/no_permissons'; export const EndpointPackageCustomExtension = memo( (props) => { const http = useHttp(); - const canSeeHostIsolationExceptions = useCanSeeHostIsolationExceptionsMenu(); - const { loading, canAccessEndpointManagement } = useEndpointPrivileges(); + const { loading, canAccessEndpointManagement, canReadHostIsolationExceptions } = + useEndpointPrivileges(); const trustedAppsApiClientInstance = useMemo( () => TrustedAppsApiClient.getInstance(http), @@ -74,7 +73,7 @@ export const EndpointPackageCustomExtension = memo - {canSeeHostIsolationExceptions && ( + {canReadHostIsolationExceptions && ( <> , 'queryFn'> = {} +) => { + return securitySolutionQueryClient.fetchQuery({ + queryKey: ['get-artifact-has-data', artifactApiClient], + ...queryOptions, + queryFn: async () => { + try { + return await artifactApiClient.hasData(); + } catch (error) { + window.console.log(error); + // Ignores possible failures and returns `false` if any exception was encountered + return false; + } + }, + }); +}; diff --git a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts index 21c083ac77129..e6c26c9af4773 100644 --- a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts +++ b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts @@ -18,6 +18,7 @@ import type { AgentPolicyServiceInterface, } from '@kbn/fleet-plugin/server'; import type { PluginStartContract as AlertsPluginStartContract } from '@kbn/alerting-plugin/server'; +import { ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID } from '@kbn/securitysolution-list-constants'; import { getPackagePolicyCreateCallback, getPackagePolicyUpdateCallback, @@ -47,6 +48,7 @@ import { } from '../../common/endpoint/service/authz'; import type { FeatureUsageService } from './services/feature_usage/service'; import type { ExperimentalFeatures } from '../../common/experimental_features'; +import { doesArtifactHaveData } from './services'; export interface EndpointAppContextServiceSetupContract { securitySolutionRequestContextFactory: IRequestContextFactory; @@ -168,6 +170,8 @@ export class EndpointAppContextService { const fleetAuthz = await this.getFleetAuthzService().fromRequest(request); const userRoles = this.security?.authc.getCurrentUser(request)?.roles ?? []; const { endpointRbacEnabled, endpointRbacV1Enabled } = this.experimentalFeatures; + const isPlatinumPlus = this.getLicenseService().isPlatinumPlus(); + const listClient = this.getExceptionListsClient(); let endpointPermissions = defaultEndpointPermissions(); if (this.security) { @@ -181,12 +185,17 @@ export class EndpointAppContextService { endpointPermissions = calculatePermissionsFromPrivileges(privileges.kibana); } + const hasExceptionsListItems = !isPlatinumPlus + ? await doesArtifactHaveData(listClient, ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID) + : true; + return calculateEndpointAuthz( this.getLicenseService(), fleetAuthz, userRoles, endpointRbacEnabled || endpointRbacV1Enabled, - endpointPermissions + endpointPermissions, + hasExceptionsListItems ); } @@ -255,4 +264,12 @@ export class EndpointAppContextService { return this.startDependencies.experimentalFeatures; } + + public getExceptionListsClient(): ExceptionListClient { + if (!this.startDependencies?.exceptionListsClient) { + throw new EndpointAppContentServicesNotStartedError(); + } + + return this.startDependencies.exceptionListsClient; + } } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/file_download_handler.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/file_download_handler.test.ts index 5711baac65f54..becbc6aef77d0 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/file_download_handler.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/file_download_handler.test.ts @@ -20,6 +20,7 @@ import { getFileDownloadStream as _getFileDownloadStream } from '../../services/ import stream from 'stream'; import type { ActionDetails } from '../../../../common/endpoint/types'; import { ACTION_AGENT_FILE_DOWNLOAD_ROUTE } from '../../../../common/endpoint/constants'; +import { getEndpointAuthzInitialStateMock } from '../../../../common/endpoint/service/authz/mocks'; jest.mock('../../services'); jest.mock('../../services/actions/action_files'); @@ -59,8 +60,9 @@ describe('Response Actions file download API', () => { }); it('should error if user has no authz to api', async () => { - const authz = (await httpHandlerContextMock.securitySolution).endpointAuthz; - authz.canWriteFileOperations = false; + ( + (await httpHandlerContextMock.securitySolution).getEndpointAuthz as jest.Mock + ).mockResolvedValue(getEndpointAuthzInitialStateMock({ canWriteFileOperations: false })); await apiTestSetup.getRegisteredRouteHandler('get', ACTION_AGENT_FILE_DOWNLOAD_ROUTE)( httpHandlerContextMock, diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/file_info_handler.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/file_info_handler.test.ts index fa3c7e392d91e..589c639ffecd6 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/file_info_handler.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/file_info_handler.test.ts @@ -16,6 +16,7 @@ import type { ActionDetails } from '../../../../common/endpoint/types'; import { EndpointActionGenerator } from '../../../../common/endpoint/data_generators/endpoint_action_generator'; import { getFileInfo as _getFileInfo } from '../../services/actions/action_files'; import { CustomHttpRequestError } from '../../../utils/custom_http_request_error'; +import { getEndpointAuthzInitialStateMock } from '../../../../common/endpoint/service/authz/mocks'; jest.mock('../../services'); jest.mock('../../services/actions/action_files'); @@ -52,8 +53,9 @@ describe('Response Action file info API', () => { }); it('should error if user has no authz to api', async () => { - const authz = (await httpHandlerContextMock.securitySolution).endpointAuthz; - authz.canWriteFileOperations = false; + ( + (await httpHandlerContextMock.securitySolution).getEndpointAuthz as jest.Mock + ).mockResolvedValue(getEndpointAuthzInitialStateMock({ canWriteFileOperations: false })); await apiTestSetup.getRegisteredRouteHandler('get', ACTION_AGENT_FILE_INFO_ROUTE)( httpHandlerContextMock, diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/list.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/list.test.ts index b8e7211874cd9..bd80df0c73413 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/list.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/list.test.ts @@ -97,17 +97,15 @@ describe('Action List Route', () => { const withLicense = license ? license : Platinum; licenseEmitter.next(withLicense); - ctx.securitySolution.endpointAuthz = { + ctx.securitySolution.getEndpointAuthz.mockResolvedValue({ ...getEndpointAuthzInitialStateMock({ - canReadActionsLogManagement: - // mimicking the behavior of the EndpointAuthz class - // just so we can test the license check here - // since getEndpointAuthzInitialStateMock sets all keys to true - ctx.securitySolution.endpointAuthz.canAccessEndpointManagement && - licenseService.isPlatinumPlus(), + // mimicking the behavior of the EndpointAuthz class + // just so we can test the license check here + // since getEndpointAuthzInitialStateMock sets all keys to true + canReadActionsLogManagement: licenseService.isPlatinumPlus(), }), ...authz, - }; + }); const mockRequest = httpServerMock.createKibanaRequest({ query }); const [, routeHandler]: [ diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/response_actions.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/response_actions.test.ts index 545c4f31c3650..f9f9ae22be429 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/response_actions.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/response_actions.test.ts @@ -67,6 +67,7 @@ import { legacyMetadataSearchResponseMock } from '../metadata/support/test_suppo import { registerResponseActionRoutes } from './response_actions'; import * as ActionDetailsService from '../../services/actions/action_details_by_id'; import { CaseStatuses } from '@kbn/cases-components'; +import { getEndpointAuthzInitialStateMock } from '../../../../common/endpoint/service/authz/mocks'; interface CallRouteInterface { body?: ResponseActionRequestBody; @@ -173,10 +174,9 @@ describe('Response actions', () => { const ctx = createRouteHandlerContext(mockScopedClient, mockSavedObjectClient); - ctx.securitySolution.endpointAuthz = { - ...ctx.securitySolution.endpointAuthz, - ...authz, - }; + ctx.securitySolution.getEndpointAuthz.mockResolvedValue( + getEndpointAuthzInitialStateMock(authz) + ); // mock _index_template ctx.core.elasticsearch.client.asInternalUser.indices.existsIndexTemplate.mockResponseImplementationOnce( diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/with_endpoint_authz.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/with_endpoint_authz.test.ts index 92ee8c9953ca9..573b8dc9cbae5 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/with_endpoint_authz.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/with_endpoint_authz.test.ts @@ -12,6 +12,7 @@ import type { EndpointApiNeededAuthz } from './with_endpoint_authz'; import { withEndpointAuthz } from './with_endpoint_authz'; import type { EndpointAuthz } from '../../../common/endpoint/types/authz'; import { EndpointAuthorizationError } from '../errors'; +import { getEndpointAuthzInitialStateMock } from '../../../common/endpoint/service/authz/mocks'; describe('When using `withEndpointAuthz()`', () => { let mockRequestHandler: jest.Mocked; @@ -60,7 +61,10 @@ describe('When using `withEndpointAuthz()`', () => { { canCreateArtifactsByPolicy: false }, ], ])('should grant access when needed authz is %j', async (neededAuthz, authzOverrides) => { - Object.assign(mockContext.securitySolution.endpointAuthz, authzOverrides); + mockContext.securitySolution.getEndpointAuthz.mockResolvedValue( + getEndpointAuthzInitialStateMock(authzOverrides) + ); + await withEndpointAuthz(neededAuthz, logger, mockRequestHandler)( coreMock.createCustomRequestHandlerContext(mockContext), mockRequest, @@ -87,8 +91,9 @@ describe('When using `withEndpointAuthz()`', () => { { canCreateArtifactsByPolicy: false }, ], ])('should deny access when not authorized for %j', async (neededAuthz, authzOverrides) => { - Object.assign(mockContext.securitySolution.endpointAuthz, authzOverrides); - + mockContext.securitySolution.getEndpointAuthz.mockResolvedValue( + getEndpointAuthzInitialStateMock(authzOverrides) + ); await withEndpointAuthz(neededAuthz, logger, mockRequestHandler)( coreMock.createCustomRequestHandlerContext(mockContext), mockRequest, diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/with_endpoint_authz.ts b/x-pack/plugins/security_solution/server/endpoint/routes/with_endpoint_authz.ts index 14a61a92e52e8..8822db6c68367 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/with_endpoint_authz.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/with_endpoint_authz.ts @@ -51,7 +51,7 @@ export const withEndpointAuthz = ( SecuritySolutionRequestHandlerContext > = async (context, request, response) => { if (enforceAuthz) { - const endpointAuthz = (await context.securitySolution).endpointAuthz; + const endpointAuthz = await (await context.securitySolution).getEndpointAuthz(); const permissionChecker = (permission: EndpointAuthzKeyList[0]) => endpointAuthz[permission]; // has `all`? diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts_exception_list/does_artifact_have_data.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts_exception_list/does_artifact_have_data.ts new file mode 100644 index 0000000000000..ebc1bb3536598 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts_exception_list/does_artifact_have_data.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ExceptionListClient } from '@kbn/lists-plugin/server'; +import type { Logger } from '@kbn/core/server'; + +/** + * Checks to see if a given artifact list id has data + * @param listClient + * @param listId + * @param logger + */ +export const doesArtifactHaveData = async ( + listClient: ExceptionListClient, + listId: string, + logger?: Logger +): Promise => { + try { + const dataFound = await listClient.findExceptionListItem({ + listId, + perPage: 1, + page: 1, + sortField: undefined, + sortOrder: undefined, + filter: undefined, + namespaceType: 'agnostic', + }); + + return dataFound ? dataFound.total > 0 : false; + } catch (error) { + if (logger) { + logger.debug(`Failed to find data against endpoint artifact list [${listId}]: error.message`); + } + return false; + } +}; diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts_exception_list/index.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts_exception_list/index.ts new file mode 100644 index 0000000000000..ab7bfcdc27740 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts_exception_list/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { doesArtifactHaveData } from './does_artifact_have_data'; diff --git a/x-pack/plugins/security_solution/server/endpoint/services/index.ts b/x-pack/plugins/security_solution/server/endpoint/services/index.ts index d34a9b028408c..75244c9e878e5 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/index.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/index.ts @@ -8,4 +8,5 @@ export * from './artifacts'; export { getMetadataForEndpoints } from './metadata/metadata'; export * from './actions'; +export * from './artifacts_exception_list'; export type { FeatureKeys } from './feature_usage'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts index 91ca1c6af9978..5a0e9f13d1fb9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts @@ -110,7 +110,9 @@ const createSecuritySolutionRequestContextMock = ( return { core, - endpointAuthz: getEndpointAuthzInitialStateMock(overrides.endpointAuthz), + getEndpointAuthz: jest.fn(async () => + getEndpointAuthzInitialStateMock(overrides.endpointAuthz) + ), getConfig: jest.fn(() => clients.config), getFrameworkRequest: jest.fn(() => { return { diff --git a/x-pack/plugins/security_solution/server/request_context_factory.ts b/x-pack/plugins/security_solution/server/request_context_factory.ts index 5eab776e217fe..4cf161e29407f 100644 --- a/x-pack/plugins/security_solution/server/request_context_factory.ts +++ b/x-pack/plugins/security_solution/server/request_context_factory.ts @@ -9,7 +9,6 @@ import { memoize } from 'lodash'; import type { Logger, KibanaRequest, RequestHandlerContext } from '@kbn/core/server'; -import type { FleetAuthz } from '@kbn/fleet-plugin/common'; import { DEFAULT_SPACE_ID } from '../common/constants'; import { AppClientFactory } from './client'; import type { ConfigType } from './config'; @@ -25,13 +24,6 @@ import type { } from './types'; import type { Immutable } from '../common/endpoint/types'; import type { EndpointAuthz } from '../common/endpoint/types/authz'; -import { - calculateEndpointAuthz, - calculatePermissionsFromPrivileges, - defaultEndpointPermissions, - getEndpointAuthzInitialState, -} from '../common/endpoint/service/authz'; -import { licenseService } from './lib/license'; import type { EndpointAppContextService } from './endpoint/endpoint_app_context_services'; export interface IRequestContextFactory { @@ -67,56 +59,24 @@ export class RequestContextFactory implements IRequestContextFactory { const [, startPlugins] = await core.getStartServices(); const frameworkRequest = await buildFrameworkRequest(context, security, request); + const coreContext = await context.core; + appClientFactory.setup({ getSpaceId: startPlugins.spaces?.spacesService?.getSpaceId, config, }); + // List of endpoint authz for the current request's user. Will be initialized the first + // time it is requested (see `getEndpointAuthz()` below) let endpointAuthz: Immutable; - let fleetAuthz: FleetAuthz; - - // If Fleet is enabled, then get its Authz - if (startPlugins.fleet) { - fleetAuthz = - (await context.fleet)?.authz ?? (await startPlugins.fleet?.authz.fromRequest(request)); - } - - const coreContext = await context.core; - - let endpointPermissions = defaultEndpointPermissions(); - if (endpointAppContextService.security) { - const checkPrivileges = - endpointAppContextService.security.authz.checkPrivilegesDynamicallyWithRequest(request); - const { privileges } = await checkPrivileges({ - kibana: [ - endpointAppContextService.security.authz.actions.ui.get('siem', 'crud'), - endpointAppContextService.security.authz.actions.ui.get('siem', 'show'), - ], - }); - endpointPermissions = calculatePermissionsFromPrivileges(privileges.kibana); - } return { core: coreContext, - get endpointAuthz(): Immutable { - // Lazy getter of endpoint Authz. No point in defining it if it is never used. + getEndpointAuthz: async (): Promise> => { if (!endpointAuthz) { - // If no fleet (fleet plugin is optional in the configuration), then just turn off all permissions - if (!startPlugins.fleet) { - endpointAuthz = getEndpointAuthzInitialState(); - } else { - const { endpointRbacEnabled, endpointRbacV1Enabled } = - endpointAppContextService.experimentalFeatures; - const userRoles = security?.authc.getCurrentUser(request)?.roles ?? []; - endpointAuthz = calculateEndpointAuthz( - licenseService, - fleetAuthz, - userRoles, - endpointRbacEnabled || endpointRbacV1Enabled, - endpointPermissions - ); - } + // eslint-disable-next-line require-atomic-updates + endpointAuthz = await endpointAppContextService.getEndpointAuthz(request); } return endpointAuthz; diff --git a/x-pack/plugins/security_solution/server/types.ts b/x-pack/plugins/security_solution/server/types.ts index 3655beffba5c5..84a11b6ad0ba9 100644 --- a/x-pack/plugins/security_solution/server/types.ts +++ b/x-pack/plugins/security_solution/server/types.ts @@ -18,6 +18,7 @@ import type { LicensingApiRequestHandlerContext } from '@kbn/licensing-plugin/se import type { ListsApiRequestHandlerContext, ExceptionListClient } from '@kbn/lists-plugin/server'; import type { IRuleDataService, AlertsClient } from '@kbn/rule-registry-plugin/server'; +import type { Immutable } from '../common/endpoint/types'; import type { CreateQueryRuleAdditionalOptions } from './lib/detection_engine/rule_types/types'; import { AppClient } from './client'; import type { ConfigType } from './config'; @@ -33,7 +34,7 @@ export { AppClient }; export interface SecuritySolutionApiRequestHandlerContext { core: CoreRequestHandlerContext; - endpointAuthz: EndpointAuthz; + getEndpointAuthz: () => Promise>; getConfig: () => ConfigType; getFrameworkRequest: () => FrameworkRequest; getAppClient: () => AppClient; From 6d4cadaeac4c68fc1e023f97e2ce0968af47e024 Mon Sep 17 00:00:00 2001 From: Rashmi Kulkarni Date: Thu, 17 Nov 2022 13:53:54 -0800 Subject: [PATCH 07/78] basic smoke test for Fleet installation package (#145475) covers https://github.com/elastic/kibana/pull/144899 checks `Elastic Synthetics` shows up when navigated to Installed Integrations Screen Shot 2022-11-16 at 3 16 32 PM --- .../test/fleet_functional/apps/fleet/index.ts | 1 + .../apps/fleet/integration_smoke.ts | 30 +++++++++++++++++++ ...onfig.stack_functional_integration_base.js | 4 +++ 3 files changed, 35 insertions(+) create mode 100644 x-pack/test/fleet_functional/apps/fleet/integration_smoke.ts diff --git a/x-pack/test/fleet_functional/apps/fleet/index.ts b/x-pack/test/fleet_functional/apps/fleet/index.ts index 965d4c7776197..446d78d5e203a 100644 --- a/x-pack/test/fleet_functional/apps/fleet/index.ts +++ b/x-pack/test/fleet_functional/apps/fleet/index.ts @@ -12,5 +12,6 @@ export default function (providerContext: FtrProviderContext) { describe('endpoint', function () { loadTestFile(require.resolve('./agents_page')); + loadTestFile(require.resolve('./integration_smoke')); }); } diff --git a/x-pack/test/fleet_functional/apps/fleet/integration_smoke.ts b/x-pack/test/fleet_functional/apps/fleet/integration_smoke.ts new file mode 100644 index 0000000000000..f71234ea851b0 --- /dev/null +++ b/x-pack/test/fleet_functional/apps/fleet/integration_smoke.ts @@ -0,0 +1,30 @@ +/* + * 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 { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const PageObjects = getPageObjects(['common']); + const testSubjects = getService('testSubjects'); + + describe('Elastic synthetics integration', function () { + this.tags(['smoke']); + before(async () => { + await PageObjects.common.navigateToUrl('management', 'integrations/installed', { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + shouldUseHashForSubUrl: false, + }); + }); + + // This is a basic smoke test to cover Fleet package installed logic + // https://github.com/elastic/kibana/pull/144899 + it('should show the Elastic synthetics integration', async () => { + await testSubjects.exists('integration-card:epr:synthetics'); + }); + }); +} diff --git a/x-pack/test/stack_functional_integration/configs/config.stack_functional_integration_base.js b/x-pack/test/stack_functional_integration/configs/config.stack_functional_integration_base.js index cc87395ac27f1..9b2e2f087d081 100644 --- a/x-pack/test/stack_functional_integration/configs/config.stack_functional_integration_base.js +++ b/x-pack/test/stack_functional_integration/configs/config.stack_functional_integration_base.js @@ -34,6 +34,9 @@ export default async ({ readConfigFile }) => { const xpackFunctionalConfig = await readConfigFile( require.resolve('../../functional/config.ccs.ts') ); + const fleetFunctionalConfig = await readConfigFile( + require.resolve('../../fleet_functional/config.ts') + ); process.env.stack_functional_integration = true; logAll(log); @@ -42,6 +45,7 @@ export default async ({ readConfigFile }) => { pageObjects: { triggersActionsUI: TriggersActionsPageProvider, ...xpackFunctionalConfig.get('pageObjects'), + ...fleetFunctionalConfig.get('pageObjects'), }, apps: { ...xpackFunctionalConfig.get('apps'), From c573527fc29aaf4f069b4c9335a5d52d192d0fb7 Mon Sep 17 00:00:00 2001 From: Spencer Date: Thu, 17 Nov 2022 15:04:23 -0700 Subject: [PATCH 08/78] [pkgs/peggy] unify arguments to peggy parser (#145585) We're getting closer to building peggy outside of bazel, and would like to make configuring peggy something that is file-specific and not done by configuring a peggy cli flag. This pr moves the config to `${basename}.config.json` and then loads that in the cli instead. Down the road we'll be able to write wrappers around the peggy compiler which automatically picks up that file when it's located right next to the grammar. --- packages/kbn-es-query/BUILD.bazel | 7 ++++--- packages/kbn-es-query/grammar/grammar.config.json | 3 +++ packages/kbn-interpreter/BUILD.bazel | 5 +++-- packages/kbn-interpreter/grammar/grammar.config.json | 3 +++ 4 files changed, 13 insertions(+), 5 deletions(-) create mode 100644 packages/kbn-es-query/grammar/grammar.config.json create mode 100644 packages/kbn-interpreter/grammar/grammar.config.json diff --git a/packages/kbn-es-query/BUILD.bazel b/packages/kbn-es-query/BUILD.bazel index 4be5c77358a79..772fecdbb9806 100644 --- a/packages/kbn-es-query/BUILD.bazel +++ b/packages/kbn-es-query/BUILD.bazel @@ -62,12 +62,13 @@ TYPES_DEPS = [ peggy( name = "grammar", data = [ - ":grammar/grammar.peggy" + ":grammar/grammar.peggy", + ":grammar/grammar.config.json" ], output_dir = True, args = [ - "--allowed-start-rules", - "start,Literal", + "--extra-options-file", + "./%s/grammar/grammar.config.json" % package_name(), "-o", "$(@D)/built_grammar.js", "./%s/grammar/grammar.peggy" % package_name() diff --git a/packages/kbn-es-query/grammar/grammar.config.json b/packages/kbn-es-query/grammar/grammar.config.json new file mode 100644 index 0000000000000..2428599612899 --- /dev/null +++ b/packages/kbn-es-query/grammar/grammar.config.json @@ -0,0 +1,3 @@ +{ + "allowedStartRules": ["start", "Literal"] +} diff --git a/packages/kbn-interpreter/BUILD.bazel b/packages/kbn-interpreter/BUILD.bazel index d20c34f71461d..9613a29188bd3 100644 --- a/packages/kbn-interpreter/BUILD.bazel +++ b/packages/kbn-interpreter/BUILD.bazel @@ -66,12 +66,13 @@ jsts_transpiler( peggy( name = "grammar", data = [ + ":grammar/grammar.config.json", ":grammar/grammar.peggy" ], output_dir = True, args = [ - "--allowed-start-rules", - "expression,argument", + "--extra-options-file", + "./%s/grammar/grammar.config.json" % package_name(), "-o", "$(@D)/built_grammar.js", "./%s/grammar/grammar.peggy" % package_name() diff --git a/packages/kbn-interpreter/grammar/grammar.config.json b/packages/kbn-interpreter/grammar/grammar.config.json new file mode 100644 index 0000000000000..c45556e351a5d --- /dev/null +++ b/packages/kbn-interpreter/grammar/grammar.config.json @@ -0,0 +1,3 @@ +{ + "allowedStartRules": ["expression", "argument"] +} From 0942ce1069c8bf232b7f28432f6cad0ac869c372 Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Thu, 17 Nov 2022 16:44:24 -0600 Subject: [PATCH 09/78] skip flaky suite. #145635 --- .../response_actions_log.test.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/response_actions_log.test.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/response_actions_log.test.tsx index 64b3035277136..dc7b9aef5c81f 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/response_actions_log.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/response_actions_log.test.tsx @@ -138,7 +138,8 @@ jest.mock('../../hooks/response_actions/use_get_file_info', () => { const mockUseGetEndpointsList = useGetEndpointsList as jest.Mock; -describe('Response actions history', () => { +// FLAKY https://github.com/elastic/kibana/issues/145635 +describe.skip('Response actions history', () => { const useUserPrivilegesMock = _useUserPrivileges as jest.Mock< ReturnType >; From c5239d3c4e6f9f78118448ca9d1e385113701ef3 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 17 Nov 2022 17:49:21 -0500 Subject: [PATCH 10/78] skip failing test suite (#145639) --- .../apis/synthetics/add_monitor_private_location.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/api_integration/apis/synthetics/add_monitor_private_location.ts b/x-pack/test/api_integration/apis/synthetics/add_monitor_private_location.ts index c9b97c96c9585..75e8434136f52 100644 --- a/x-pack/test/api_integration/apis/synthetics/add_monitor_private_location.ts +++ b/x-pack/test/api_integration/apis/synthetics/add_monitor_private_location.ts @@ -18,7 +18,8 @@ import { comparePolicies, getTestSyntheticsPolicy } from '../uptime/rest/sample_ import { PrivateLocationTestService } from './services/private_location_test_service'; export default function ({ getService }: FtrProviderContext) { - describe('PrivateLocationMonitor', function () { + // Failing: See https://github.com/elastic/kibana/issues/145639 + describe.skip('PrivateLocationMonitor', function () { this.tags('skipCloud'); const kibanaServer = getService('kibanaServer'); const supertestAPI = getService('supertest'); From 39bacae9bceb53b9f53863bc4f7b36fea9c299fb Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Thu, 17 Nov 2022 16:02:10 -0700 Subject: [PATCH 11/78] [Security solution] [Explore] Alerts/cases guided onboarding cypress tests (#145312) --- .../server/helpers/plugin_state_utils.ts | 9 +- .../cypress/e2e/guided_onboarding/tour.cy.ts | 128 ++++++++++++------ .../cypress/screens/guided_onboarding.ts | 15 +- .../cypress/tasks/api_calls/tour.ts | 42 ++++++ .../cypress/tasks/guided_onboarding.ts | 72 ++++++++-- .../translations/translations/fr-FR.json | 5 - .../translations/translations/ja-JP.json | 5 - .../translations/translations/zh-CN.json | 5 - .../test/security_solution_cypress/config.ts | 2 + 9 files changed, 208 insertions(+), 75 deletions(-) create mode 100644 x-pack/plugins/security_solution/cypress/tasks/api_calls/tour.ts diff --git a/src/plugins/guided_onboarding/server/helpers/plugin_state_utils.ts b/src/plugins/guided_onboarding/server/helpers/plugin_state_utils.ts index f24fdf814f83b..06fc211f1864f 100644 --- a/src/plugins/guided_onboarding/server/helpers/plugin_state_utils.ts +++ b/src/plugins/guided_onboarding/server/helpers/plugin_state_utils.ts @@ -40,7 +40,14 @@ export const getPluginState = async (savedObjectsClient: SavedObjectsClient) => return pluginState; } else { // create a SO to keep track of the correct creation date - await updatePluginStatus(savedObjectsClient, 'not_started'); + try { + await updatePluginStatus(savedObjectsClient, 'not_started'); + // @yulia, we need to add a user permissions + // check here instead of swallowing this error + // see issue: https://github.com/elastic/kibana/issues/145434 + // eslint-disable-next-line no-empty + } catch (e) {} + return { status: 'not_started', isActivePeriod: true, diff --git a/x-pack/plugins/security_solution/cypress/e2e/guided_onboarding/tour.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/guided_onboarding/tour.cy.ts index 0339445bc8240..e1dc50d8d28c8 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/guided_onboarding/tour.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/guided_onboarding/tour.cy.ts @@ -5,58 +5,100 @@ * 2.0. */ -import { login, visit } from '../../tasks/login'; -import { completeTour, goToNextStep, skipTour } from '../../tasks/guided_onboarding'; -import { OVERVIEW_URL } from '../../urls/navigation'; +import { navigateFromHeaderTo } from '../../tasks/security_header'; +import { ALERTS, TIMELINES } from '../../screens/security_header'; +import { closeAlertFlyout, expandFirstAlert } from '../../tasks/alerts'; import { - WELCOME_STEP, - MANAGE_STEP, - ALERTS_STEP, - CASES_STEP, - DATA_STEP, -} from '../../screens/guided_onboarding'; - -before(() => { - login(); -}); + assertTourStepExist, + assertTourStepNotExist, + closeCreateCaseFlyout, + completeTourWithActions, + completeTourWithNextButton, + addToCase, + finishTour, + goToStep, + startTour, +} from '../../tasks/guided_onboarding'; +import { cleanKibana } from '../../tasks/common'; +import { createCustomRuleEnabled } from '../../tasks/api_calls/rules'; +import { getNewRule } from '../../objects/rule'; +import { ALERTS_URL, DASHBOARDS_URL } from '../../urls/navigation'; +import { waitForAlertsToPopulate } from '../../tasks/create_new_rule'; +import { login, visit } from '../../tasks/login'; +import { quitGlobalTour, startAlertsCasesTour } from '../../tasks/api_calls/tour'; +import { AlertsCasesTourSteps } from '../../../public/common/components/guided_onboarding_tour/tour_config'; -// need to redo these tests for new implementation -describe.skip('Guided onboarding tour', () => { - describe('Tour is enabled', () => { - beforeEach(() => { - visit(OVERVIEW_URL); - }); +describe('Guided onboarding tour', () => { + before(() => { + cleanKibana(); + login(); + createCustomRuleEnabled({ ...getNewRule(), customQuery: 'user.name:*' }); + }); + beforeEach(() => { + startAlertsCasesTour(); + visit(ALERTS_URL); + waitForAlertsToPopulate(); + }); + after(() => { + quitGlobalTour(); + }); + it('Completes the tour with next button clicks', () => { + startTour(); + completeTourWithNextButton(); + finishTour(); + cy.url().should('include', DASHBOARDS_URL); + }); - it('can be completed', () => { - // Step 1: Overview - cy.get(WELCOME_STEP).should('be.visible'); - goToNextStep(WELCOME_STEP); + it('Completes the tour with action clicks', () => { + startTour(); + completeTourWithActions(); + finishTour(); + cy.url().should('include', DASHBOARDS_URL); + }); - // Step 2: Manage - cy.get(MANAGE_STEP).should('be.visible'); - goToNextStep(MANAGE_STEP); + // unhappy paths + it('Resets the tour to step 1 when we navigate away', () => { + startTour(); + goToStep(AlertsCasesTourSteps.expandEvent); + assertTourStepExist(AlertsCasesTourSteps.expandEvent); + assertTourStepNotExist(AlertsCasesTourSteps.pointToAlertName); + navigateFromHeaderTo(TIMELINES); + navigateFromHeaderTo(ALERTS); + assertTourStepNotExist(AlertsCasesTourSteps.expandEvent); + assertTourStepExist(AlertsCasesTourSteps.pointToAlertName); + }); - // Step 3: Alerts - cy.get(ALERTS_STEP).should('be.visible'); - goToNextStep(ALERTS_STEP); + describe('persists tour steps in flyout on flyout toggle', () => { + const stepsInAlertsFlyout = [ + AlertsCasesTourSteps.reviewAlertDetailsFlyout, + AlertsCasesTourSteps.addAlertToCase, + AlertsCasesTourSteps.viewCase, + ]; - // Step 4: Cases - cy.get(CASES_STEP).should('be.visible'); - goToNextStep(CASES_STEP); + const stepsInCasesFlyout = [AlertsCasesTourSteps.createCase, AlertsCasesTourSteps.submitCase]; - // Step 5: Add data - cy.get(DATA_STEP).should('be.visible'); - completeTour(); + stepsInAlertsFlyout.forEach((step) => { + it(`step: ${step}, resets to ${step}`, () => { + startTour(); + goToStep(step); + assertTourStepExist(step); + closeAlertFlyout(); + assertTourStepNotExist(step); + expandFirstAlert(); + assertTourStepExist(step); + }); }); - it('can be skipped', () => { - cy.get(WELCOME_STEP).should('be.visible'); - - skipTour(); - // step 1 is not displayed - cy.get(WELCOME_STEP).should('not.exist'); - // step 2 is not displayed - cy.get(MANAGE_STEP).should('not.exist'); + stepsInCasesFlyout.forEach((step) => { + it(`step: ${step}, resets to ${AlertsCasesTourSteps.createCase}`, () => { + startTour(); + goToStep(step); + assertTourStepExist(step); + closeCreateCaseFlyout(); + assertTourStepNotExist(step); + addToCase(); + assertTourStepExist(AlertsCasesTourSteps.createCase); + }); }); }); }); diff --git a/x-pack/plugins/security_solution/cypress/screens/guided_onboarding.ts b/x-pack/plugins/security_solution/cypress/screens/guided_onboarding.ts index 6b3f4bc20ac03..f11c18817e1ba 100644 --- a/x-pack/plugins/security_solution/cypress/screens/guided_onboarding.ts +++ b/x-pack/plugins/security_solution/cypress/screens/guided_onboarding.ts @@ -5,12 +5,13 @@ * 2.0. */ -export const WELCOME_STEP = '[data-test-subj="welcomeStep"]'; -export const MANAGE_STEP = '[data-test-subj="manageStep"]'; -export const ALERTS_STEP = '[data-test-subj="alertsStep"]'; -export const CASES_STEP = '[data-test-subj="casesStep"]'; -export const DATA_STEP = '[data-test-subj="dataStep"]'; +export const ALERTS_STEP_GUIDE_BUTTON = '[data-test-subj="onboarding--stepButton--siem--step3"]'; +export const COMPLETE_SIEM_GUIDE_BUTTON = + '[data-test-subj="onboarding--completeGuideButton--siem"]'; export const NEXT_STEP_BUTTON = '[data-test-subj="onboarding--securityTourNextStepButton"]'; -export const END_TOUR_BUTTON = '[data-test-subj="onboarding--securityTourEndButton"]'; -export const SKIP_TOUR_BUTTON = '[data-test-subj="onboarding--securityTourSkipButton"]'; +export const COMPLETION_POPOVER = '[data-test-subj="manualCompletionPopover"]'; + +export const GLOBAL_TOUR_BUTTON = `[data-test-subj="guideButton"]`; + +export const CLOSE_CREATE_CASE_FLYOUT = `[data-test-subj="create-case-flyout"] [data-test-subj="euiFlyoutCloseButton"]`; diff --git a/x-pack/plugins/security_solution/cypress/tasks/api_calls/tour.ts b/x-pack/plugins/security_solution/cypress/tasks/api_calls/tour.ts new file mode 100644 index 0000000000000..5eac1af18745f --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/tasks/api_calls/tour.ts @@ -0,0 +1,42 @@ +/* + * 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. + */ + +const alertsGuideActiveState = { + isActive: true, + status: 'in_progress', + steps: [ + { id: 'add_data', status: 'complete' }, + { id: 'rules', status: 'complete' }, + { id: 'alertsCases', status: 'active' }, + ], + guideId: 'security', +}; + +export const startAlertsCasesTour = () => + cy.request({ + method: 'PUT', + url: 'api/guided_onboarding/state', + headers: { 'kbn-xsrf': 'cypress-creds' }, + body: { + status: 'in_progress', + guide: alertsGuideActiveState, + }, + }); + +export const quitGlobalTour = () => + cy.request({ + method: 'PUT', + url: 'api/guided_onboarding/state', + headers: { 'kbn-xsrf': 'cypress-creds' }, + body: { + status: 'quit', + guide: { + ...alertsGuideActiveState, + isActive: false, + }, + }, + }); diff --git a/x-pack/plugins/security_solution/cypress/tasks/guided_onboarding.ts b/x-pack/plugins/security_solution/cypress/tasks/guided_onboarding.ts index 2e5c54a396b24..fe3170b31e951 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/guided_onboarding.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/guided_onboarding.ts @@ -5,21 +5,75 @@ * 2.0. */ +import { ATTACH_TO_NEW_CASE_BUTTON, TAKE_ACTION_BTN } from '../screens/alerts'; +import { createCase } from './create_new_case'; import { NEXT_STEP_BUTTON, - END_TOUR_BUTTON, - DATA_STEP, - SKIP_TOUR_BUTTON, + GLOBAL_TOUR_BUTTON, + ALERTS_STEP_GUIDE_BUTTON, + COMPLETION_POPOVER, + COMPLETE_SIEM_GUIDE_BUTTON, + CLOSE_CREATE_CASE_FLYOUT, } from '../screens/guided_onboarding'; +import { expandFirstAlert } from './alerts'; -export const goToNextStep = (currentStep: string) => { - cy.get(`${currentStep} ${NEXT_STEP_BUTTON}`).click(); +export const goToNextStep = (currentStep: number) => { + cy.get( + `[data-test-subj="tourStepAnchor-alertsCases-${currentStep}"] ${NEXT_STEP_BUTTON}` + ).click(); }; -export const completeTour = () => { - cy.get(`${DATA_STEP} ${END_TOUR_BUTTON}`).click(); +export const startTour = () => { + cy.get(GLOBAL_TOUR_BUTTON).click(); + cy.get(ALERTS_STEP_GUIDE_BUTTON).click(); }; -export const skipTour = () => { - cy.get(SKIP_TOUR_BUTTON).click(); +export const finishTour = () => { + cy.get(COMPLETION_POPOVER).should('exist'); + cy.get(GLOBAL_TOUR_BUTTON).click(); + cy.get(ALERTS_STEP_GUIDE_BUTTON).click(); + cy.get(COMPLETE_SIEM_GUIDE_BUTTON).click(); }; + +export const completeTourWithNextButton = () => { + for (let i = 1; i < 6; i++) { + goToNextStep(i); + } + createCase(); + goToNextStep(7); +}; + +export const addToCase = () => { + cy.get(TAKE_ACTION_BTN).click(); + cy.get(ATTACH_TO_NEW_CASE_BUTTON).click(); +}; + +export const completeTourWithActions = () => { + goToNextStep(1); + expandFirstAlert(); + goToNextStep(3); + addToCase(); + goToNextStep(5); + createCase(); + goToNextStep(7); +}; + +export const goToStep = (step: number) => { + for (let i = 1; i < 6; i++) { + if (i === step) { + break; + } + goToNextStep(i); + } + if (step === 7) { + createCase(); + } +}; + +export const assertTourStepExist = (step: number) => + cy.get(`[data-test-subj="tourStepAnchor-alertsCases-${step}"]`).should('exist'); + +export const assertTourStepNotExist = (step: number) => + cy.get(`[data-test-subj="tourStepAnchor-alertsCases-${step}"]`).should('not.exist'); + +export const closeCreateCaseFlyout = () => cy.get(CLOSE_CREATE_CASE_FLYOUT).click(); diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index f1633386a6ea9..d2a97e17a5882 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -27372,11 +27372,6 @@ "xpack.securitySolution.detectionEngine.rules.allRules.columns.tagsTitle": "Balises", "xpack.securitySolution.detectionEngine.rules.allRules.columns.versionTitle": "Version", "xpack.securitySolution.detectionEngine.rules.allRules.exportFilenameTitle": "rules_export", - "xpack.securitySolution.detectionEngine.rules.allRules.featureTour.nextStepLabel": "Aller à l'étape suivante", - "xpack.securitySolution.detectionEngine.rules.allRules.featureTour.previousStepLabel": "Revenir à l'étape précédente", - "xpack.securitySolution.detectionEngine.rules.allRules.featureTour.searchCapabilitiesDescription": "Il est maintenant possible de rechercher des règles par modèle d'indexation, tel que \"filebeat-*\", ou par tactique ou technique MITRE ATT&CK™, telle que \"Évasion par la défense \" ou \"TA0005\".", - "xpack.securitySolution.detectionEngine.rules.allRules.featureTour.searchCapabilitiesTitle": "Capacités de recherche améliorées", - "xpack.securitySolution.detectionEngine.rules.allRules.featureTour.tourTitle": "Nouveautés", "xpack.securitySolution.detectionEngine.rules.allRules.filters.customRulesTitle": "Règles personnalisées", "xpack.securitySolution.detectionEngine.rules.allRules.filters.elasticRulesTitle": "Règles Elastic", "xpack.securitySolution.detectionEngine.rules.allRules.filters.noRulesBodyTitle": "Nous n'avons trouvé aucune règle avec les filtres ci-dessus.", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index f817015c2ee9a..5ad47b09d93de 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -27347,11 +27347,6 @@ "xpack.securitySolution.detectionEngine.rules.allRules.columns.tagsTitle": "タグ", "xpack.securitySolution.detectionEngine.rules.allRules.columns.versionTitle": "バージョン", "xpack.securitySolution.detectionEngine.rules.allRules.exportFilenameTitle": "rules_export", - "xpack.securitySolution.detectionEngine.rules.allRules.featureTour.nextStepLabel": "次のステップに進む", - "xpack.securitySolution.detectionEngine.rules.allRules.featureTour.previousStepLabel": "前のステップに戻る", - "xpack.securitySolution.detectionEngine.rules.allRules.featureTour.searchCapabilitiesDescription": "「filebeat-*」などのインデックスパターンや、「Defense Evasion」や「TA0005」などのMITRE ATT&CK™方式または手法でルールを検索できます。", - "xpack.securitySolution.detectionEngine.rules.allRules.featureTour.searchCapabilitiesTitle": "拡張検索機能", - "xpack.securitySolution.detectionEngine.rules.allRules.featureTour.tourTitle": "新機能", "xpack.securitySolution.detectionEngine.rules.allRules.filters.customRulesTitle": "カスタムルール", "xpack.securitySolution.detectionEngine.rules.allRules.filters.elasticRulesTitle": "Elasticルール", "xpack.securitySolution.detectionEngine.rules.allRules.filters.noRulesBodyTitle": "上記のフィルターでルールが見つかりませんでした。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 02e20106947ea..ea64b3b043ab0 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -27381,11 +27381,6 @@ "xpack.securitySolution.detectionEngine.rules.allRules.columns.tagsTitle": "标签", "xpack.securitySolution.detectionEngine.rules.allRules.columns.versionTitle": "版本", "xpack.securitySolution.detectionEngine.rules.allRules.exportFilenameTitle": "rules_export", - "xpack.securitySolution.detectionEngine.rules.allRules.featureTour.nextStepLabel": "前往下一步", - "xpack.securitySolution.detectionEngine.rules.allRules.featureTour.previousStepLabel": "前往上一步", - "xpack.securitySolution.detectionEngine.rules.allRules.featureTour.searchCapabilitiesDescription": "现在可以按搜索模式(如“filebeat-*”) 或者 MITRE ATT&CK™ 策略或技术(如“Defense Evasion”或“TA0005”)搜索规则。", - "xpack.securitySolution.detectionEngine.rules.allRules.featureTour.searchCapabilitiesTitle": "已增强搜索功能", - "xpack.securitySolution.detectionEngine.rules.allRules.featureTour.tourTitle": "最新动态", "xpack.securitySolution.detectionEngine.rules.allRules.filters.customRulesTitle": "定制规则", "xpack.securitySolution.detectionEngine.rules.allRules.filters.elasticRulesTitle": "Elastic 规则", "xpack.securitySolution.detectionEngine.rules.allRules.filters.noRulesBodyTitle": "使用上述筛选,我们无法找到任何规则。", diff --git a/x-pack/test/security_solution_cypress/config.ts b/x-pack/test/security_solution_cypress/config.ts index 18d7577516fd9..843d6457b55f1 100644 --- a/x-pack/test/security_solution_cypress/config.ts +++ b/x-pack/test/security_solution_cypress/config.ts @@ -52,6 +52,8 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { `--xpack.securitySolution.enableExperimental=${JSON.stringify([ 'alertDetailsPageEnabled', ])}`, + // mock cloud to enable the guided onboarding tour in e2e tests + '--xpack.cloud.id=test', `--home.disableWelcomeScreen=true`, ], }, From caab55fbbfabb1da61370d579849d6031533fa95 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Thu, 17 Nov 2022 15:52:22 -0800 Subject: [PATCH 12/78] [DOCS] Add Opsgenie to run connector API (#145311) --- .../actions-and-connectors/execute.asciidoc | 121 +++++++++++++++++- docs/management/connectors/index.asciidoc | 4 +- 2 files changed, 119 insertions(+), 6 deletions(-) diff --git a/docs/api/actions-and-connectors/execute.asciidoc b/docs/api/actions-and-connectors/execute.asciidoc index b5c59bb86bc70..8b046038b8d07 100644 --- a/docs/api/actions-and-connectors/execute.asciidoc +++ b/docs/api/actions-and-connectors/execute.asciidoc @@ -60,8 +60,7 @@ refer to <>. `documents`:: (Required, array of objects) The documents to index in JSON format. -For more information, refer to -{kibana-ref}/index-action-type.html[Index connector and action]. +For more information, refer to <>. ===== .Jira connectors @@ -158,10 +157,124 @@ the knowledge base. ======= ====== -For more information, refer to -{kibana-ref}/jira-action-type.html[{jira} connector and action]. +For more information, refer to <>. ===== +.{opsgenie} connectors +[%collapsible%open] +===== +`subAction`:: +(Required, string) The action to test. Valid values include: `createAlert` and +`closeAlert`. + +`subActionParams`:: +(Required, object) The set of configuration properties, which vary depending on +the `subAction` value. ++ +.Properties when `subAction` is `createAlert` +[%collapsible%open] +====== +`actions`:::: +(Optional, array of strings) The custom actions available to the alert. + +`alias`:::: +(Optional, string) The unique identifier used for alert de-duplication in {opsgenie}. + +`description`:::: +(Optional, string) A description that provides detailed information about the alert. + +`details`:::: +(Optional, object) The custom properties of the alert. For example: +`{"key1":"value1","key2":"value2"}`. + +`entity`:::: +(Optional, string) The domain of the alert. For example, the application or server +name. + +`message`:::: +(Required, string) The alert message. + +`note`:::: +(Optional, string) Additional information for the alert. + +`priority`:::: +(Optional, string) The priority level for the alert. Valid values are: `P1`, +`P2`, `P3`, `P4`, and `P5`. + +`responders`:::: +(Optional, array of objects) The entities to receive notifications about the +alert. If `type` is `user`, either `id` or `username` is required. If `type` is +`team`, either `id` or `name` is required. ++ +.Properties of `responders` objects +[%collapsible%open] +======= +`id`:::: +(Required^*^, string) The identifier for the entity. + +`name`:::: +(Required^*^, string) The name of the entity. + +`type`:::: +(Required, string) Valid values are `escalation`, `schedule`, `team`, and `user`. + +`username`:::: +(Required^*^, string) A valid email address for the user. +======= + +`source`:::: +(Optional, string) The display name for the source of the alert. + +`tags`:::: +(Optional, array of strings) The tags for the alert. + +`user`:::: +(Optional, string) The display name for the owner. + +`visibleTo`:::: +(Optional, array of objects) The teams and users that the alert will be visible +to without sending a notification. Only one of `id`, `name`, or `username` is +required. ++ +.Properties of `visibleTo` objects +[%collapsible%open] +======= +`id`:::: +(Required^*^, string) The identifier for the entity. + +`name`:::: +(Required^*^, string) The name of the entity. + +`type`:::: +(Required, string) Valid values are `team` and `user`. + +`username`:::: +(Required^*^, string) The user name. This property is required only when the +`type` is `user`. +======= +====== ++ +.Properties when `subAction` is `closeAlert` +[%collapsible%open] +====== +`alias`:::: +(Required, string) The unique identifier used for alert de-duplication in {opsgenie}. +The alias must match the value used when creating the alert. + +`note`:::: +(Optional, string) Additional information for the alert. + +`source`:::: +(Optional, string) The display name for the source of the alert. + +`user`:::: +(Optional, string) The display name for the owner. +====== + +For more information, refer to <>. +===== + + .{sn-itom} connectors [%collapsible%open] ===== diff --git a/docs/management/connectors/index.asciidoc b/docs/management/connectors/index.asciidoc index fe65120e4b2b9..b443ffd967a6f 100644 --- a/docs/management/connectors/index.asciidoc +++ b/docs/management/connectors/index.asciidoc @@ -3,6 +3,7 @@ include::action-types/resilient.asciidoc[] include::action-types/index.asciidoc[] include::action-types/jira.asciidoc[] include::action-types/teams.asciidoc[] +include::action-types/opsgenie.asciidoc[] include::action-types/pagerduty.asciidoc[] include::action-types/server-log.asciidoc[] include::action-types/servicenow.asciidoc[leveloffset=+1] @@ -10,9 +11,8 @@ include::action-types/servicenow-sir.asciidoc[leveloffset=+1] include::action-types/servicenow-itom.asciidoc[leveloffset=+1] include::action-types/swimlane.asciidoc[] include::action-types/slack.asciidoc[] +include::action-types/tines.asciidoc[leveloffset=+1] include::action-types/webhook.asciidoc[] include::action-types/cases-webhook.asciidoc[leveloffset=+1] -include::action-types/opsgenie.asciidoc[] include::action-types/xmatters.asciidoc[] -include::action-types/tines.asciidoc[leveloffset=+1] include::pre-configured-connectors.asciidoc[] From 64bae2ea096cedf4b2a3d58adf702fa8d12e7721 Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Thu, 17 Nov 2022 18:04:11 -0600 Subject: [PATCH 13/78] skip src/core/server/integration_tests/ui_settings/jest.integration.config.js (#145646) This suite has become flaky: https://buildkite.com/elastic/kibana-on-merge/builds/23746#01848784-7e14-4800-b63a-bfc5905864d7/3656-4619 https://buildkite.com/elastic/kibana-on-merge/builds/23745#01848782-d6ff-42f5-a111-6f2608db2c47/2443-3188 --- .buildkite/disabled_jest_configs.json | 3 ++- .../pipeline-utils/ci-stats/pick_test_group_run_order.ts | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.buildkite/disabled_jest_configs.json b/.buildkite/disabled_jest_configs.json index 9727d38158520..ce6235823b0ab 100644 --- a/.buildkite/disabled_jest_configs.json +++ b/.buildkite/disabled_jest_configs.json @@ -1,4 +1,5 @@ [ "x-pack/plugins/triggers_actions_ui/jest.config.js", - "x-pack/plugins/watcher/jest.config.js" + "x-pack/plugins/watcher/jest.config.js", + "src/core/server/integration_tests/ui_settings/jest.integration.config.js" ] diff --git a/.buildkite/pipeline-utils/ci-stats/pick_test_group_run_order.ts b/.buildkite/pipeline-utils/ci-stats/pick_test_group_run_order.ts index b7c223b3ca595..dfb384d7e3998 100644 --- a/.buildkite/pipeline-utils/ci-stats/pick_test_group_run_order.ts +++ b/.buildkite/pipeline-utils/ci-stats/pick_test_group_run_order.ts @@ -234,6 +234,7 @@ export async function pickTestGroupRunOrder() { ? globby.sync(['**/jest.integration.config.js', '!**/__fixtures__/**'], { cwd: process.cwd(), absolute: false, + ignore: DISABLED_JEST_CONFIGS, }) : []; From 31ca6447332e52e502f0ed3645ebb02f054807c7 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Thu, 17 Nov 2022 17:08:41 -0800 Subject: [PATCH 14/78] [DOCS] Automate connector-listing.png (#143605) Co-authored-by: Robert Oskamp --- docs/management/action-types.asciidoc | 1 + .../connectors/images/connector-listing.png | Bin 126507 -> 95274 bytes x-pack/test/functional/config.base.js | 3 ++ .../test/functional/services/actions/api.ts | 46 +++++++++++++++++ .../test/functional/services/actions/index.ts | 2 + x-pack/test/functional/services/index.ts | 2 + .../functional/services/ml/test_resources.ts | 34 ------------- .../functional/services/sample_data/index.ts | 15 ++++++ .../services/sample_data/test_resources.ts | 37 ++++++++++++++ .../triggers_actions_ui/connectors/general.ts | 12 ++--- .../connectors/opsgenie.ts | 5 +- .../triggers_actions_ui/connectors/tines.ts | 5 +- .../triggers_actions_ui/connectors/utils.ts | 37 ++------------ .../screenshot_creation/apps/ml_docs/index.ts | 5 +- .../apps/response_ops_docs/index.ts | 6 ++- .../response_ops_docs/stack_alerting/index.ts | 14 +++++ .../stack_alerting/list_view.ts | 48 ++++++++++++++++++ .../ftr_provider_context.d.ts | 2 +- .../screenshot_creation/page_objects/index.ts | 16 ++++++ 19 files changed, 207 insertions(+), 83 deletions(-) create mode 100644 x-pack/test/functional/services/actions/api.ts create mode 100644 x-pack/test/functional/services/sample_data/index.ts create mode 100644 x-pack/test/functional/services/sample_data/test_resources.ts create mode 100644 x-pack/test/screenshot_creation/apps/response_ops_docs/stack_alerting/index.ts create mode 100644 x-pack/test/screenshot_creation/apps/response_ops_docs/stack_alerting/list_view.ts create mode 100644 x-pack/test/screenshot_creation/page_objects/index.ts diff --git a/docs/management/action-types.asciidoc b/docs/management/action-types.asciidoc index 74738a8fddeb1..330f05734cb6a 100644 --- a/docs/management/action-types.asciidoc +++ b/docs/management/action-types.asciidoc @@ -91,6 +91,7 @@ Rules use connectors to route actions to different destinations like log files, [role="screenshot"] image::images/connector-listing.png[Example connector listing in the {rules-ui} UI] +// NOTE: This is an autogenerated screenshot. Do not edit it directly. [float] === Required permissions diff --git a/docs/management/connectors/images/connector-listing.png b/docs/management/connectors/images/connector-listing.png index 3c743ede804d38376503887b62368b8b880eee09..aef38f84d4b543b5d43dd92c36f28cc017fc221c 100644 GIT binary patch literal 95274 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

HPUz%>u@~t7hBdb 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 literal 126507 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