diff --git a/.ci/Jenkinsfile_baseline_capture b/.ci/Jenkinsfile_baseline_capture index 7c7cc8d98c306..3f90a6bc05af0 100644 --- a/.ci/Jenkinsfile_baseline_capture +++ b/.ci/Jenkinsfile_baseline_capture @@ -8,12 +8,12 @@ kibanaPipeline(timeoutMinutes: 120) { catchError { parallel([ 'oss-visualRegression': { - workers.ci(name: 'oss-visualRegression', size: 's', ramDisk: false) { + workers.ci(name: 'oss-visualRegression', size: 's-highmem', ramDisk: true) { kibanaPipeline.functionalTestProcess('oss-visualRegression', './test/scripts/jenkins_visual_regression.sh')(1) } }, 'xpack-visualRegression': { - workers.ci(name: 'xpack-visualRegression', size: 's', ramDisk: false) { + workers.ci(name: 'xpack-visualRegression', size: 's-highmem', ramDisk: true) { kibanaPipeline.functionalTestProcess('xpack-visualRegression', './test/scripts/jenkins_xpack_visual_regression.sh')(1) } }, diff --git a/src/plugins/visualizations/public/expressions/visualization_function.ts b/src/plugins/visualizations/public/expressions/visualization_function.ts index 222479158934b..68a153f4272a3 100644 --- a/src/plugins/visualizations/public/expressions/visualization_function.ts +++ b/src/plugins/visualizations/public/expressions/visualization_function.ts @@ -21,7 +21,7 @@ import { get } from 'lodash'; import { i18n } from '@kbn/i18n'; import { VisResponseValue, PersistedState } from '../../../../plugins/visualizations/public'; import { ExpressionFunctionDefinition, Render } from '../../../../plugins/expressions/public'; -import { getTypes, getIndexPatterns, getFilterManager } from '../services'; +import { getTypes, getIndexPatterns, getFilterManager, getSearch } from '../services'; interface Arguments { index?: string | null; @@ -31,6 +31,7 @@ interface Arguments { schemas?: string; visConfig?: string; uiState?: string; + aggConfigs?: string; } export type ExpressionFunctionVisualization = ExpressionFunctionDefinition< @@ -84,6 +85,11 @@ export const visualization = (): ExpressionFunctionVisualization => ({ default: '"{}"', help: 'User interface state', }, + aggConfigs: { + types: ['string'], + default: '"{}"', + help: 'Aggregation configurations', + }, }, async fn(input, args, { inspectorAdapters }) { const visConfigParams = args.visConfig ? JSON.parse(args.visConfig) : {}; @@ -94,6 +100,11 @@ export const visualization = (): ExpressionFunctionVisualization => ({ const uiStateParams = args.uiState ? JSON.parse(args.uiState) : {}; const uiState = new PersistedState(uiStateParams); + const aggConfigsState = args.aggConfigs ? JSON.parse(args.aggConfigs) : []; + const aggs = indexPattern + ? getSearch().aggs.createAggConfigs(indexPattern, aggConfigsState) + : undefined; + if (typeof visType.requestHandler === 'function') { input = await visType.requestHandler({ partialRows: args.partialRows, @@ -107,6 +118,7 @@ export const visualization = (): ExpressionFunctionVisualization => ({ inspectorAdapters, queryFilter: getFilterManager(), forceFetch: true, + aggs, }); } diff --git a/src/plugins/visualizations/public/legacy/build_pipeline.ts b/src/plugins/visualizations/public/legacy/build_pipeline.ts index 2ef07bf18c91c..e74a83d91fabf 100644 --- a/src/plugins/visualizations/public/legacy/build_pipeline.ts +++ b/src/plugins/visualizations/public/legacy/build_pipeline.ts @@ -535,7 +535,10 @@ export const buildPipeline = async ( metricsAtAllLevels=${vis.isHierarchical()} partialRows=${vis.type.requiresPartialRows || vis.params.showPartialRows || false} `; if (indexPattern) { - pipeline += `${prepareString('index', indexPattern.id)}`; + pipeline += `${prepareString('index', indexPattern.id)} `; + if (vis.data.aggs) { + pipeline += `${prepareJson('aggConfigs', vis.data.aggs!.aggs)}`; + } } } diff --git a/x-pack/plugins/ingest_manager/common/services/agent_status.ts b/x-pack/plugins/ingest_manager/common/services/agent_status.ts index 6489c30308771..536003b0f743d 100644 --- a/x-pack/plugins/ingest_manager/common/services/agent_status.ts +++ b/x-pack/plugins/ingest_manager/common/services/agent_status.ts @@ -4,11 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - AGENT_POLLING_THRESHOLD_MS, - AGENT_TYPE_PERMANENT, - AGENT_SAVED_OBJECT_TYPE, -} from '../constants'; +import { AGENT_POLLING_THRESHOLD_MS, AGENT_SAVED_OBJECT_TYPE } from '../constants'; import { Agent, AgentStatus } from '../types'; export function getAgentStatus(agent: Agent, now: number = Date.now()): AgentStatus { @@ -41,8 +37,16 @@ export function getAgentStatus(agent: Agent, now: number = Date.now()): AgentSta return 'online'; } +export function buildKueryForEnrollingAgents() { + return `not ${AGENT_SAVED_OBJECT_TYPE}.last_checkin:*`; +} + +export function buildKueryForUnenrollingAgents() { + return `${AGENT_SAVED_OBJECT_TYPE}.unenrollment_started_at:*`; +} + export function buildKueryForOnlineAgents() { - return `not (${buildKueryForOfflineAgents()}) AND not (${buildKueryForErrorAgents()})`; + return `not (${buildKueryForOfflineAgents()}) AND not (${buildKueryForErrorAgents()}) AND not (${buildKueryForEnrollingAgents()}) AND not (${buildKueryForUnenrollingAgents()})`; } export function buildKueryForErrorAgents() { @@ -50,7 +54,7 @@ export function buildKueryForErrorAgents() { } export function buildKueryForOfflineAgents() { - return `((${AGENT_SAVED_OBJECT_TYPE}.type:${AGENT_TYPE_PERMANENT} AND ${AGENT_SAVED_OBJECT_TYPE}.last_checkin < now-${ + return `${AGENT_SAVED_OBJECT_TYPE}.last_checkin < now-${ (4 * AGENT_POLLING_THRESHOLD_MS) / 1000 - }s) AND not ( ${buildKueryForErrorAgents()} ))`; + }s AND not (${buildKueryForErrorAgents()})`; } diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts index ed7d73ab0b719..7ec5a8d68311f 100644 --- a/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts @@ -173,5 +173,6 @@ export interface GetAgentStatusResponse { online: number; error: number; offline: number; + other: number; }; } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/donut_chart.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/donut_chart.tsx index bfa9c80f12851..99a4f27b428fe 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/donut_chart.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/donut_chart.tsx @@ -31,7 +31,7 @@ export const DonutChart = ({ height, width, data }: DonutChartProps) => { .ordinal() // @ts-ignore .domain(data) - .range(['#017D73', '#98A2B3', '#BD271E']); + .range(['#017D73', '#98A2B3', '#BD271E', '#F5A700']); const pieGenerator = d3.layout .pie() .value(({ value }: any) => value) diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/list_layout.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/list_layout.tsx index 46190033d4d6b..16acda9dc4afd 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/list_layout.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/list_layout.tsx @@ -66,6 +66,7 @@ export const ListLayout: React.FunctionComponent<{}> = ({ children }) => { online: agentStatus?.online || 0, offline: agentStatus?.offline || 0, error: agentStatus?.error || 0, + other: agentStatus?.other || 0, }} /> diff --git a/x-pack/plugins/ingest_manager/server/services/agents/status.ts b/x-pack/plugins/ingest_manager/server/services/agents/status.ts index 016a2344cf532..86336714a511e 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/status.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/status.ts @@ -25,9 +25,10 @@ export async function getAgentStatusForConfig( soClient: SavedObjectsClientContract, configId?: string ) { - const [all, error, offline] = await Promise.all( + const [all, online, error, offline] = await Promise.all( [ undefined, + AgentStatusKueryHelper.buildKueryForOnlineAgents(), AgentStatusKueryHelper.buildKueryForErrorAgents(), AgentStatusKueryHelper.buildKueryForOfflineAgents(), ].map((kuery) => @@ -47,9 +48,10 @@ export async function getAgentStatusForConfig( return { events: await getEventsCount(soClient, configId), total: all.total, - online: all.total - error.total - offline.total, + online: online.total, error: error.total, offline: offline.total, + other: all.total - online.total - error.total - offline.total, }; } diff --git a/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.mock.ts b/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.mock.ts index f7a6af98c8f0e..9e1a88ceb28bd 100644 --- a/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.mock.ts +++ b/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.mock.ts @@ -16,7 +16,7 @@ export const getExceptionListItemSchemaMock = (): ExceptionListItemSchema => ({ entries: ENTRIES, id: '1', item_id: 'endpoint_list_item', - list_id: 'endpoint_list', + list_id: 'endpoint_list_id', meta: {}, name: 'Sample Endpoint Exception List', namespace_type: 'single', diff --git a/x-pack/plugins/lists/common/schemas/response/exception_list_schema.mock.ts b/x-pack/plugins/lists/common/schemas/response/exception_list_schema.mock.ts index 017b959a2baf3..906dcf6560ee5 100644 --- a/x-pack/plugins/lists/common/schemas/response/exception_list_schema.mock.ts +++ b/x-pack/plugins/lists/common/schemas/response/exception_list_schema.mock.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ENDPOINT_LIST_ID } from '../..'; + import { ExceptionListSchema } from './exception_list_schema'; export const getExceptionListSchemaMock = (): ExceptionListSchema => ({ @@ -12,10 +14,10 @@ export const getExceptionListSchemaMock = (): ExceptionListSchema => ({ created_by: 'user_name', description: 'This is a sample endpoint type exception', id: '1', - list_id: 'endpoint_list', + list_id: ENDPOINT_LIST_ID, meta: {}, name: 'Sample Endpoint Exception List', - namespace_type: 'single', + namespace_type: 'agnostic', tags: ['user added string for a tag', 'malware'], tie_breaker_id: '77fd1909-6786-428a-a671-30229a719c1f', type: 'endpoint', diff --git a/x-pack/plugins/lists/public/exceptions/api.test.ts b/x-pack/plugins/lists/public/exceptions/api.test.ts index 1414d828fa6d4..455670098307f 100644 --- a/x-pack/plugins/lists/public/exceptions/api.test.ts +++ b/x-pack/plugins/lists/public/exceptions/api.test.ts @@ -508,7 +508,7 @@ describe('Exceptions Lists API', () => { test('it returns expected format when call succeeds', async () => { const exceptionResponse = await fetchExceptionListItemsByListId({ http: mockKibanaHttpService(), - listId: 'endpoint_list', + listId: 'endpoint_list_id', namespaceType: 'single', pagination: { page: 1, diff --git a/x-pack/plugins/maps/public/classes/sources/kibana_tilemap_source/create_source_editor.js b/x-pack/plugins/maps/public/classes/sources/kibana_tilemap_source/create_source_editor.js index 1cbf4c1a87de3..ee557f6244f49 100644 --- a/x-pack/plugins/maps/public/classes/sources/kibana_tilemap_source/create_source_editor.js +++ b/x-pack/plugins/maps/public/classes/sources/kibana_tilemap_source/create_source_editor.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useEffect } from 'react'; import PropTypes from 'prop-types'; import { EuiFieldText, EuiFormRow, EuiPanel } from '@elastic/eui'; @@ -13,10 +13,12 @@ import { i18n } from '@kbn/i18n'; export function CreateSourceEditor({ onSourceConfigChange }) { const tilemap = getKibanaTileMap(); - - if (tilemap.url) { - onSourceConfigChange(); - } + useEffect(() => { + if (tilemap.url) { + onSourceConfigChange(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); return ( @@ -33,7 +35,7 @@ export function CreateSourceEditor({ onSourceConfigChange }) { }) } > - + ); diff --git a/x-pack/plugins/maps/public/routing/routes/maps_app/load_map_and_render.js b/x-pack/plugins/maps/public/routing/routes/maps_app/load_map_and_render.js index c87f6eb330531..3eea5b00d324e 100644 --- a/x-pack/plugins/maps/public/routing/routes/maps_app/load_map_and_render.js +++ b/x-pack/plugins/maps/public/routing/routes/maps_app/load_map_and_render.js @@ -7,7 +7,7 @@ import React from 'react'; import { MapsAppView } from '.'; import { getMapsSavedObjectLoader } from '../../bootstrap/services/gis_map_saved_object_loader'; -import { getToasts } from '../../../kibana_services'; +import { getCoreChrome, getToasts } from '../../../kibana_services'; import { i18n } from '@kbn/i18n'; import { Redirect } from 'react-router-dom'; @@ -30,6 +30,10 @@ export const LoadMapAndRender = class extends React.Component { try { const savedMap = await getMapsSavedObjectLoader().get(this.props.savedMapId); if (this._isMounted) { + getCoreChrome().docTitle.change(savedMap.title); + if (this.props.savedMapId) { + getCoreChrome().recentlyAccessed.add(savedMap.getFullPath(), savedMap.title, savedMap.id); + } this.setState({ savedMap }); } } catch (err) { diff --git a/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.js b/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.js index 29fbb5f46e29b..aa7f24155ab43 100644 --- a/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.js +++ b/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.js @@ -56,11 +56,6 @@ export class MapsAppView extends React.Component { } componentDidMount() { - const { savedMap } = this.props; - - getCoreChrome().docTitle.change(savedMap.title); - getCoreChrome().recentlyAccessed.add(savedMap.getFullPath(), savedMap.title, savedMap.id); - // Init sync utils // eslint-disable-next-line react-hooks/rules-of-hooks this._globalSyncUnsubscribe = useGlobalStateSyncing(); diff --git a/x-pack/plugins/security_solution/common/detection_engine/utils.ts b/x-pack/plugins/security_solution/common/detection_engine/utils.ts index fa1812235f897..153130fc16d60 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/utils.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/utils.ts @@ -5,6 +5,7 @@ */ import { EntriesArray } from '../shared_imports'; +import { RuleType } from './types'; export const hasLargeValueList = (entries: EntriesArray): boolean => { const found = entries.filter(({ type }) => type === 'list'); @@ -15,3 +16,5 @@ export const hasNestedEntry = (entries: EntriesArray): boolean => { const found = entries.filter(({ type }) => type === 'nested'); return found.length > 0; }; + +export const isThresholdRule = (ruleType: RuleType) => ruleType === 'threshold'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx index d5eeef0f1e768..79383676266f5 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx @@ -29,8 +29,8 @@ import { } from '../../../../../public/lists_plugin_deps'; import * as i18n from './translations'; import { TimelineNonEcsData, Ecs } from '../../../../graphql/types'; +import { useAppToasts } from '../../../hooks/use_app_toasts'; import { useKibana } from '../../../lib/kibana'; -import { errorToToaster, displaySuccessToast, useStateToaster } from '../../toasters'; import { ExceptionBuilder } from '../builder'; import { Loader } from '../../loader'; import { useAddOrUpdateException } from '../use_add_exception'; @@ -115,7 +115,7 @@ export const AddExceptionModal = memo(function AddExceptionModal({ Array >([]); const [fetchOrCreateListError, setFetchOrCreateListError] = useState(false); - const [, dispatchToaster] = useStateToaster(); + const { addError, addSuccess } = useAppToasts(); const { loading: isSignalIndexLoading, signalIndexName } = useSignalIndex(); const [{ isLoading: indexPatternLoading, indexPatterns }] = useFetchIndexPatterns( @@ -124,15 +124,15 @@ export const AddExceptionModal = memo(function AddExceptionModal({ const onError = useCallback( (error: Error) => { - errorToToaster({ title: i18n.ADD_EXCEPTION_ERROR, error, dispatchToaster }); + addError(error, { title: i18n.ADD_EXCEPTION_ERROR }); onCancel(); }, - [dispatchToaster, onCancel] + [addError, onCancel] ); const onSuccess = useCallback(() => { - displaySuccessToast(i18n.ADD_EXCEPTION_SUCCESS, dispatchToaster); + addSuccess(i18n.ADD_EXCEPTION_SUCCESS); onConfirm(shouldCloseAlert); - }, [dispatchToaster, onConfirm, shouldCloseAlert]); + }, [addSuccess, onConfirm, shouldCloseAlert]); const [{ isLoading: addExceptionIsLoading }, addOrUpdateExceptionItems] = useAddOrUpdateException( { diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx index 73933d483e2cb..dbc70dfe21dd0 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx @@ -30,7 +30,7 @@ import { } from '../../../../../public/lists_plugin_deps'; import * as i18n from './translations'; import { useKibana } from '../../../lib/kibana'; -import { errorToToaster, displaySuccessToast, useStateToaster } from '../../toasters'; +import { useAppToasts } from '../../../hooks/use_app_toasts'; import { ExceptionBuilder } from '../builder'; import { useAddOrUpdateException } from '../use_add_exception'; import { AddExceptionComments } from '../add_exception_comments'; @@ -93,7 +93,7 @@ export const EditExceptionModal = memo(function EditExceptionModal({ const [exceptionItemsToAdd, setExceptionItemsToAdd] = useState< Array >([]); - const [, dispatchToaster] = useStateToaster(); + const { addError, addSuccess } = useAppToasts(); const { loading: isSignalIndexLoading, signalIndexName } = useSignalIndex(); const [{ isLoading: indexPatternLoading, indexPatterns }] = useFetchIndexPatterns( @@ -102,15 +102,15 @@ export const EditExceptionModal = memo(function EditExceptionModal({ const onError = useCallback( (error) => { - errorToToaster({ title: i18n.EDIT_EXCEPTION_ERROR, error, dispatchToaster }); + addError(error, { title: i18n.EDIT_EXCEPTION_ERROR }); onCancel(); }, - [dispatchToaster, onCancel] + [addError, onCancel] ); const onSuccess = useCallback(() => { - displaySuccessToast(i18n.EDIT_EXCEPTION_SUCCESS, dispatchToaster); + addSuccess(i18n.EDIT_EXCEPTION_SUCCESS); onConfirm(); - }, [dispatchToaster, onConfirm]); + }, [addSuccess, onConfirm]); const [{ isLoading: addExceptionIsLoading }, addOrUpdateExceptionItems] = useAddOrUpdateException( { diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.test.tsx index 0e2908fc34232..90752f9450e4c 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.test.tsx @@ -13,6 +13,8 @@ import { ExceptionItem } from './'; import { getExceptionListItemSchemaMock } from '../../../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; import { getCommentsArrayMock } from '../../../../../../../lists/common/schemas/types/comments.mock'; +jest.mock('../../../../lib/kibana'); + describe('ExceptionItem', () => { it('it renders ExceptionDetails and ExceptionEntries', () => { const exceptionItem = getExceptionListItemSchemaMock(); diff --git a/x-pack/plugins/security_solution/public/common/components/toasters/utils.ts b/x-pack/plugins/security_solution/public/common/components/toasters/utils.ts index e7cc389d4c06b..47c5588a12830 100644 --- a/x-pack/plugins/security_solution/public/common/components/toasters/utils.ts +++ b/x-pack/plugins/security_solution/public/common/components/toasters/utils.ts @@ -9,7 +9,7 @@ import { isError } from 'lodash/fp'; import { AppToast, ActionToaster } from './'; import { isToasterError } from './errors'; -import { isApiError } from '../../utils/api'; +import { isAppError } from '../../utils/api'; /** * Displays an error toast for the provided title and message @@ -114,7 +114,7 @@ export const errorToToaster = ({ iconType, errors: error.messages, }; - } else if (isApiError(error)) { + } else if (isAppError(error)) { toast = { id, title, diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.test.ts b/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.test.ts new file mode 100644 index 0000000000000..e0e629793952a --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.test.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { renderHook } from '@testing-library/react-hooks'; + +import { useToasts } from '../lib/kibana'; +import { useAppToasts } from './use_app_toasts'; + +jest.mock('../lib/kibana'); + +describe('useDeleteList', () => { + let addErrorMock: jest.Mock; + let addSuccessMock: jest.Mock; + + beforeEach(() => { + addErrorMock = jest.fn(); + addSuccessMock = jest.fn(); + (useToasts as jest.Mock).mockImplementation(() => ({ + addError: addErrorMock, + addSuccess: addSuccessMock, + })); + }); + + it('works normally with a regular error', async () => { + const error = new Error('regular error'); + const { result } = renderHook(() => useAppToasts()); + + result.current.addError(error, { title: 'title' }); + + expect(addErrorMock).toHaveBeenCalledWith(error, { title: 'title' }); + }); + + it("uses a AppError's body.message as the toastMessage", async () => { + const kibanaApiError = { + message: 'Not Found', + body: { status_code: 404, message: 'Detailed Message' }, + }; + + const { result } = renderHook(() => useAppToasts()); + + result.current.addError(kibanaApiError, { title: 'title' }); + + expect(addErrorMock).toHaveBeenCalledWith(kibanaApiError, { + title: 'title', + toastMessage: 'Detailed Message', + }); + }); + + it('converts an unknown error to an Error', () => { + const unknownError = undefined; + + const { result } = renderHook(() => useAppToasts()); + + result.current.addError(unknownError, { title: 'title' }); + + expect(addErrorMock).toHaveBeenCalledWith(Error(`${undefined}`), { + title: 'title', + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts b/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts new file mode 100644 index 0000000000000..bc59d87100058 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useCallback, useRef } from 'react'; + +import { ErrorToastOptions, ToastsStart, Toast } from '../../../../../../src/core/public'; +import { useToasts } from '../lib/kibana'; +import { isAppError, AppError } from '../utils/api'; + +export type UseAppToasts = Pick & { + api: ToastsStart; + addError: (error: unknown, options: ErrorToastOptions) => Toast; +}; + +export const useAppToasts = (): UseAppToasts => { + const toasts = useToasts(); + const addError = useRef(toasts.addError.bind(toasts)).current; + const addSuccess = useRef(toasts.addSuccess.bind(toasts)).current; + + const addAppError = useCallback( + (error: AppError, options: ErrorToastOptions) => + addError(error, { + ...options, + toastMessage: error.body.message, + }), + [addError] + ); + + const _addError = useCallback( + (error: unknown, options: ErrorToastOptions) => { + if (isAppError(error)) { + return addAppError(error, options); + } else { + if (error instanceof Error) { + return addError(error, options); + } else { + return addError(new Error(String(error)), options); + } + } + }, + [addAppError, addError] + ); + + return { api: toasts, addError: _addError, addSuccess }; +}; diff --git a/x-pack/plugins/security_solution/public/common/lib/clipboard/clipboard.tsx b/x-pack/plugins/security_solution/public/common/lib/clipboard/clipboard.tsx index fdb6ed130a525..75b7308ab61f1 100644 --- a/x-pack/plugins/security_solution/public/common/lib/clipboard/clipboard.tsx +++ b/x-pack/plugins/security_solution/public/common/lib/clipboard/clipboard.tsx @@ -4,13 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiGlobalToastListToast as Toast, EuiButtonIcon } from '@elastic/eui'; +import { EuiButtonIcon } from '@elastic/eui'; import copy from 'copy-to-clipboard'; import React from 'react'; -import uuid from 'uuid'; import * as i18n from './translations'; -import { useStateToaster } from '../../components/toasters'; +import { useAppToasts } from '../../hooks/use_app_toasts'; export type OnCopy = ({ content, @@ -20,17 +19,6 @@ export type OnCopy = ({ isSuccess: boolean; }) => void; -interface GetSuccessToastParams { - titleSummary?: string; -} - -const getSuccessToast = ({ titleSummary }: GetSuccessToastParams): Toast => ({ - id: `copy-success-${uuid.v4()}`, - color: 'success', - iconType: 'copyClipboard', - title: `${i18n.COPIED} ${titleSummary} ${i18n.TO_THE_CLIPBOARD}`, -}); - interface Props { children?: JSX.Element; content: string | number; @@ -40,7 +28,7 @@ interface Props { } export const Clipboard = ({ children, content, onCopy, titleSummary, toastLifeTimeMs }: Props) => { - const dispatchToaster = useStateToaster()[1]; + const { addSuccess } = useAppToasts(); const onClick = (event: React.MouseEvent) => { event.preventDefault(); event.stopPropagation(); @@ -52,10 +40,7 @@ export const Clipboard = ({ children, content, onCopy, titleSummary, toastLifeTi } if (isSuccess) { - dispatchToaster({ - type: 'addToaster', - toast: { toastLifeTimeMs, ...getSuccessToast({ titleSummary }) }, - }); + addSuccess(`${i18n.COPIED} ${titleSummary} ${i18n.TO_THE_CLIPBOARD}`, { toastLifeTimeMs }); } }; 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 c3e1f35f37356..6ada887ece175 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 @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { notificationServiceMock } from '../../../../../../../../src/core/public/mocks'; import { createKibanaContextProviderMock, createUseUiSettingMock, @@ -19,6 +20,7 @@ export const useUiSetting$ = jest.fn(createUseUiSetting$Mock()); export const useTimeZone = jest.fn(); export const useDateFormat = jest.fn(); export const useBasePath = jest.fn(() => '/test/base/path'); +export const useToasts = jest.fn(() => notificationServiceMock.createStartContract().toasts); export const useCurrentUser = jest.fn(); export const withKibana = jest.fn(createWithKibanaMock()); export const KibanaContextProvider = jest.fn(createKibanaContextProviderMock()); diff --git a/x-pack/plugins/security_solution/public/common/utils/api/index.ts b/x-pack/plugins/security_solution/public/common/utils/api/index.ts index ab442d0d09cf9..e8934259fe43e 100644 --- a/x-pack/plugins/security_solution/public/common/utils/api/index.ts +++ b/x-pack/plugins/security_solution/public/common/utils/api/index.ts @@ -6,14 +6,33 @@ import { has } from 'lodash/fp'; -export interface KibanaApiError { +export interface AppError { name: string; message: string; + body: { + message: string; + }; +} + +export interface KibanaError extends AppError { + body: { + message: string; + statusCode: number; + }; +} + +export interface SecurityAppError extends AppError { body: { message: string; status_code: number; }; } -export const isApiError = (error: unknown): error is KibanaApiError => +export const isKibanaError = (error: unknown): error is KibanaError => + has('message', error) && has('body.message', error) && has('body.statusCode', error); + +export const isSecurityAppError = (error: unknown): error is SecurityAppError => has('message', error) && has('body.message', error) && has('body.status_code', error); + +export const isAppError = (error: unknown): error is AppError => + isKibanaError(error) || isSecurityAppError(error); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.tsx index 560c092d12076..3bc84bb7c32ee 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.tsx @@ -80,10 +80,14 @@ const getHistogramOption = (fieldName: string): MatrixHistogramOption => ({ const NO_LEGEND_DATA: LegendItem[] = []; +const DEFAULT_STACK_BY = 'signal.rule.name'; +const getDefaultStackByOption = (): AlertsHistogramOption => + alertsHistogramOptions.find(({ text }) => text === DEFAULT_STACK_BY) ?? alertsHistogramOptions[0]; + export const AlertsHistogramPanel = memo( ({ chartHeight, - defaultStackByOption = alertsHistogramOptions[8], // signal.rule.name + defaultStackByOption = getDefaultStackByOption(), deleteQuery, filters, headerChildren, diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/index.tsx index 6546c1ba59d84..c6ea269e1a355 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/index.tsx @@ -8,14 +8,13 @@ import React, { useCallback, useMemo } from 'react'; import { EuiCard, EuiFlexGrid, EuiFlexItem, EuiFormRow, EuiIcon } from '@elastic/eui'; import { isMlRule } from '../../../../../common/machine_learning/helpers'; +import { isThresholdRule } from '../../../../../common/detection_engine/utils'; import { RuleType } from '../../../../../common/detection_engine/types'; import { FieldHook } from '../../../../shared_imports'; import { useKibana } from '../../../../common/lib/kibana'; import * as i18n from './translations'; import { MlCardDescription } from './ml_card_description'; -const isThresholdRule = (ruleType: RuleType) => ruleType === 'threshold'; - interface SelectRuleTypeProps { describedByIds?: string[]; field: FieldHook; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx index 1fc88da5fd7c7..ec812fa63eadf 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx @@ -4,10 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiAccordion, EuiFlexItem, EuiSpacer, EuiButtonEmpty, EuiFormRow } from '@elastic/eui'; +import { EuiAccordion, EuiFlexItem, EuiSpacer, EuiFormRow } from '@elastic/eui'; import React, { FC, memo, useCallback, useEffect, useState } from 'react'; import styled from 'styled-components'; +import { isMlRule } from '../../../../../common/machine_learning/helpers'; +import { isThresholdRule } from '../../../../../common/detection_engine/utils'; import { RuleStepProps, RuleStep, @@ -58,26 +60,6 @@ const TagContainer = styled.div` TagContainer.displayName = 'TagContainer'; -const AdvancedSettingsAccordion = styled(EuiAccordion)` - .euiAccordion__iconWrapper { - display: none; - } - - .euiAccordion__childWrapper { - transition-duration: 1ms; /* hack to fire Step accordion to set proper content's height */ - } - - &.euiAccordion-isOpen .euiButtonEmpty__content > svg { - transform: rotate(90deg); - } -`; - -const AdvancedSettingsAccordionButton = ( - - {I18n.ADVANCED_SETTINGS} - -); - const StepAboutRuleComponent: FC = ({ addPadding = false, defaultValues, @@ -94,6 +76,10 @@ const StepAboutRuleComponent: FC = ({ const [{ isLoading: indexPatternLoading, indexPatterns }] = useFetchIndexPatterns( defineRuleData?.index ?? [] ); + const canUseExceptions = + defineRuleData?.ruleType && + !isMlRule(defineRuleData.ruleType) && + !isThresholdRule(defineRuleData.ruleType); const { form } = useForm({ defaultValue: initialState, @@ -193,10 +179,10 @@ const StepAboutRuleComponent: FC = ({ /> - = ({ idAria: 'detectionEngineStepAboutRuleAssociatedToEndpointList', 'data-test-subj': 'detectionEngineStepAboutRuleAssociatedToEndpointList', euiFieldProps: { - fullWidth: true, - isDisabled: isLoading, + disabled: isLoading || !canUseExceptions, }, }} /> @@ -287,8 +272,7 @@ const StepAboutRuleComponent: FC = ({ idAria: 'detectionEngineStepAboutRuleBuildingBlock', 'data-test-subj': 'detectionEngineStepAboutRuleBuildingBlock', euiFieldProps: { - fullWidth: true, - isDisabled: isLoading, + disabled: isLoading, }, }} /> @@ -319,7 +303,7 @@ const StepAboutRuleComponent: FC = ({ placeholder: '', }} /> - + {({ severity }) => { const newRiskScore = defaultRiskScoreBySeverity[severity as SeverityValue]; diff --git a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/modal.tsx b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/modal.tsx index 0a935a9cdb1c4..d7d4be6d951b8 100644 --- a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/modal.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/modal.tsx @@ -23,7 +23,8 @@ import { useDeleteList, useCursor, } from '../../../shared_imports'; -import { useToasts, useKibana } from '../../../common/lib/kibana'; +import { useKibana } from '../../../common/lib/kibana'; +import { useAppToasts } from '../../../common/hooks/use_app_toasts'; import { GenericDownloader } from '../../../common/components/generic_downloader'; import * as i18n from './translations'; import { ValueListsTable } from './table'; @@ -45,7 +46,7 @@ export const ValueListsModalComponent: React.FC = ({ const { start: findLists, ...lists } = useFindLists(); const { start: deleteList, result: deleteResult } = useDeleteList(); const [exportListId, setExportListId] = useState(); - const toasts = useToasts(); + const { addError, addSuccess } = useAppToasts(); const fetchLists = useCallback(() => { findLists({ cursor, http, pageIndex: pageIndex + 1, pageSize }); @@ -82,21 +83,21 @@ export const ValueListsModalComponent: React.FC = ({ const handleUploadError = useCallback( (error: Error) => { if (error.name !== 'AbortError') { - toasts.addError(error, { title: i18n.UPLOAD_ERROR }); + addError(error, { title: i18n.UPLOAD_ERROR }); } }, - [toasts] + [addError] ); const handleUploadSuccess = useCallback( (response: ListSchema) => { - toasts.addSuccess({ + addSuccess({ text: i18n.uploadSuccessMessage(response.name), title: i18n.UPLOAD_SUCCESS_TITLE, }); fetchLists(); }, // eslint-disable-next-line react-hooks/exhaustive-deps - [toasts] + [addSuccess] ); useEffect(() => { diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.tsx index 65a2721013b5e..14fd9ffa50843 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.tsx @@ -9,7 +9,7 @@ import { useEffect, useState } from 'react'; import { errorToToaster, useStateToaster } from '../../../../common/components/toasters'; import { createSignalIndex, getSignalIndex } from './api'; import * as i18n from './translations'; -import { isApiError } from '../../../../common/utils/api'; +import { isSecurityAppError } from '../../../../common/utils/api'; type Func = () => void; @@ -59,7 +59,7 @@ export const useSignalIndex = (): ReturnSignalIndex => { signalIndexName: null, createDeSignalIndex: createIndex, }); - if (isApiError(error) && error.body.status_code !== 404) { + if (isSecurityAppError(error) && error.body.status_code !== 404) { errorToToaster({ title: i18n.SIGNAL_GET_NAME_FAILURE, error, dispatchToaster }); } } @@ -81,7 +81,7 @@ export const useSignalIndex = (): ReturnSignalIndex => { } } catch (error) { if (isSubscribed) { - if (isApiError(error) && error.body.status_code === 409) { + if (isSecurityAppError(error) && error.body.status_code === 409) { fetchData(); } else { setSignalIndex({ diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_config.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_config.tsx index e21cbceeaef27..71847e7b7d8cb 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_config.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_config.tsx @@ -19,16 +19,16 @@ export interface UseListsConfigReturn { } export const useListsConfig = (): UseListsConfigReturn => { - const { createIndex, createIndexError, indexExists, loading: indexLoading } = useListsIndex(); + const { createIndex, indexExists, loading: indexLoading, error: indexError } = useListsIndex(); const { canManageIndex, canWriteIndex, loading: privilegesLoading } = useListsPrivileges(); const { lists } = useKibana().services; const enabled = lists != null; const loading = indexLoading || privilegesLoading; const needsIndex = indexExists === false; - const indexCreationFailed = createIndexError != null; + const hasIndexError = indexError != null; const needsIndexConfiguration = - needsIndex && (canManageIndex === false || (canManageIndex === true && indexCreationFailed)); + needsIndex && (canManageIndex === false || (canManageIndex === true && hasIndexError)); const needsConfiguration = !enabled || canWriteIndex === false || needsIndexConfiguration; useEffect(() => { diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_index.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_index.tsx index 75f12bd07d3ae..ee1316eb8a1fd 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_index.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_index.tsx @@ -7,28 +7,24 @@ import { useEffect, useState, useCallback } from 'react'; import { useReadListIndex, useCreateListIndex } from '../../../../shared_imports'; -import { useHttp, useToasts, useKibana } from '../../../../common/lib/kibana'; -import { isApiError } from '../../../../common/utils/api'; +import { useHttp, useKibana } from '../../../../common/lib/kibana'; +import { isSecurityAppError } from '../../../../common/utils/api'; import * as i18n from './translations'; +import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; -export interface UseListsIndexState { +export interface UseListsIndexReturn { + createIndex: () => void; indexExists: boolean | null; -} - -export interface UseListsIndexReturn extends UseListsIndexState { + error: unknown; loading: boolean; - createIndex: () => void; - createIndexError: unknown; - createIndexResult: { acknowledged: boolean } | undefined; } export const useListsIndex = (): UseListsIndexReturn => { - const [state, setState] = useState({ - indexExists: null, - }); + const [indexExists, setIndexExists] = useState(null); + const [error, setError] = useState(null); const { lists } = useKibana().services; const http = useHttp(); - const toasts = useToasts(); + const { addError } = useAppToasts(); const { loading: readLoading, start: readListIndex, ...readListIndexState } = useReadListIndex(); const { loading: createLoading, @@ -51,18 +47,17 @@ export const useListsIndex = (): UseListsIndexReturn => { // initial read list useEffect(() => { - if (!readLoading && state.indexExists === null) { + if (!readLoading && !error && indexExists === null) { readIndex(); } - }, [readIndex, readLoading, state.indexExists]); + }, [error, indexExists, readIndex, readLoading]); // handle read result useEffect(() => { if (readListIndexState.result != null) { - setState({ - indexExists: - readListIndexState.result.list_index && readListIndexState.result.list_item_index, - }); + setIndexExists( + readListIndexState.result.list_index && readListIndexState.result.list_item_index + ); } }, [readListIndexState.result]); @@ -75,34 +70,30 @@ export const useListsIndex = (): UseListsIndexReturn => { // handle read error useEffect(() => { - const error = readListIndexState.error; - if (isApiError(error)) { - setState({ indexExists: false }); - if (error.body.status_code !== 404) { - toasts.addError(error, { - title: i18n.LISTS_INDEX_FETCH_FAILURE, - toastMessage: error.body.message, - }); + const err = readListIndexState.error; + if (err != null) { + if (isSecurityAppError(err) && err.body.status_code === 404) { + setIndexExists(false); + } else { + setError(err); + addError(err, { title: i18n.LISTS_INDEX_FETCH_FAILURE }); } } - }, [readListIndexState.error, toasts]); + }, [addError, readListIndexState.error]); // handle create error useEffect(() => { - const error = createListIndexState.error; - if (isApiError(error)) { - toasts.addError(error, { - title: i18n.LISTS_INDEX_CREATE_FAILURE, - toastMessage: error.body.message, - }); + const err = createListIndexState.error; + if (err != null) { + setError(err); + addError(err, { title: i18n.LISTS_INDEX_CREATE_FAILURE }); } - }, [createListIndexState.error, toasts]); + }, [addError, createListIndexState.error]); return { - loading, createIndex, - createIndexError: createListIndexState.error, - createIndexResult: createListIndexState.result, - ...state, + error, + indexExists, + loading, }; }; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_privileges.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_privileges.tsx index fbbcff33402c3..f99f62b1948e6 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_privileges.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_privileges.tsx @@ -7,8 +7,8 @@ import { useEffect, useState, useCallback } from 'react'; import { useReadListPrivileges } from '../../../../shared_imports'; -import { useHttp, useToasts, useKibana } from '../../../../common/lib/kibana'; -import { isApiError } from '../../../../common/utils/api'; +import { useHttp, useKibana } from '../../../../common/lib/kibana'; +import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; import * as i18n from './translations'; export interface UseListsPrivilegesState { @@ -79,8 +79,8 @@ export const useListsPrivileges = (): UseListsPrivilegesReturn => { }); const { lists } = useKibana().services; const http = useHttp(); - const toasts = useToasts(); - const { loading, start: readListPrivileges, ...privilegesState } = useReadListPrivileges(); + const { addError } = useAppToasts(); + const { loading, start: readListPrivileges, ...readState } = useReadListPrivileges(); const readPrivileges = useCallback(() => { if (lists) { @@ -90,20 +90,20 @@ export const useListsPrivileges = (): UseListsPrivilegesReturn => { // initRead useEffect(() => { - if (!loading && state.isAuthenticated === null) { + if (!loading && !readState.error && state.isAuthenticated === null) { readPrivileges(); } - }, [loading, readPrivileges, state.isAuthenticated]); + }, [loading, readState.error, readPrivileges, state.isAuthenticated]); // handleReadResult useEffect(() => { - if (privilegesState.result != null) { + if (readState.result != null) { try { const { is_authenticated: isAuthenticated, lists: { index: listsPrivileges }, listItems: { index: listItemsPrivileges }, - } = privilegesState.result as ListPrivileges; + } = readState.result as ListPrivileges; setState({ isAuthenticated, @@ -114,19 +114,18 @@ export const useListsPrivileges = (): UseListsPrivilegesReturn => { setState({ isAuthenticated: null, canManageIndex: false, canWriteIndex: false }); } } - }, [privilegesState.result]); + }, [readState.result]); // handleReadError useEffect(() => { - const error = privilegesState.error; - if (isApiError(error)) { - setState({ isAuthenticated: null, canManageIndex: false, canWriteIndex: false }); - toasts.addError(error, { + const error = readState.error; + if (error != null) { + setState({ isAuthenticated: false, canManageIndex: false, canWriteIndex: false }); + addError(error, { title: i18n.LISTS_PRIVILEGES_READ_FAILURE, - toastMessage: error.body.message, }); } - }, [privilegesState.error, toasts]); + }, [addError, readState.error]); return { loading, ...state }; }; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts index 226fa5313e34f..38f7836f678f9 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts @@ -12,6 +12,7 @@ import { NOTIFICATION_THROTTLE_NO_ACTIONS } from '../../../../../../common/const import { transformAlertToRuleAction } from '../../../../../../common/detection_engine/transform_actions'; import { RuleType } from '../../../../../../common/detection_engine/types'; import { isMlRule } from '../../../../../../common/machine_learning/helpers'; +import { ENDPOINT_LIST_ID } from '../../../../../shared_imports'; import { NewRule } from '../../../../containers/detection_engine/rules'; import { @@ -167,7 +168,7 @@ export const formatAboutStepData = (aboutStepData: AboutStepRule): AboutStepRule ...(isAssociatedToEndpointList ? { exceptions_list: [ - { id: 'endpoint_list', namespace_type: 'agnostic', type: 'endpoint' }, + { id: ENDPOINT_LIST_ID, namespace_type: 'agnostic', type: 'endpoint' }, ] as AboutStepRuleJson['exceptions_list'], } : {}), diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index 7eb5c3a535377..484c28b4b428c 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -37,7 +37,7 @@ import { } from '../../../../../common/components/link_to/redirect_to_detection_engine'; import { SiemSearchBar } from '../../../../../common/components/search_bar'; import { WrapperPage } from '../../../../../common/components/wrapper_page'; -import { useRule } from '../../../../containers/detection_engine/rules'; +import { useRule, Rule } from '../../../../containers/detection_engine/rules'; import { useListsConfig } from '../../../../containers/detection_engine/lists/use_lists_config'; import { useWithSource } from '../../../../../common/containers/source'; @@ -90,6 +90,8 @@ import { MIN_EVENTS_VIEWER_BODY_HEIGHT, } from '../../../../../timelines/components/timeline/body/helpers'; import { footerHeight } from '../../../../../timelines/components/timeline/footer'; +import { isMlRule } from '../../../../../../common/machine_learning/helpers'; +import { isThresholdRule } from '../../../../../../common/detection_engine/utils'; enum RuleDetailTabs { alerts = 'alerts', @@ -97,23 +99,26 @@ enum RuleDetailTabs { exceptions = 'exceptions', } -const ruleDetailTabs = [ - { - id: RuleDetailTabs.alerts, - name: detectionI18n.ALERT, - disabled: false, - }, - { - id: RuleDetailTabs.exceptions, - name: i18n.EXCEPTIONS_TAB, - disabled: false, - }, - { - id: RuleDetailTabs.failures, - name: i18n.FAILURE_HISTORY_TAB, - disabled: false, - }, -]; +const getRuleDetailsTabs = (rule: Rule | null) => { + const canUseExceptions = rule && !isMlRule(rule.type) && !isThresholdRule(rule.type); + return [ + { + id: RuleDetailTabs.alerts, + name: detectionI18n.ALERT, + disabled: false, + }, + { + id: RuleDetailTabs.exceptions, + name: i18n.EXCEPTIONS_TAB, + disabled: !canUseExceptions, + }, + { + id: RuleDetailTabs.failures, + name: i18n.FAILURE_HISTORY_TAB, + disabled: false, + }, + ]; +}; export const RuleDetailsPageComponent: FC = ({ filters, @@ -160,6 +165,7 @@ export const RuleDetailsPageComponent: FC = ({ // TODO: Refactor license check + hasMlAdminPermissions to common check const hasMlPermissions = mlCapabilities.isPlatinumOrTrialLicense && hasMlAdminPermissions(mlCapabilities); + const ruleDetailTabs = getRuleDetailsTabs(rule); const title = isLoading === true || rule === null ? : rule.name; const subTitle = useMemo( diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx index 3de508dcbb3be..11b779e71b9b2 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx @@ -13,6 +13,7 @@ import { RuleAlertAction, RuleType } from '../../../../../common/detection_engin import { isMlRule } from '../../../../../common/machine_learning/helpers'; import { transformRuleToAlertAction } from '../../../../../common/detection_engine/transform_actions'; import { Filter } from '../../../../../../../../src/plugins/data/public'; +import { ENDPOINT_LIST_ID } from '../../../../shared_imports'; import { Rule } from '../../../containers/detection_engine/rules'; import { AboutStepRule, @@ -137,7 +138,7 @@ export const getAboutStepsData = (rule: Rule, detailsView: boolean): AboutStepRu return { isNew: false, author, - isAssociatedToEndpointList: exceptionsList?.some(({ id }) => id === 'endpoint_list') ?? false, + isAssociatedToEndpointList: exceptionsList?.some(({ id }) => id === ENDPOINT_LIST_ID) ?? false, isBuildingBlock: buildingBlockType !== undefined, license: license ?? '', ruleNameOverride: ruleNameOverride ?? '', diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer.ts index b3b74c2ca9dae..e7aa2c8893f8e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer.ts @@ -21,6 +21,7 @@ export const initialPolicyDetailsState: () => Immutable = () offline: 0, online: 0, total: 0, + other: 0, }, }); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/index.test.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/index.test.ts index 0a24c9eea71eb..8203aae244f24 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/index.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/index.test.ts @@ -152,6 +152,7 @@ describe('policy list store concerns', () => { offline: 0, online: 0, total: 0, + other: 0, }, }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/reducer.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/reducer.ts index 52bed8d850ef4..53954449ab9c3 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/reducer.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/reducer.ts @@ -31,6 +31,7 @@ export const initialPolicyListState: () => Immutable = () => ({ offline: 0, online: 0, total: 0, + other: 0, }, }); diff --git a/x-pack/plugins/security_solution/public/overview/components/signals_by_category/index.tsx b/x-pack/plugins/security_solution/public/overview/components/signals_by_category/index.tsx index fbfdefa13d738..0ac136044c06d 100644 --- a/x-pack/plugins/security_solution/public/overview/components/signals_by_category/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/signals_by_category/index.tsx @@ -17,7 +17,6 @@ import { UpdateDateRange } from '../../../common/components/charts/common'; import { GlobalTimeArgs } from '../../../common/containers/use_global_time'; const DEFAULT_QUERY: Query = { query: '', language: 'kuery' }; -const DEFAULT_STACK_BY = 'signal.rule.threat.tactic.name'; const NO_FILTERS: Filter[] = []; interface Props extends Pick { @@ -62,13 +61,9 @@ const SignalsByCategoryComponent: React.FC = ({ [setAbsoluteRangeDatePicker] ); - const defaultStackByOption = - alertsHistogramOptions.find((o) => o.text === DEFAULT_STACK_BY) ?? alertsHistogramOptions[0]; - return ( { /> ); expect(wrapper.find('[data-test-subj="webhookViewHeadersSwitch"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="webhookHeaderText"]').length > 0).toBeTruthy(); wrapper.find('[data-test-subj="webhookViewHeadersSwitch"]').first().simulate('click'); expect(wrapper.find('[data-test-subj="webhookMethodSelect"]').length > 0).toBeTruthy(); expect(wrapper.find('[data-test-subj="webhookUrlText"]').length > 0).toBeTruthy(); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.tsx index 57c88607c0884..2321d5b4b5479 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.tsx @@ -31,14 +31,16 @@ const HTTP_VERBS = ['post', 'put']; const WebhookActionConnectorFields: React.FunctionComponent> = ({ action, editActionConfig, editActionSecrets, errors }) => { + const { user, password } = action.secrets; + const { method, url, headers } = action.config; + const [httpHeaderKey, setHttpHeaderKey] = useState(''); const [httpHeaderValue, setHttpHeaderValue] = useState(''); const [hasHeaders, setHasHeaders] = useState(false); - const { user, password } = action.secrets; - const { method, url, headers } = action.config; - - editActionConfig('method', 'post'); // set method to POST by default + if (!method) { + editActionConfig('method', 'post'); // set method to POST by default + } const headerErrors = { keyHeader: new Array(), @@ -80,7 +82,7 @@ const WebhookActionConnectorFields: React.FunctionComponent
- {hasHeaders && Object.keys(headers || {}).length > 0 ? ( - + {Object.keys(headers || {}).length > 0 ? ( + <>
@@ -351,10 +353,10 @@ const WebhookActionConnectorFields: React.FunctionComponent {headersList} - + ) : null} - {headerControl} + {hasHeaders && headerControl}
diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts index 0f339154bd948..db1e59746162b 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts @@ -30,7 +30,8 @@ export default function alertTests({ getService }: FtrProviderContext) { const esTestIndexTool = new ESTestIndexTool(es, retry); const taskManagerUtils = new TaskManagerUtils(es, retry); - describe('alerts', () => { + // FLAKY: https://github.com/elastic/kibana/issues/72207 + describe.skip('alerts', () => { const authorizationIndex = '.kibana-test-authorization'; const objectRemover = new ObjectRemover(supertest); diff --git a/x-pack/test/api_integration/apis/fleet/install.ts b/x-pack/test/api_integration/apis/fleet/install.ts index 3a122463fae55..59b040e30fb48 100644 --- a/x-pack/test/api_integration/apis/fleet/install.ts +++ b/x-pack/test/api_integration/apis/fleet/install.ts @@ -6,11 +6,15 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; +import { setupIngest } from './agents/services'; -export default function ({ getService }: FtrProviderContext) { +export default function (providerContext: FtrProviderContext) { + const { getService } = providerContext; const supertest = getService('supertest'); describe('fleet_install', () => { + setupIngest(providerContext); + it('should return a 400 if we try download an install script for a not supported OS', async () => { await supertest.get(`/api/ingest_manager/fleet/install/gameboy`).expect(400); }); diff --git a/x-pack/test/api_integration/apis/fleet/unenroll_agent.ts b/x-pack/test/api_integration/apis/fleet/unenroll_agent.ts index bc6c44e590cc4..4e1443ad1fc68 100644 --- a/x-pack/test/api_integration/apis/fleet/unenroll_agent.ts +++ b/x-pack/test/api_integration/apis/fleet/unenroll_agent.ts @@ -16,7 +16,8 @@ export default function (providerContext: FtrProviderContext) { const supertest = getService('supertest'); const esClient = getService('es'); - describe('fleet_unenroll_agent', () => { + // FLAKY: https://github.com/elastic/kibana/issues/64696 + describe.skip('fleet_unenroll_agent', () => { let accessAPIKeyId: string; let outputAPIKeyId: string; before(async () => { diff --git a/x-pack/test/api_integration/apis/lists/create_exception_list_item.ts b/x-pack/test/api_integration/apis/lists/create_exception_list_item.ts index 41f2a2dd2e3f5..bf35a6283aae5 100644 --- a/x-pack/test/api_integration/apis/lists/create_exception_list_item.ts +++ b/x-pack/test/api_integration/apis/lists/create_exception_list_item.ts @@ -6,6 +6,7 @@ import expect from '@kbn/expect/expect.js'; import { FtrProviderContext } from '../../ftr_provider_context'; +import { ENDPOINT_LIST_ID } from '../../../../plugins/lists/common'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); @@ -20,7 +21,7 @@ export default function ({ getService }: FtrProviderContext) { namespace_type: 'agnostic', description: 'bad endpoint item for testing', name: 'bad endpoint item', - list_id: 'endpoint_list', + list_id: ENDPOINT_LIST_ID, type: 'simple', entries: [ { @@ -50,7 +51,7 @@ export default function ({ getService }: FtrProviderContext) { namespace_type: 'agnostic', description: 'bad endpoint item for testing', name: 'bad endpoint item', - list_id: 'endpoint_list', + list_id: ENDPOINT_LIST_ID, type: 'simple', entries: [ { diff --git a/x-pack/test/functional/apps/discover/async_scripted_fields.js b/x-pack/test/functional/apps/discover/async_scripted_fields.js index 46848b1db0ef4..33a64e4f9cdd3 100644 --- a/x-pack/test/functional/apps/discover/async_scripted_fields.js +++ b/x-pack/test/functional/apps/discover/async_scripted_fields.js @@ -14,9 +14,9 @@ export default function ({ getService, getPageObjects }) { const esArchiver = getService('esArchiver'); const log = getService('log'); const testSubjects = getService('testSubjects'); - const PageObjects = getPageObjects(['common', 'settings', 'discover', 'timePicker']); const queryBar = getService('queryBar'); + const security = getService('security'); describe('async search with scripted fields', function () { this.tags(['skipFirefox']); @@ -24,6 +24,7 @@ export default function ({ getService, getPageObjects }) { before(async function () { await esArchiver.load('kibana_scripted_fields_on_logstash'); await esArchiver.loadIfNeeded('logstash_functional'); + await security.testUser.setRoles(['test_logstash_reader', 'global_discover_read']); // changing the timepicker default here saves us from having to set it in Discover (~8s) await kibanaServer.uiSettings.update({ 'timepicker:timeDefaults': @@ -36,6 +37,7 @@ export default function ({ getService, getPageObjects }) { await kibanaServer.uiSettings.update({}); await esArchiver.unload('logstash_functional'); await esArchiver.load('empty_kibana'); + await security.testUser.restoreDefaults(); }); it('query should show failed shards pop up', async function () {