diff --git a/x-pack/plugins/aiops/common/api/log_categorization/create_categorize_query.ts b/x-pack/plugins/aiops/common/api/log_categorization/create_categorize_query.ts new file mode 100644 index 0000000000000..8b9e3a95c653f --- /dev/null +++ b/x-pack/plugins/aiops/common/api/log_categorization/create_categorize_query.ts @@ -0,0 +1,48 @@ +/* + * 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 { cloneDeep } from 'lodash'; + +import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; + +export function createCategorizeQuery( + queryIn: QueryDslQueryContainer, + timeField: string, + from: number | undefined, + to: number | undefined +) { + const query = cloneDeep(queryIn); + + if (query.bool === undefined) { + query.bool = {}; + } + if (query.bool.must === undefined) { + query.bool.must = []; + if (query.match_all !== undefined) { + query.bool.must.push({ match_all: query.match_all }); + delete query.match_all; + } + } + if (query.multi_match !== undefined) { + query.bool.should = { + multi_match: query.multi_match, + }; + delete query.multi_match; + } + + (query.bool.must as QueryDslQueryContainer[]).push({ + range: { + [timeField]: { + gte: from, + lte: to, + format: 'epoch_millis', + }, + }, + }); + + return query; +} diff --git a/x-pack/plugins/aiops/common/api/log_categorization/create_category_request.ts b/x-pack/plugins/aiops/common/api/log_categorization/create_category_request.ts new file mode 100644 index 0000000000000..38acb5029e830 --- /dev/null +++ b/x-pack/plugins/aiops/common/api/log_categorization/create_category_request.ts @@ -0,0 +1,66 @@ +/* + * 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 { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; + +import { createRandomSamplerWrapper } from '@kbn/ml-random-sampler-utils'; + +import { createCategorizeQuery } from './create_categorize_query'; + +const CATEGORY_LIMIT = 1000; +const EXAMPLE_LIMIT = 1; + +export function createCategoryRequest( + index: string, + field: string, + timeField: string, + from: number | undefined, + to: number | undefined, + queryIn: QueryDslQueryContainer, + wrap: ReturnType['wrap'], + intervalMs?: number +) { + const query = createCategorizeQuery(queryIn, timeField, from, to); + const aggs = { + categories: { + categorize_text: { + field, + size: CATEGORY_LIMIT, + }, + aggs: { + hit: { + top_hits: { + size: EXAMPLE_LIMIT, + sort: [timeField], + _source: field, + }, + }, + ...(intervalMs + ? { + sparkline: { + date_histogram: { + field: timeField, + fixed_interval: `${intervalMs}ms`, + }, + }, + } + : {}), + }, + }, + }; + + return { + params: { + index, + size: 0, + body: { + query, + aggs: wrap(aggs), + }, + }, + }; +} diff --git a/x-pack/plugins/aiops/common/api/log_categorization/get_category_query.ts b/x-pack/plugins/aiops/common/api/log_categorization/get_category_query.ts new file mode 100644 index 0000000000000..ba6e2886f6a9e --- /dev/null +++ b/x-pack/plugins/aiops/common/api/log_categorization/get_category_query.ts @@ -0,0 +1,33 @@ +/* + * 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 { Category } from './types'; + +export const QUERY_MODE = { + INCLUDE: 'should', + EXCLUDE: 'must_not', +} as const; +export type QueryMode = typeof QUERY_MODE[keyof typeof QUERY_MODE]; + +export const getCategoryQuery = ( + field: string, + categories: Category[], + mode: QueryMode = QUERY_MODE.INCLUDE +) => ({ + bool: { + [mode]: categories.map(({ key: query }) => ({ + match: { + [field]: { + auto_generate_synonyms_phrase_query: false, + fuzziness: 0, + operator: 'and', + query, + }, + }, + })), + }, +}); diff --git a/x-pack/plugins/aiops/common/api/log_categorization/process_category_results.ts b/x-pack/plugins/aiops/common/api/log_categorization/process_category_results.ts new file mode 100644 index 0000000000000..347ba5711719d --- /dev/null +++ b/x-pack/plugins/aiops/common/api/log_categorization/process_category_results.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { get } from 'lodash'; + +import { estypes } from '@elastic/elasticsearch'; + +import { createRandomSamplerWrapper } from '@kbn/ml-random-sampler-utils'; + +import type { Category, CategoriesAgg, CatResponse, SparkLinesPerCategory } from './types'; + +export function processCategoryResults( + result: CatResponse, + field: string, + unwrap: ReturnType['unwrap'] +) { + const sparkLinesPerCategory: SparkLinesPerCategory = {}; + const { aggregations } = result.rawResponse; + if (aggregations === undefined) { + throw new Error('processCategoryResults failed, did not return aggregations.'); + } + const { + categories: { buckets }, + } = unwrap( + aggregations as unknown as Record + ) as CategoriesAgg; + + const categories: Category[] = buckets.map((b) => { + sparkLinesPerCategory[b.key] = + b.sparkline === undefined + ? {} + : b.sparkline.buckets.reduce>((acc2, cur2) => { + acc2[cur2.key] = cur2.doc_count; + return acc2; + }, {}); + + return { + key: b.key, + count: b.doc_count, + examples: b.hit.hits.hits.map((h) => get(h._source, field)), + }; + }); + return { + categories, + sparkLinesPerCategory, + }; +} diff --git a/x-pack/plugins/aiops/common/api/log_categorization/types.ts b/x-pack/plugins/aiops/common/api/log_categorization/types.ts new file mode 100644 index 0000000000000..83e16d8ada7d0 --- /dev/null +++ b/x-pack/plugins/aiops/common/api/log_categorization/types.ts @@ -0,0 +1,38 @@ +/* + * 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 { estypes } from '@elastic/elasticsearch'; + +export interface Category { + key: string; + count: number; + examples: string[]; + sparkline?: Array<{ doc_count: number; key: number; key_as_string: string }>; +} + +export interface CategoriesAgg { + categories: { + buckets: Array<{ + key: string; + doc_count: number; + hit: { hits: { hits: Array<{ _source: { message: string } }> } }; + sparkline: { + buckets: Array<{ key_as_string: string; key: number; doc_count: number }>; + }; + }>; + }; +} + +interface CategoriesSampleAgg { + sample: CategoriesAgg; +} + +export interface CatResponse { + rawResponse: estypes.SearchResponseBody; +} + +export type SparkLinesPerCategory = Record>; diff --git a/x-pack/plugins/aiops/public/components/log_categorization/category_table/category_table.tsx b/x-pack/plugins/aiops/public/components/log_categorization/category_table/category_table.tsx index 1dc30c04253bc..346fc7f5b3562 100644 --- a/x-pack/plugins/aiops/public/components/log_categorization/category_table/category_table.tsx +++ b/x-pack/plugins/aiops/public/components/log_categorization/category_table/category_table.tsx @@ -6,8 +6,7 @@ */ import React, { FC, useMemo, useState } from 'react'; -import { i18n } from '@kbn/i18n'; -import type { TimefilterContract } from '@kbn/data-plugin/public'; + import { useEuiBackgroundColor, EuiInMemoryTable, @@ -19,14 +18,25 @@ import { EuiSpacer, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import type { TimefilterContract } from '@kbn/data-plugin/public'; import { DataViewField } from '@kbn/data-views-plugin/common'; import { Filter } from '@kbn/es-query'; import { useTableState } from '@kbn/ml-in-memory-table'; -import { useDiscoverLinks, createFilter, QueryMode, QUERY_MODE } from '../use_discover_links'; -import { MiniHistogram } from '../../mini_histogram'; + +import type { + Category, + SparkLinesPerCategory, +} from '../../../../common/api/log_categorization/types'; + import { useEuiTheme } from '../../../hooks/use_eui_theme'; import type { LogCategorizationAppState } from '../../../application/utils/url_state'; -import type { EventRate, Category, SparkLinesPerCategory } from '../use_categorize_request'; + +import { MiniHistogram } from '../../mini_histogram'; + +import { useDiscoverLinks, createFilter, QueryMode, QUERY_MODE } from '../use_discover_links'; +import type { EventRate } from '../use_categorize_request'; + import { getLabels } from './labels'; import { TableHeader } from './table_header'; diff --git a/x-pack/plugins/aiops/public/components/log_categorization/document_count_chart.tsx b/x-pack/plugins/aiops/public/components/log_categorization/document_count_chart.tsx index d34861b924ccb..859eaed0fec57 100644 --- a/x-pack/plugins/aiops/public/components/log_categorization/document_count_chart.tsx +++ b/x-pack/plugins/aiops/public/components/log_categorization/document_count_chart.tsx @@ -9,11 +9,15 @@ import React, { FC, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { DocumentCountChart as DocumentCountChartRoot } from '@kbn/aiops-components'; + +import type { Category, SparkLinesPerCategory } from '../../../common/api/log_categorization/types'; + import { useAiopsAppContext } from '../../hooks/use_aiops_app_context'; +import { DocumentCountStats } from '../../get_document_stats'; + import { TotalCountHeader } from '../document_count_content/total_count_header'; -import type { Category, SparkLinesPerCategory } from './use_categorize_request'; + import type { EventRate } from './use_categorize_request'; -import { DocumentCountStats } from '../../get_document_stats'; interface Props { totalCount: number; diff --git a/x-pack/plugins/aiops/public/components/log_categorization/log_categorization_for_flyout.tsx b/x-pack/plugins/aiops/public/components/log_categorization/log_categorization_for_flyout.tsx index 599a669197ee5..28c1350a88141 100644 --- a/x-pack/plugins/aiops/public/components/log_categorization/log_categorization_for_flyout.tsx +++ b/x-pack/plugins/aiops/public/components/log_categorization/log_categorization_for_flyout.tsx @@ -5,10 +5,7 @@ * 2.0. */ import React, { FC, useState, useEffect, useCallback, useRef, useMemo } from 'react'; -import type { SavedSearch } from '@kbn/saved-search-plugin/public'; -import type { DataView, DataViewField } from '@kbn/data-views-plugin/public'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; + import { EuiTitle, EuiFlyoutHeader, @@ -18,26 +15,33 @@ import { useEuiTheme, } from '@elastic/eui'; +import type { SavedSearch } from '@kbn/saved-search-plugin/public'; +import type { DataView, DataViewField } from '@kbn/data-views-plugin/public'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; import { buildEmptyFilter, Filter } from '@kbn/es-query'; - import { usePageUrlState } from '@kbn/ml-url-state'; import type { FieldValidationResults } from '@kbn/ml-category-validator'; + +import type { Category, SparkLinesPerCategory } from '../../../common/api/log_categorization/types'; + +import { + type LogCategorizationPageUrlState, + getDefaultLogCategorizationAppState, +} from '../../application/utils/url_state'; +import { createMergedEsQuery } from '../../application/utils/search_utils'; import { useData } from '../../hooks/use_data'; import { useSearch } from '../../hooks/use_search'; +import { useAiopsAppContext } from '../../hooks/use_aiops_app_context'; + import { useCategorizeRequest } from './use_categorize_request'; -import type { EventRate, Category, SparkLinesPerCategory } from './use_categorize_request'; +import type { EventRate } from './use_categorize_request'; import { CategoryTable } from './category_table'; -import { useAiopsAppContext } from '../../hooks/use_aiops_app_context'; import { InformationText } from './information_text'; -import { createMergedEsQuery } from '../../application/utils/search_utils'; import { SamplingMenu } from './sampling_menu'; import { TechnicalPreviewBadge } from './technical_preview_badge'; import { LoadingCategorization } from './loading_categorization'; import { useValidateFieldRequest } from './use_validate_category_field'; -import { - type LogCategorizationPageUrlState, - getDefaultLogCategorizationAppState, -} from '../../application/utils/url_state'; import { FieldValidationCallout } from './category_validation_callout'; export interface LogCategorizationPageProps { diff --git a/x-pack/plugins/aiops/public/components/log_categorization/log_categorization_page.tsx b/x-pack/plugins/aiops/public/components/log_categorization/log_categorization_page.tsx index 9539ff607e05e..5ccdca64d1036 100644 --- a/x-pack/plugins/aiops/public/components/log_categorization/log_categorization_page.tsx +++ b/x-pack/plugins/aiops/public/components/log_categorization/log_categorization_page.tsx @@ -24,9 +24,11 @@ import { Filter, Query } from '@kbn/es-query'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { usePageUrlState, useUrlState } from '@kbn/ml-url-state'; - import type { FieldValidationResults } from '@kbn/ml-category-validator'; import type { SearchQueryLanguage } from '@kbn/ml-query-utils'; + +import type { Category, SparkLinesPerCategory } from '../../../common/api/log_categorization/types'; + import { useDataSource } from '../../hooks/use_data_source'; import { useData } from '../../hooks/use_data'; import { useSearch } from '../../hooks/use_search'; @@ -39,7 +41,7 @@ import { import { SearchPanel } from '../search_panel'; import { PageHeader } from '../page_header'; -import type { EventRate, Category, SparkLinesPerCategory } from './use_categorize_request'; +import type { EventRate } from './use_categorize_request'; import { useCategorizeRequest } from './use_categorize_request'; import { CategoryTable } from './category_table'; import { DocumentCountChart } from './document_count_chart'; diff --git a/x-pack/plugins/aiops/public/components/log_categorization/use_categorize_request.ts b/x-pack/plugins/aiops/public/components/log_categorization/use_categorize_request.ts index 3108ec9391cef..8179751266e6e 100644 --- a/x-pack/plugins/aiops/public/components/log_categorization/use_categorize_request.ts +++ b/x-pack/plugins/aiops/public/components/log_categorization/use_categorize_request.ts @@ -5,62 +5,37 @@ * 2.0. */ -import { cloneDeep, get } from 'lodash'; import { useRef, useCallback, useMemo } from 'react'; -import { isCompleteResponse } from '@kbn/data-plugin/public'; + import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; -import { createRandomSamplerWrapper } from '@kbn/ml-random-sampler-utils'; -import { estypes } from '@elastic/elasticsearch'; +import { isCompleteResponse } from '@kbn/data-plugin/public'; import { useStorage } from '@kbn/ml-local-storage'; + +import { createCategoryRequest } from '../../../common/api/log_categorization/create_category_request'; +import { processCategoryResults } from '../../../common/api/log_categorization/process_category_results'; +import type { + Category, + CatResponse, + SparkLinesPerCategory, +} from '../../../common/api/log_categorization/types'; + import { useAiopsAppContext } from '../../hooks/use_aiops_app_context'; -import { RandomSampler } from './sampling_menu'; import { type AiOpsKey, type AiOpsStorageMapped, AIOPS_RANDOM_SAMPLING_MODE_PREFERENCE, AIOPS_RANDOM_SAMPLING_PROBABILITY_PREFERENCE, } from '../../types/storage'; -import { RANDOM_SAMPLER_OPTION, DEFAULT_PROBABILITY } from './sampling_menu/random_sampler'; -const CATEGORY_LIMIT = 1000; -const EXAMPLE_LIMIT = 1; - -interface CategoriesAgg { - categories: { - buckets: Array<{ - key: string; - doc_count: number; - hit: { hits: { hits: Array<{ _source: { message: string } }> } }; - sparkline: { - buckets: Array<{ key_as_string: string; key: number; doc_count: number }>; - }; - }>; - }; -} - -interface CategoriesSampleAgg { - sample: CategoriesAgg; -} - -interface CatResponse { - rawResponse: estypes.SearchResponseBody; -} - -export interface Category { - key: string; - count: number; - examples: string[]; - sparkline?: Array<{ doc_count: number; key: number; key_as_string: string }>; -} +import { RandomSampler } from './sampling_menu'; +import { RANDOM_SAMPLER_OPTION, DEFAULT_PROBABILITY } from './sampling_menu/random_sampler'; export type EventRate = Array<{ key: number; docCount: number; }>; -export type SparkLinesPerCategory = Record>; - export function useCategorizeRequest() { const [randomSamplerMode, setRandomSamplerMode] = useStorage< AiOpsKey, @@ -135,129 +110,3 @@ export function useCategorizeRequest() { return { runCategorizeRequest, cancelRequest, randomSampler }; } - -function createCategoryRequest( - index: string, - field: string, - timeField: string, - from: number | undefined, - to: number | undefined, - queryIn: QueryDslQueryContainer, - wrap: ReturnType['wrap'], - intervalMs?: number -) { - const query = createCategorizeQuery(queryIn, timeField, from, to); - const aggs = { - categories: { - categorize_text: { - field, - size: CATEGORY_LIMIT, - }, - aggs: { - hit: { - top_hits: { - size: EXAMPLE_LIMIT, - sort: [timeField], - _source: field, - }, - }, - ...(intervalMs - ? { - sparkline: { - date_histogram: { - field: timeField, - fixed_interval: `${intervalMs}ms`, - }, - }, - } - : {}), - }, - }, - }; - - return { - params: { - index, - size: 0, - body: { - query, - aggs: wrap(aggs), - }, - }, - }; -} - -export function createCategorizeQuery( - queryIn: QueryDslQueryContainer, - timeField: string, - from: number | undefined, - to: number | undefined -) { - const query = cloneDeep(queryIn); - - if (query.bool === undefined) { - query.bool = {}; - } - if (query.bool.must === undefined) { - query.bool.must = []; - if (query.match_all !== undefined) { - query.bool.must.push({ match_all: query.match_all }); - delete query.match_all; - } - } - if (query.multi_match !== undefined) { - query.bool.should = { - multi_match: query.multi_match, - }; - delete query.multi_match; - } - - (query.bool.must as QueryDslQueryContainer[]).push({ - range: { - [timeField]: { - gte: from, - lte: to, - format: 'epoch_millis', - }, - }, - }); - - return query; -} - -function processCategoryResults( - result: CatResponse, - field: string, - unwrap: ReturnType['unwrap'] -) { - const sparkLinesPerCategory: SparkLinesPerCategory = {}; - const { aggregations } = result.rawResponse; - if (aggregations === undefined) { - throw new Error('processCategoryResults failed, did not return aggregations.'); - } - const { - categories: { buckets }, - } = unwrap( - aggregations as unknown as Record - ) as CategoriesAgg; - - const categories: Category[] = buckets.map((b) => { - sparkLinesPerCategory[b.key] = - b.sparkline === undefined - ? {} - : b.sparkline.buckets.reduce>((acc2, cur2) => { - acc2[cur2.key] = cur2.doc_count; - return acc2; - }, {}); - - return { - key: b.key, - count: b.doc_count, - examples: b.hit.hits.hits.map((h) => get(h._source, field)), - }; - }); - return { - categories, - sparkLinesPerCategory, - }; -} diff --git a/x-pack/plugins/aiops/public/components/log_categorization/use_discover_links.ts b/x-pack/plugins/aiops/public/components/log_categorization/use_discover_links.ts index cb3b60268f438..22e8e50ebcf19 100644 --- a/x-pack/plugins/aiops/public/components/log_categorization/use_discover_links.ts +++ b/x-pack/plugins/aiops/public/components/log_categorization/use_discover_links.ts @@ -5,15 +5,18 @@ * 2.0. */ -import rison from '@kbn/rison'; import moment from 'moment'; +import rison from '@kbn/rison'; import type { TimeRangeBounds } from '@kbn/data-plugin/common'; import { i18n } from '@kbn/i18n'; import type { Filter } from '@kbn/es-query'; + +import { getCategoryQuery } from '../../../common/api/log_categorization/get_category_query'; +import type { Category } from '../../../common/api/log_categorization/types'; + import type { AiOpsIndexBasedAppState } from '../../application/utils/url_state'; import { useAiopsAppContext } from '../../hooks/use_aiops_app_context'; -import type { Category } from './use_categorize_request'; export const QUERY_MODE = { INCLUDE: 'should', @@ -71,20 +74,7 @@ export function createFilter( ): Filter { const selectedRows = category === undefined ? selection : [category]; return { - query: { - bool: { - [mode]: selectedRows.map(({ key: query }) => ({ - match: { - [field]: { - auto_generate_synonyms_phrase_query: false, - fuzziness: 0, - operator: 'and', - query, - }, - }, - })), - }, - }, + query: getCategoryQuery(field, selectedRows, mode), meta: { alias: i18n.translate('xpack.aiops.logCategorization.filterAliasLabel', { defaultMessage: 'Categorization - {field}', diff --git a/x-pack/plugins/aiops/public/components/log_categorization/use_validate_category_field.ts b/x-pack/plugins/aiops/public/components/log_categorization/use_validate_category_field.ts index d354ed5f3fbb6..8e0850aa1daa8 100644 --- a/x-pack/plugins/aiops/public/components/log_categorization/use_validate_category_field.ts +++ b/x-pack/plugins/aiops/public/components/log_categorization/use_validate_category_field.ts @@ -8,10 +8,13 @@ import { useRef, useCallback } from 'react'; import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; + import type { FieldValidationResults } from '@kbn/ml-category-validator'; + import { AIOPS_API_ENDPOINT } from '../../../common/api'; +import { createCategorizeQuery } from '../../../common/api/log_categorization/create_categorize_query'; + import { useAiopsAppContext } from '../../hooks/use_aiops_app_context'; -import { createCategorizeQuery } from './use_categorize_request'; export function useValidateFieldRequest() { const { http } = useAiopsAppContext(); diff --git a/x-pack/plugins/cloud_security_posture/common/constants.ts b/x-pack/plugins/cloud_security_posture/common/constants.ts index 23de4b6d02e36..1cc356cbfd5e3 100644 --- a/x-pack/plugins/cloud_security_posture/common/constants.ts +++ b/x-pack/plugins/cloud_security_posture/common/constants.ts @@ -100,6 +100,9 @@ export const CLOUDBEAT_VULN_MGMT_GCP = 'cloudbeat/vuln_mgmt_gcp'; export const CLOUDBEAT_VULN_MGMT_AZURE = 'cloudbeat/vuln_mgmt_azure'; export const CIS_AWS = 'cis_aws'; export const CIS_GCP = 'cis_gcp'; +export const CIS_K8S = 'cis_k8s'; +export const CIS_EKS = 'cis_eks'; +export const CIS_AZURE = 'cis_azure'; export const KSPM_POLICY_TEMPLATE = 'kspm'; export const CSPM_POLICY_TEMPLATE = 'cspm'; export const VULN_MGMT_POLICY_TEMPLATE = 'vuln_mgmt'; diff --git a/x-pack/plugins/cloud_security_posture/public/components/accounts_evaluated_widget.tsx b/x-pack/plugins/cloud_security_posture/public/components/accounts_evaluated_widget.tsx index 39b37c85aee39..418b1c37a1bdd 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/accounts_evaluated_widget.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/accounts_evaluated_widget.tsx @@ -5,13 +5,43 @@ * 2.0. */ import React from 'react'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { CIS_AWS, CIS_GCP } from '../../common/constants'; +import { EuiFlexGroup, EuiFlexItem, useEuiTheme } from '@elastic/eui'; +import { css } from '@emotion/react'; +import { CIS_AWS, CIS_GCP, CIS_AZURE, CIS_K8S, CIS_EKS } from '../../common/constants'; import { Cluster } from '../../common/types'; import { CISBenchmarkIcon } from './cis_benchmark_icon'; import { CompactFormattedNumber } from './compact_formatted_number'; import { useNavigateFindings } from '../common/hooks/use_navigate_findings'; +// order in array will determine order of appearance in the dashboard +const benchmarks = [ + { + type: CIS_AWS, + name: 'Amazon Web Services (AWS)', + provider: 'aws', + }, + { + type: CIS_GCP, + name: 'Google Cloud Platform (GCP)', + provider: 'gcp', + }, + { + type: CIS_AZURE, + name: 'Azure', + provider: 'azure', + }, + { + type: CIS_K8S, + name: 'Kubernetes', + benchmarkId: 'cis_k8s', + }, + { + type: CIS_EKS, + name: 'Amazon Elastic Kubernetes Service (EKS)', + benchmarkId: 'cis_eks', + }, +]; + export const AccountsEvaluatedWidget = ({ clusters, benchmarkAbbreviateAbove = 999, @@ -20,6 +50,8 @@ export const AccountsEvaluatedWidget = ({ /** numbers higher than the value of this field will be abbreviated using compact notation and have a tooltip displaying the full value */ benchmarkAbbreviateAbove?: number; }) => { + const { euiTheme } = useEuiTheme(); + const filterClustersById = (benchmarkId: string) => { return clusters?.filter((obj) => obj?.meta.benchmark.id === benchmarkId) || []; }; @@ -30,56 +62,52 @@ export const AccountsEvaluatedWidget = ({ navToFindings({ 'cloud.provider': provider }); }; - const cisAwsClusterAmount = filterClustersById(CIS_AWS).length; - const cisGcpClusterAmount = filterClustersById(CIS_GCP).length; + const navToFindingsByCisBenchmark = (cisBenchmark: string) => { + navToFindings({ 'rule.benchmark.id': cisBenchmark }); + }; + + const benchmarkElements = benchmarks.map((benchmark) => { + const clusterAmount = filterClustersById(benchmark.type).length; + + return ( + clusterAmount > 0 && ( + { + if (benchmark.provider) { + navToFindingsByCloudProvider(benchmark.provider); + } + if (benchmark.benchmarkId) { + navToFindingsByCisBenchmark(benchmark.benchmarkId); + } + }} + css={css` + transition: ${euiTheme.animation.normal} ease-in; + border-bottom: ${euiTheme.border.thick}; + border-color: transparent; - const cisAwsBenchmarkName = 'Amazon Web Services (AWS)'; - const cisGcpBenchmarkName = 'Google Cloud Platform (GCP)'; + :hover { + cursor: pointer; + border-color: ${euiTheme.colors.darkestShade}; + } + `} + > + + + + + + + + + + ) + ); + }); - return ( - <> - - {cisAwsClusterAmount > 0 && ( - - - - - - { - navToFindingsByCloudProvider('aws'); - }} - > - - - - - )} - {cisGcpClusterAmount > 0 && ( - - - - - - { - navToFindingsByCloudProvider('gcp'); - }} - > - - - - - )} - - - ); + // Render the benchmark elements + return {benchmarkElements}; }; diff --git a/x-pack/plugins/cloud_security_posture/public/components/cis_benchmark_icon.tsx b/x-pack/plugins/cloud_security_posture/public/components/cis_benchmark_icon.tsx index a0048b21fb92f..50f9ca1b15d9d 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/cis_benchmark_icon.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/cis_benchmark_icon.tsx @@ -23,6 +23,8 @@ const getBenchmarkIdIconType = (props: Props): string => { switch (props.type) { case 'cis_eks': return cisEksIcon; + case 'cis_azure': + return 'logoAzure'; case 'cis_aws': return 'logoAWS'; case 'cis_gcp': diff --git a/x-pack/plugins/cloud_security_posture/public/components/csp_counter_card.tsx b/x-pack/plugins/cloud_security_posture/public/components/csp_counter_card.tsx index def19c1c871ec..9b07c0f3edded 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/csp_counter_card.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/csp_counter_card.tsx @@ -5,14 +5,13 @@ * 2.0. */ -import React, { MouseEventHandler } from 'react'; -import { css } from '@emotion/react'; -import { EuiIcon, EuiPanel, EuiStat, useEuiTheme } from '@elastic/eui'; +import React, { ReactNode } from 'react'; +import { EuiPanel, EuiStat, useEuiTheme, EuiHorizontalRule } from '@elastic/eui'; import type { EuiStatProps } from '@elastic/eui'; export interface CspCounterCardProps { id: string; - onClick?: MouseEventHandler; + button?: ReactNode; title: EuiStatProps['title']; titleColor?: EuiStatProps['titleColor']; description: EuiStatProps['description']; @@ -22,25 +21,10 @@ export const CspCounterCard = (counter: CspCounterCardProps) => { const { euiTheme } = useEuiTheme(); return ( - + { descriptionElement="h6" description={counter.description} /> - {counter.onClick && ( - - )} + + {counter.button} ); }; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/summary_section.test.tsx b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/summary_section.test.tsx index f78b596074349..0545d4f3bb429 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/summary_section.test.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/summary_section.test.tsx @@ -31,11 +31,7 @@ describe('', () => { renderCloudSummarySection(); expectIdsInDoc({ - be: [ - DASHBOARD_COUNTER_CARDS.CLUSTERS_EVALUATED, - DASHBOARD_COUNTER_CARDS.RESOURCES_EVALUATED, - DASHBOARD_COUNTER_CARDS.FAILING_FINDINGS, - ], + be: [DASHBOARD_COUNTER_CARDS.CLUSTERS_EVALUATED, DASHBOARD_COUNTER_CARDS.RESOURCES_EVALUATED], }); }); @@ -46,7 +42,6 @@ describe('', () => { expect(screen.getByTestId(DASHBOARD_COUNTER_CARDS.RESOURCES_EVALUATED)).toHaveTextContent( '162' ); - expect(screen.getByTestId(DASHBOARD_COUNTER_CARDS.FAILING_FINDINGS)).toHaveTextContent('17'); }); it('renders counters value in compact abbreviation if its above one million', () => { @@ -55,12 +50,5 @@ describe('', () => { expect(screen.getByTestId(DASHBOARD_COUNTER_CARDS.RESOURCES_EVALUATED)).toHaveTextContent( '999,999' ); - expect(screen.getByTestId(DASHBOARD_COUNTER_CARDS.FAILING_FINDINGS)).toHaveTextContent('1M'); - }); - - it('renders N/A as an empty state', () => { - renderCloudSummarySection({ stats: { totalFailed: undefined } }); - - expect(screen.getByTestId(DASHBOARD_COUNTER_CARDS.FAILING_FINDINGS)).toHaveTextContent('N/A'); }); }); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/summary_section.tsx b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/summary_section.tsx index 9837c531021f2..50d5493387466 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/summary_section.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/summary_section.tsx @@ -6,10 +6,10 @@ */ import React, { useMemo } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiFlexItemProps } from '@elastic/eui'; +import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiFlexItemProps } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { css } from '@emotion/react'; -import { statusColors } from '../../../common/constants'; +import { useCspIntegrationLink } from '../../../common/navigation/use_csp_integration_link'; import { DASHBOARD_COUNTER_CARDS, DASHBOARD_SUMMARY_CONTAINER } from '../test_subjects'; import { CspCounterCard, CspCounterCardProps } from '../../../components/csp_counter_card'; import { CompactFormattedNumber } from '../../../components/compact_formatted_number'; @@ -56,6 +56,8 @@ export const SummarySection = ({ }) => { const navToFindings = useNavigateFindings(); const navToFindingsByResource = useNavigateFindingsByResource(); + const cspmIntegrationLink = useCspIntegrationLink(CSPM_POLICY_TEMPLATE); + const kspmIntegrationLink = useCspIntegrationLink(KSPM_POLICY_TEMPLATE); const handleEvalCounterClick = (evaluation: Evaluation) => { navToFindings({ 'result.evaluation': evaluation, ...getPolicyTemplateQuery(dashboardType) }); @@ -87,12 +89,26 @@ export const SummarySection = ({ 'xpack.csp.dashboard.summarySection.counterCard.accountsEvaluatedDescription', { defaultMessage: 'Accounts Evaluated' } ), - title: - dashboardType === KSPM_POLICY_TEMPLATE ? ( - - ) : ( - - ), + title: , + button: ( + + {dashboardType === KSPM_POLICY_TEMPLATE + ? i18n.translate( + 'xpack.csp.dashboard.summarySection.counterCard.clustersEvaluatedButtonTitle', + { defaultMessage: 'Enroll more clusters' } + ) + : i18n.translate( + 'xpack.csp.dashboard.summarySection.counterCard.accountsEvaluatedButtonTitle', + { defaultMessage: 'Enroll more accounts' } + )} + + ), }, { id: DASHBOARD_COUNTER_CARDS.RESOURCES_EVALUATED, @@ -101,32 +117,27 @@ export const SummarySection = ({ { defaultMessage: 'Resources Evaluated' } ), title: , - onClick: () => { - navToFindingsByResource(getPolicyTemplateQuery(dashboardType)); - }, - }, - { - id: DASHBOARD_COUNTER_CARDS.FAILING_FINDINGS, - description: i18n.translate( - 'xpack.csp.dashboard.summarySection.counterCard.failingFindingsDescription', - { defaultMessage: 'Failing Findings' } + button: ( + { + navToFindingsByResource(getPolicyTemplateQuery(dashboardType)); + }} + > + {i18n.translate( + 'xpack.csp.dashboard.summarySection.counterCard.resourcesEvaluatedButtonTitle', + { defaultMessage: 'View all resources' } + )} + ), - title: , - titleColor: complianceData.stats.totalFailed > 0 ? statusColors.failed : 'text', - onClick: () => { - navToFindings({ - 'result.evaluation': RULE_FAILED, - ...getPolicyTemplateQuery(dashboardType), - }); - }, }, ], [ complianceData.clusters, complianceData.stats.resourcesEvaluated, - complianceData.stats.totalFailed, + cspmIntegrationLink, dashboardType, - navToFindings, + kspmIntegrationLink, navToFindingsByResource, ] ); diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 576070fc1c865..026c70c9bebdd 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -11377,7 +11377,6 @@ "xpack.csp.dashboard.summarySection.complianceByCisSectionPanelTitle": "Conformité par section CIS", "xpack.csp.dashboard.summarySection.counterCard.accountsEvaluatedDescription": "Comptes évalués", "xpack.csp.dashboard.summarySection.counterCard.clustersEvaluatedDescription": "Clusters évalués", - "xpack.csp.dashboard.summarySection.counterCard.failingFindingsDescription": "Résultats en échec", "xpack.csp.dashboard.summarySection.counterCard.resourcesEvaluatedDescription": "Ressources évaluées", "xpack.csp.dashboardTabs.cloudTab.tabTitle": "Cloud", "xpack.csp.dashboardTabs.kubernetesTab.tabTitle": "Kubernetes", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index a6a8b9c0e6dbb..583cfd2d9d474 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -11392,7 +11392,6 @@ "xpack.csp.dashboard.summarySection.complianceByCisSectionPanelTitle": "CISセクション別のコンプライアンス", "xpack.csp.dashboard.summarySection.counterCard.accountsEvaluatedDescription": "評価されたアカウント", "xpack.csp.dashboard.summarySection.counterCard.clustersEvaluatedDescription": "評価されたクラスター", - "xpack.csp.dashboard.summarySection.counterCard.failingFindingsDescription": "失敗した調査結果", "xpack.csp.dashboard.summarySection.counterCard.resourcesEvaluatedDescription": "評価されたリソース", "xpack.csp.dashboardTabs.cloudTab.tabTitle": "クラウド", "xpack.csp.dashboardTabs.kubernetesTab.tabTitle": "Kubernetes", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 16823393c6d2b..2e16e687539ff 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -11392,7 +11392,6 @@ "xpack.csp.dashboard.summarySection.complianceByCisSectionPanelTitle": "合规性(按 CIS 部分)", "xpack.csp.dashboard.summarySection.counterCard.accountsEvaluatedDescription": "已评估帐户", "xpack.csp.dashboard.summarySection.counterCard.clustersEvaluatedDescription": "集群已评估", - "xpack.csp.dashboard.summarySection.counterCard.failingFindingsDescription": "失败的结果", "xpack.csp.dashboard.summarySection.counterCard.resourcesEvaluatedDescription": "资源已评估", "xpack.csp.dashboardTabs.cloudTab.tabTitle": "云", "xpack.csp.dashboardTabs.kubernetesTab.tabTitle": "Kubernetes", diff --git a/x-pack/test/functional/page_objects/infra_hosts_view.ts b/x-pack/test/functional/page_objects/infra_hosts_view.ts index 1cd0cf15996ec..4dda165ea96a7 100644 --- a/x-pack/test/functional/page_objects/infra_hosts_view.ts +++ b/x-pack/test/functional/page_objects/infra_hosts_view.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { AlertStatus, ALERT_STATUS_ACTIVE, ALERT_STATUS_RECOVERED } from '@kbn/rule-data-utils'; +import { AlertStatus } from '@kbn/rule-data-utils'; import { WebElementWrapper } from '../../../../test/functional/services/lib/web_element_wrapper'; import { FtrProviderContext } from '../ftr_provider_context'; @@ -201,9 +201,10 @@ export function InfraHostsViewProvider({ getService }: FtrProviderContext) { }, setAlertStatusFilter(alertStatus?: AlertStatus) { - const buttons = { - [ALERT_STATUS_ACTIVE]: 'hostsView-alert-status-filter-active-button', - [ALERT_STATUS_RECOVERED]: 'hostsView-alert-status-filter-recovered-button', + const buttons: Record = { + active: 'hostsView-alert-status-filter-active-button', + recovered: 'hostsView-alert-status-filter-recovered-button', + untracked: 'hostsView-alert-status-filter-untracked-button', all: 'hostsView-alert-status-filter-show-all-button', }; diff --git a/x-pack/test/functional/page_objects/security_page.ts b/x-pack/test/functional/page_objects/security_page.ts index 081bd9f297503..5b0a9a679840a 100644 --- a/x-pack/test/functional/page_objects/security_page.ts +++ b/x-pack/test/functional/page_objects/security_page.ts @@ -168,7 +168,7 @@ export class SecurityPageObject extends FtrService { ); } - private async isLoginFormVisible() { + public async isLoginFormVisible() { return await this.testSubjects.exists('loginForm'); } @@ -323,7 +323,14 @@ export class SecurityPageObject extends FtrService { if (alert?.accept) { await alert.accept(); } - return !(await this.browser.getCurrentUrl()).includes('/logout'); + + if (this.config.get('serverless')) { + // Logout might trigger multiple redirects, but in the end we expect the Cloud login page + this.log.debug('Wait 5 sec for Cloud login page to be displayed'); + return await this.find.existsByDisplayedByCssSelector('.login-form-password', 5000); + } else { + return !(await this.browser.getCurrentUrl()).includes('/logout'); + } }); } } diff --git a/x-pack/test_serverless/functional/page_objects/svl_common_page.ts b/x-pack/test_serverless/functional/page_objects/svl_common_page.ts index cf96bfd274eb9..7762bf92d046a 100644 --- a/x-pack/test_serverless/functional/page_objects/svl_common_page.ts +++ b/x-pack/test_serverless/functional/page_objects/svl_common_page.ts @@ -10,16 +10,90 @@ import { FtrProviderContext } from '../ftr_provider_context'; export function SvlCommonPageProvider({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const config = getService('config'); - const pageObjects = getPageObjects(['security']); + const pageObjects = getPageObjects(['security', 'common']); + const retry = getService('retry'); + const deployment = getService('deployment'); + const log = getService('log'); + const browser = getService('browser'); + + const delay = (ms: number) => + new Promise((resolve) => { + setTimeout(resolve, ms); + }); return { + async navigateToLoginForm() { + const url = deployment.getHostPort() + '/login'; + await browser.get(url); + // ensure welcome screen won't be shown. This is relevant for environments which don't allow + // to use the yml setting, e.g. cloud + await browser.setLocalStorageItem('home:welcome:show', 'false'); + + log.debug('Waiting for Login Form to appear.'); + await retry.waitForWithTimeout('login form', 10_000, async () => { + return await pageObjects.security.isLoginFormVisible(); + }); + }, + async login() { await pageObjects.security.forceLogout({ waitForLoginPage: false }); - return await pageObjects.security.login(config.get('servers.kibana.username')); + + // adding sleep to settle down logout + await pageObjects.common.sleep(2500); + + await retry.waitForWithTimeout( + 'Waiting for successful authentication', + 90_000, + async () => { + if (!(await testSubjects.exists('loginUsername', { timeout: 1000 }))) { + await this.navigateToLoginForm(); + + await testSubjects.setValue('loginUsername', config.get('servers.kibana.username')); + await testSubjects.setValue('loginPassword', config.get('servers.kibana.password')); + await testSubjects.click('loginSubmit'); + } + + if (await testSubjects.exists('userMenuButton', { timeout: 10_000 })) { + log.debug('userMenuButton is found, logged in passed'); + return true; + } else { + throw new Error(`Failed to login to Kibana via UI`); + } + }, + async () => { + // Sometimes authentication fails and user is redirected to Cloud login page + // [plugins.security.authentication] Authentication attempt failed: UNEXPECTED_SESSION_ERROR + const currentUrl = await browser.getCurrentUrl(); + if (currentUrl.startsWith('https://cloud.elastic.co')) { + log.debug( + 'Probably authentication attempt failed, we are at Cloud login page. Retrying from scratch' + ); + } else { + const authError = await testSubjects.exists('promptPage', { timeout: 2500 }); + if (authError) { + log.debug('Probably SAML callback page, doing logout again'); + await pageObjects.security.forceLogout({ waitForLoginPage: false }); + } else { + const isOnLoginPage = await testSubjects.exists('loginUsername', { timeout: 1000 }); + if (isOnLoginPage) { + log.debug( + 'Probably ES user profile activation failed, waiting 2 seconds and pressing Login button again' + ); + await delay(2000); + await testSubjects.click('loginSubmit'); + } else { + log.debug('New behaviour, trying to navigate and login again'); + } + } + } + } + ); + log.debug('Logged in successfully'); }, async forceLogout() { await pageObjects.security.forceLogout({ waitForLoginPage: false }); + log.debug('Logged out successfully'); }, async assertProjectHeaderExists() {