From 2368e63668ebb12924177157ba237a8813c68936 Mon Sep 17 00:00:00 2001 From: Alexander Wert Date: Thu, 24 Jun 2021 08:27:05 +0200 Subject: [PATCH 01/69] Exploratory View Mobile: Renamed Latency and Throuput labels to align with metric names in APM (#102711) --- .../configurations/constants/labels.ts | 4 ++-- .../mobile/kpi_over_time_config.ts | 24 +++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/labels.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/labels.ts index 73739b7db12ef..eb8af4f26c01a 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/labels.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/labels.ts @@ -272,7 +272,7 @@ export const CARRIER_LOCATION = i18n.translate( export const RESPONSE_LATENCY = i18n.translate( 'xpack.observability.expView.fieldLabels.responseLatency', { - defaultMessage: 'Response latency', + defaultMessage: 'Latency', } ); @@ -294,7 +294,7 @@ export const CPU_USAGE = i18n.translate('xpack.observability.expView.fieldLabels export const TRANSACTIONS_PER_MINUTE = i18n.translate( 'xpack.observability.expView.fieldLabels.transactionPerMinute', { - defaultMessage: 'Transactions per minute', + defaultMessage: 'Throughput', } ); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/kpi_over_time_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/kpi_over_time_config.ts index 2ed4d95760db7..9a2e86a8f7969 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/kpi_over_time_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/kpi_over_time_config.ts @@ -71,18 +71,6 @@ export function getMobileKPIConfig({ indexPattern }: ConfigProps): DataSeries { id: TRANSACTION_DURATION, columnType: OPERATION_COLUMN, }, - { - label: MEMORY_USAGE, - field: METRIC_SYSTEM_MEMORY_USAGE, - id: METRIC_SYSTEM_MEMORY_USAGE, - columnType: OPERATION_COLUMN, - }, - { - label: CPU_USAGE, - field: METRIC_SYSTEM_CPU_USAGE, - id: METRIC_SYSTEM_CPU_USAGE, - columnType: OPERATION_COLUMN, - }, { field: RECORDS_FIELD, id: RECORDS_FIELD, @@ -95,6 +83,18 @@ export function getMobileKPIConfig({ indexPattern }: ConfigProps): DataSeries { ], timeScale: 'm', }, + { + label: MEMORY_USAGE, + field: METRIC_SYSTEM_MEMORY_USAGE, + id: METRIC_SYSTEM_MEMORY_USAGE, + columnType: OPERATION_COLUMN, + }, + { + label: CPU_USAGE, + field: METRIC_SYSTEM_CPU_USAGE, + id: METRIC_SYSTEM_CPU_USAGE, + columnType: OPERATION_COLUMN, + }, ], }, ], From aee0585bc531d8460d3ea3f5028568910c160374 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Thu, 24 Jun 2021 09:35:33 +0200 Subject: [PATCH 02/69] [Lens] Do not reset columns on incomplete switch before closing flyout (#102876) --- .../dimension_panel/dimension_editor.tsx | 9 +--- .../dimension_panel/dimension_panel.test.tsx | 32 ++++++------- .../operations/layer_helpers.test.ts | 48 +++++++++++++++++++ .../operations/layer_helpers.ts | 42 +++++++++++----- .../test/functional/apps/lens/smokescreen.ts | 4 +- 5 files changed, 96 insertions(+), 39 deletions(-) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx index b35986c42054d..05100567c1b03 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx @@ -117,21 +117,14 @@ export function DimensionEditor(props: DimensionEditorProps) { const setStateWrapper = ( setter: IndexPatternLayer | ((prevLayer: IndexPatternLayer) => IndexPatternLayer) ) => { - const prevOperationType = - operationDefinitionMap[state.layers[layerId].columns[columnId]?.operationType]?.input; - const hypotheticalLayer = typeof setter === 'function' ? setter(state.layers[layerId]) : setter; - const hasIncompleteColumns = Boolean(hypotheticalLayer.incompleteColumns?.[columnId]); setState( (prevState) => { const layer = typeof setter === 'function' ? setter(prevState.layers[layerId]) : setter; return mergeLayer({ state: prevState, layerId, newLayer: layer }); }, { - isDimensionComplete: - prevOperationType === 'fullReference' - ? !hasIncompleteColumns - : Boolean(hypotheticalLayer.columns[columnId]), + isDimensionComplete: Boolean(hypotheticalLayer.columns[columnId]), } ); }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx index afcecdf5be9b8..d757d8573f25a 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx @@ -908,20 +908,21 @@ describe('IndexPatternDimensionEditorPanel', () => { }); }); - it('should clean up when transitioning from incomplete reference-based operations to field operation', () => { + it('should keep current state and write incomplete column when transitioning from incomplete reference-based operations to field operation', () => { + const baseState = getStateWithColumns({ + ...defaultProps.state.layers.first.columns, + col2: { + label: 'Counter rate', + dataType: 'number', + isBucketed: false, + operationType: 'counter_rate', + references: ['ref'], + }, + }); wrapper = mount( ); @@ -932,15 +933,12 @@ describe('IndexPatternDimensionEditorPanel', () => { .simulate('click'); // Now check that the dimension gets cleaned up on state update - expect(setState.mock.calls[0]).toEqual([ - expect.any(Function), - { isDimensionComplete: false }, - ]); + expect(setState.mock.calls[0]).toEqual([expect.any(Function), { isDimensionComplete: true }]); expect(setState.mock.calls[0][0](state)).toEqual({ - ...state, + ...baseState, layers: { first: { - ...state.layers.first, + ...baseState.layers.first, incompleteColumns: { col2: { operationType: 'average' }, }, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts index 7de1318cbac61..9eedae6d82d43 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts @@ -1917,6 +1917,54 @@ describe('state_helpers', () => { }) ); }); + + it('should keep state and set incomplete column on incompatible switch', () => { + const layer: IndexPatternLayer = { + indexPatternId: '1', + columnOrder: ['metric', 'ref'], + columns: { + metric: { + dataType: 'number' as const, + isBucketed: false, + sourceField: 'source', + operationType: 'unique_count' as const, + filter: { language: 'kuery', query: 'bytes > 4000' }, + timeShift: '3h', + label: 'Cardinality', + customLabel: true, + }, + ref: { + label: 'Reference', + dataType: 'number', + isBucketed: false, + operationType: 'differences', + references: ['metric'], + filter: { language: 'kuery', query: 'bytes > 4000' }, + timeShift: '3h', + }, + }, + }; + const result = replaceColumn({ + layer, + indexPattern, + columnId: 'ref', + op: 'sum', + visualizationGroups: [], + }); + expect(result.columnOrder).toEqual(layer.columnOrder); + expect(result.columns).toEqual(layer.columns); + expect(result.incompleteColumns).toEqual({ + ref: { + operationType: 'sum', + filter: { + language: 'kuery', + query: 'bytes > 4000', + }, + timeScale: undefined, + timeShift: '3h', + }, + }); + }); }); it('should allow making a replacement on an operation that is being referenced, even if it ends up invalid', () => { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts index fd3df9f97cecf..b5b1960b7b769 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts @@ -19,6 +19,7 @@ import { OperationType, IndexPatternColumn, RequiredReference, + OperationDefinition, GenericOperationDefinition, } from './definitions'; import type { @@ -532,20 +533,15 @@ export function replaceColumn({ ); } - // This logic comes after the transitions because they need to look at previous columns - if (previousDefinition.input === 'fullReference') { - (previousColumn as ReferenceBasedIndexPatternColumn).references.forEach((id: string) => { - tempLayer = deleteColumn({ - layer: tempLayer, - columnId: id, - indexPattern, - }); - }); - } - if (operationDefinition.input === 'none') { let newColumn = operationDefinition.buildColumn({ ...baseOptions, layer: tempLayer }); newColumn = copyCustomLabel(newColumn, previousColumn); + tempLayer = removeOrphanedColumns( + previousDefinition, + previousColumn, + tempLayer, + indexPattern + ); const newLayer = { ...tempLayer, columns: { ...tempLayer.columns, [columnId]: newColumn } }; return updateDefaultLabels( @@ -564,7 +560,6 @@ export function replaceColumn({ } & ColumnAdvancedParams = { operationType: op }; // if no field is available perform a full clean of the column from the layer if (previousDefinition.input === 'fullReference') { - tempLayer = deleteColumn({ layer: tempLayer, columnId, indexPattern }); const previousReferenceId = (previousColumn as ReferenceBasedIndexPatternColumn) .references[0]; const referenceColumn = layer.columns[previousReferenceId]; @@ -598,6 +593,8 @@ export function replaceColumn({ }; } + tempLayer = removeOrphanedColumns(previousDefinition, previousColumn, tempLayer, indexPattern); + let newColumn = operationDefinition.buildColumn({ ...baseOptions, layer: tempLayer, field }); if (!shouldResetLabel) { newColumn = copyCustomLabel(newColumn, previousColumn); @@ -637,6 +634,27 @@ export function replaceColumn({ } } +function removeOrphanedColumns( + previousDefinition: + | OperationDefinition + | OperationDefinition + | OperationDefinition, + previousColumn: IndexPatternColumn, + tempLayer: IndexPatternLayer, + indexPattern: IndexPattern +) { + if (previousDefinition.input === 'fullReference') { + (previousColumn as ReferenceBasedIndexPatternColumn).references.forEach((id: string) => { + tempLayer = deleteColumn({ + layer: tempLayer, + columnId: id, + indexPattern, + }); + }); + } + return tempLayer; +} + export function canTransition({ layer, columnId, diff --git a/x-pack/test/functional/apps/lens/smokescreen.ts b/x-pack/test/functional/apps/lens/smokescreen.ts index ec32d7620fcf9..78900e6fabca4 100644 --- a/x-pack/test/functional/apps/lens/smokescreen.ts +++ b/x-pack/test/functional/apps/lens/smokescreen.ts @@ -604,7 +604,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ); }); - it('should not leave an incomplete column in the visualization config with reference-based operations', async () => { + it('should revert to previous configuration and not leave an incomplete column in the visualization config with reference-based operations', async () => { await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickVisType('lens'); await PageObjects.lens.goToTimeRange(); @@ -636,7 +636,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.lens.closeDimensionEditor(); expect(await PageObjects.lens.getDimensionTriggerText('lnsXY_yDimensionPanel')).to.eql( - undefined + 'Moving average of Count of records' ); }); From 01a486000e52128a173802f715d4fc347665ad7d Mon Sep 17 00:00:00 2001 From: Liza Katz Date: Thu, 24 Jun 2021 10:50:09 +0300 Subject: [PATCH 03/69] [Search Sessions] Split tasks (#99967) * cancel the previous session * split to 3 tasks * fixes * cancellation * updated tests * split out and improve jest tests * cleanup previous session properly * don't fail delete and cancel if item was already cleaned up * test * test * ignore resource_not_found_exception when deleting an already cleared \ expired async search * jest * update jest * api int * fix jest * testssss * Code review @dosant * types * remove any * Fix merge * type * test * jest Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- src/plugins/data/config.ts | 12 +- .../search/session/session_service.test.ts | 8 + ... => check_non_persiseted_sessions.test.ts} | 295 ++-------------- .../session/check_non_persiseted_sessions.ts | 129 +++++++ .../session/check_persisted_sessions.test.ts | 76 +++++ .../session/check_persisted_sessions.ts | 72 ++++ .../search/session/check_running_sessions.ts | 257 -------------- .../session/expire_persisted_sessions.ts | 74 ++++ .../session/get_search_session_page.test.ts | 282 +++++++++++++++ .../search/session/get_search_session_page.ts | 61 ++++ .../server/search/session/index.ts | 1 - .../server/search/session/monitoring_task.ts | 119 ------- .../search/session/session_service.test.ts | 4 + .../server/search/session/session_service.ts | 78 ++++- .../server/search/session/setup_task.ts | 121 +++++++ .../server/search/session/types.ts | 47 +++ .../session/update_session_status.test.ts | 323 ++++++++++++++++++ .../search/session/update_session_status.ts | 128 +++++++ .../server/search/session/utils.ts | 9 + x-pack/test/api_integration/config.ts | 1 + 20 files changed, 1446 insertions(+), 651 deletions(-) rename x-pack/plugins/data_enhanced/server/search/session/{check_running_sessions.test.ts => check_non_persiseted_sessions.test.ts} (65%) create mode 100644 x-pack/plugins/data_enhanced/server/search/session/check_non_persiseted_sessions.ts create mode 100644 x-pack/plugins/data_enhanced/server/search/session/check_persisted_sessions.test.ts create mode 100644 x-pack/plugins/data_enhanced/server/search/session/check_persisted_sessions.ts delete mode 100644 x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.ts create mode 100644 x-pack/plugins/data_enhanced/server/search/session/expire_persisted_sessions.ts create mode 100644 x-pack/plugins/data_enhanced/server/search/session/get_search_session_page.test.ts create mode 100644 x-pack/plugins/data_enhanced/server/search/session/get_search_session_page.ts delete mode 100644 x-pack/plugins/data_enhanced/server/search/session/monitoring_task.ts create mode 100644 x-pack/plugins/data_enhanced/server/search/session/setup_task.ts create mode 100644 x-pack/plugins/data_enhanced/server/search/session/update_session_status.test.ts create mode 100644 x-pack/plugins/data_enhanced/server/search/session/update_session_status.ts diff --git a/src/plugins/data/config.ts b/src/plugins/data/config.ts index 9306b64019bbc..1b7bfbc09ad16 100644 --- a/src/plugins/data/config.ts +++ b/src/plugins/data/config.ts @@ -44,10 +44,20 @@ export const searchSessionsConfigSchema = schema.object({ */ pageSize: schema.number({ defaultValue: 100 }), /** - * trackingInterval controls how often we track search session objects progress + * trackingInterval controls how often we track persisted search session objects progress */ trackingInterval: schema.duration({ defaultValue: '10s' }), + /** + * cleanupInterval controls how often we track non-persisted search session objects for cleanup + */ + cleanupInterval: schema.duration({ defaultValue: '60s' }), + + /** + * expireInterval controls how often we track persisted search session objects for expiration + */ + expireInterval: schema.duration({ defaultValue: '60m' }), + /** * monitoringTaskTimeout controls for how long task manager waits for search session monitoring task to complete before considering it timed out, * If tasks timeouts it receives cancel signal and next task starts in "trackingInterval" time diff --git a/src/plugins/data/public/search/session/session_service.test.ts b/src/plugins/data/public/search/session/session_service.test.ts index 39680c4948366..7f388a29cd454 100644 --- a/src/plugins/data/public/search/session/session_service.test.ts +++ b/src/plugins/data/public/search/session/session_service.test.ts @@ -98,6 +98,14 @@ describe('Session service', () => { expect(nowProvider.reset).toHaveBeenCalled(); }); + it("Can clear other apps' session", async () => { + sessionService.start(); + expect(sessionService.getSessionId()).not.toBeUndefined(); + currentAppId$.next('change'); + sessionService.clear(); + expect(sessionService.getSessionId()).toBeUndefined(); + }); + it("Can start a new session in case there is other apps' stale session", async () => { const s1 = sessionService.start(); expect(sessionService.getSessionId()).not.toBeUndefined(); diff --git a/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.test.ts b/x-pack/plugins/data_enhanced/server/search/session/check_non_persiseted_sessions.test.ts similarity index 65% rename from x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.test.ts rename to x-pack/plugins/data_enhanced/server/search/session/check_non_persiseted_sessions.test.ts index c0a48d5d44862..0a80f1c06998f 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.test.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/check_non_persiseted_sessions.test.ts @@ -5,10 +5,7 @@ * 2.0. */ -import { - checkRunningSessions as checkRunningSessions$, - CheckRunningSessionsDeps, -} from './check_running_sessions'; +import { checkNonPersistedSessions as checkNonPersistedSessions$ } from './check_non_persiseted_sessions'; import { SearchSessionStatus, SearchSessionSavedObjectAttributes, @@ -16,22 +13,20 @@ import { EQL_SEARCH_STRATEGY, } from '../../../../../../src/plugins/data/common'; import { savedObjectsClientMock } from '../../../../../../src/core/server/mocks'; -import { SearchSessionsConfig, SearchStatus } from './types'; +import { SearchSessionsConfig, CheckSearchSessionsDeps, SearchStatus } from './types'; import moment from 'moment'; import { SavedObjectsBulkUpdateObject, SavedObjectsDeleteOptions, SavedObjectsClientContract, } from '../../../../../../src/core/server'; -import { Subject } from 'rxjs'; -import { takeUntil } from 'rxjs/operators'; jest.useFakeTimers(); -const checkRunningSessions = (deps: CheckRunningSessionsDeps, config: SearchSessionsConfig) => - checkRunningSessions$(deps, config).toPromise(); +const checkNonPersistedSessions = (deps: CheckSearchSessionsDeps, config: SearchSessionsConfig) => + checkNonPersistedSessions$(deps, config).toPromise(); -describe('getSearchStatus', () => { +describe('checkNonPersistedSessions', () => { let mockClient: any; let savedObjectsClient: jest.Mocked; const config: SearchSessionsConfig = { @@ -42,7 +37,9 @@ describe('getSearchStatus', () => { maxUpdateRetries: 3, defaultExpiration: moment.duration(7, 'd'), trackingInterval: moment.duration(10, 's'), + expireInterval: moment.duration(10, 'm'), monitoringTaskTimeout: moment.duration(5, 'm'), + cleanupInterval: moment.duration(10, 's'), management: {} as any, }; const mockLogger: any = { @@ -51,16 +48,6 @@ describe('getSearchStatus', () => { error: jest.fn(), }; - const emptySO = { - attributes: { - persisted: false, - status: SearchSessionStatus.IN_PROGRESS, - created: moment().subtract(moment.duration(3, 'm')), - touched: moment().subtract(moment.duration(10, 's')), - idMapping: {}, - }, - }; - beforeEach(() => { savedObjectsClient = savedObjectsClientMock.create(); mockClient = { @@ -81,7 +68,7 @@ describe('getSearchStatus', () => { total: 0, } as any); - await checkRunningSessions( + await checkNonPersistedSessions( { savedObjectsClient, client: mockClient, @@ -94,240 +81,7 @@ describe('getSearchStatus', () => { expect(savedObjectsClient.delete).not.toBeCalled(); }); - describe('pagination', () => { - test('fetches one page if not objects exist', async () => { - savedObjectsClient.find.mockResolvedValueOnce({ - saved_objects: [], - total: 0, - } as any); - - await checkRunningSessions( - { - savedObjectsClient, - client: mockClient, - logger: mockLogger, - }, - config - ); - - expect(savedObjectsClient.find).toHaveBeenCalledTimes(1); - }); - - test('fetches one page if less than page size object are returned', async () => { - savedObjectsClient.find.mockResolvedValueOnce({ - saved_objects: [emptySO, emptySO], - total: 5, - } as any); - - await checkRunningSessions( - { - savedObjectsClient, - client: mockClient, - logger: mockLogger, - }, - config - ); - - expect(savedObjectsClient.find).toHaveBeenCalledTimes(1); - }); - - test('fetches two pages if exactly page size objects are returned', async () => { - let i = 0; - savedObjectsClient.find.mockImplementation(() => { - return new Promise((resolve) => { - resolve({ - saved_objects: i++ === 0 ? [emptySO, emptySO, emptySO, emptySO, emptySO] : [], - total: 5, - page: i, - } as any); - }); - }); - - await checkRunningSessions( - { - savedObjectsClient, - client: mockClient, - logger: mockLogger, - }, - config - ); - - expect(savedObjectsClient.find).toHaveBeenCalledTimes(2); - - // validate that page number increases - const { page: page1 } = savedObjectsClient.find.mock.calls[0][0]; - const { page: page2 } = savedObjectsClient.find.mock.calls[1][0]; - expect(page1).toBe(1); - expect(page2).toBe(2); - }); - - test('fetches two pages if page size +1 objects are returned', async () => { - let i = 0; - savedObjectsClient.find.mockImplementation(() => { - return new Promise((resolve) => { - resolve({ - saved_objects: i++ === 0 ? [emptySO, emptySO, emptySO, emptySO, emptySO] : [emptySO], - total: 5, - page: i, - } as any); - }); - }); - - await checkRunningSessions( - { - savedObjectsClient, - client: mockClient, - logger: mockLogger, - }, - config - ); - - expect(savedObjectsClient.find).toHaveBeenCalledTimes(2); - }); - - test('fetching is abortable', async () => { - let i = 0; - const abort$ = new Subject(); - savedObjectsClient.find.mockImplementation(() => { - return new Promise((resolve) => { - if (++i === 2) { - abort$.next(); - } - resolve({ - saved_objects: i <= 5 ? [emptySO, emptySO, emptySO, emptySO, emptySO] : [], - total: 25, - page: i, - } as any); - }); - }); - - await checkRunningSessions$( - { - savedObjectsClient, - client: mockClient, - logger: mockLogger, - }, - config - ) - .pipe(takeUntil(abort$)) - .toPromise(); - - jest.runAllTimers(); - - // if not for `abort$` then this would be called 6 times! - expect(savedObjectsClient.find).toHaveBeenCalledTimes(2); - }); - - test('sorting is by "touched"', async () => { - savedObjectsClient.find.mockResolvedValueOnce({ - saved_objects: [], - total: 0, - } as any); - - await checkRunningSessions( - { - savedObjectsClient, - client: mockClient, - logger: mockLogger, - }, - config - ); - - expect(savedObjectsClient.find).toHaveBeenCalledWith( - expect.objectContaining({ sortField: 'touched', sortOrder: 'asc' }) - ); - }); - - test('sessions fetched in the beginning are processed even if sessions in the end fail', async () => { - let i = 0; - savedObjectsClient.find.mockImplementation(() => { - return new Promise((resolve, reject) => { - if (++i === 2) { - reject(new Error('Fake find error...')); - } - resolve({ - saved_objects: - i <= 5 - ? [ - i === 1 - ? { - id: '123', - attributes: { - persisted: false, - status: SearchSessionStatus.IN_PROGRESS, - created: moment().subtract(moment.duration(3, 'm')), - touched: moment().subtract(moment.duration(2, 'm')), - idMapping: { - 'map-key': { - strategy: ENHANCED_ES_SEARCH_STRATEGY, - id: 'async-id', - }, - }, - }, - } - : emptySO, - emptySO, - emptySO, - emptySO, - emptySO, - ] - : [], - total: 25, - page: i, - } as any); - }); - }); - - await checkRunningSessions$( - { - savedObjectsClient, - client: mockClient, - logger: mockLogger, - }, - config - ).toPromise(); - - jest.runAllTimers(); - - expect(savedObjectsClient.find).toHaveBeenCalledTimes(2); - - // by checking that delete was called we validate that sessions from session that were successfully fetched were processed - expect(mockClient.asyncSearch.delete).toBeCalled(); - const { id } = mockClient.asyncSearch.delete.mock.calls[0][0]; - expect(id).toBe('async-id'); - }); - }); - describe('delete', () => { - test('doesnt delete a persisted session', async () => { - savedObjectsClient.find.mockResolvedValue({ - saved_objects: [ - { - id: '123', - attributes: { - persisted: true, - status: SearchSessionStatus.IN_PROGRESS, - created: moment().subtract(moment.duration(30, 'm')), - touched: moment().subtract(moment.duration(10, 'm')), - idMapping: {}, - }, - }, - ], - total: 1, - } as any); - await checkRunningSessions( - { - savedObjectsClient, - client: mockClient, - logger: mockLogger, - }, - config - ); - - expect(savedObjectsClient.bulkUpdate).not.toBeCalled(); - expect(savedObjectsClient.delete).not.toBeCalled(); - }); - test('doesnt delete a non persisted, recently touched session', async () => { savedObjectsClient.find.mockResolvedValue({ saved_objects: [ @@ -336,6 +90,7 @@ describe('getSearchStatus', () => { attributes: { persisted: false, status: SearchSessionStatus.IN_PROGRESS, + expires: moment().add(moment.duration(3, 'm')), created: moment().subtract(moment.duration(3, 'm')), touched: moment().subtract(moment.duration(10, 's')), idMapping: {}, @@ -344,7 +99,7 @@ describe('getSearchStatus', () => { ], total: 1, } as any); - await checkRunningSessions( + await checkNonPersistedSessions( { savedObjectsClient, client: mockClient, @@ -367,6 +122,7 @@ describe('getSearchStatus', () => { status: SearchSessionStatus.COMPLETE, created: moment().subtract(moment.duration(3, 'm')), touched: moment().subtract(moment.duration(1, 'm')), + expires: moment().add(moment.duration(3, 'm')), idMapping: { 'search-hash': { id: 'search-id', @@ -379,7 +135,7 @@ describe('getSearchStatus', () => { ], total: 1, } as any); - await checkRunningSessions( + await checkNonPersistedSessions( { savedObjectsClient, client: mockClient, @@ -401,6 +157,7 @@ describe('getSearchStatus', () => { attributes: { persisted: false, status: SearchSessionStatus.IN_PROGRESS, + expires: moment().add(moment.duration(3, 'm')), created: moment().subtract(moment.duration(3, 'm')), touched: moment().subtract(moment.duration(2, 'm')), idMapping: { @@ -415,7 +172,7 @@ describe('getSearchStatus', () => { total: 1, } as any); - await checkRunningSessions( + await checkNonPersistedSessions( { savedObjectsClient, client: mockClient, @@ -441,6 +198,7 @@ describe('getSearchStatus', () => { status: SearchSessionStatus.IN_PROGRESS, created: moment().subtract(moment.duration(3, 'm')), touched: moment().subtract(moment.duration(2, 'm')), + expires: moment().add(moment.duration(3, 'm')), idMapping: { 'map-key': { strategy: ENHANCED_ES_SEARCH_STRATEGY, @@ -453,7 +211,7 @@ describe('getSearchStatus', () => { total: 1, } as any); - await checkRunningSessions( + await checkNonPersistedSessions( { savedObjectsClient, client: mockClient, @@ -481,6 +239,7 @@ describe('getSearchStatus', () => { attributes: { persisted: false, status: SearchSessionStatus.COMPLETE, + expires: moment().add(moment.duration(3, 'm')), created: moment().subtract(moment.duration(30, 'm')), touched: moment().subtract(moment.duration(6, 'm')), idMapping: { @@ -501,7 +260,7 @@ describe('getSearchStatus', () => { total: 1, } as any); - await checkRunningSessions( + await checkNonPersistedSessions( { savedObjectsClient, client: mockClient, @@ -530,6 +289,7 @@ describe('getSearchStatus', () => { attributes: { persisted: false, status: SearchSessionStatus.COMPLETE, + expires: moment().add(moment.duration(3, 'm')), created: moment().subtract(moment.duration(30, 'm')), touched: moment().subtract(moment.duration(6, 'm')), idMapping: { @@ -545,7 +305,7 @@ describe('getSearchStatus', () => { total: 1, } as any); - await checkRunningSessions( + await checkNonPersistedSessions( { savedObjectsClient, client: mockClient, @@ -573,6 +333,7 @@ describe('getSearchStatus', () => { status: SearchSessionStatus.IN_PROGRESS, created: moment().subtract(moment.duration(3, 'm')), touched: moment().subtract(moment.duration(10, 's')), + expires: moment().add(moment.duration(3, 'm')), idMapping: { 'search-hash': { id: 'search-id', @@ -594,7 +355,7 @@ describe('getSearchStatus', () => { }, }); - await checkRunningSessions( + await checkNonPersistedSessions( { savedObjectsClient, client: mockClient, @@ -614,6 +375,7 @@ describe('getSearchStatus', () => { id: '123', attributes: { status: SearchSessionStatus.ERROR, + expires: moment().add(moment.duration(3, 'm')), idMapping: { 'search-hash': { id: 'search-id', @@ -633,7 +395,7 @@ describe('getSearchStatus', () => { total: 1, } as any); - await checkRunningSessions( + await checkNonPersistedSessions( { savedObjectsClient, client: mockClient, @@ -653,6 +415,7 @@ describe('getSearchStatus', () => { namespaces: ['awesome'], attributes: { status: SearchSessionStatus.IN_PROGRESS, + expires: moment().add(moment.duration(3, 'm')), touched: '123', idMapping: { 'search-hash': { @@ -676,7 +439,7 @@ describe('getSearchStatus', () => { }, }); - await checkRunningSessions( + await checkNonPersistedSessions( { savedObjectsClient, client: mockClient, @@ -696,6 +459,7 @@ describe('getSearchStatus', () => { const so = { attributes: { status: SearchSessionStatus.IN_PROGRESS, + expires: moment().add(moment.duration(3, 'm')), touched: '123', idMapping: { 'search-hash': { @@ -719,7 +483,7 @@ describe('getSearchStatus', () => { }, }); - await checkRunningSessions( + await checkNonPersistedSessions( { savedObjectsClient, client: mockClient, @@ -744,6 +508,7 @@ describe('getSearchStatus', () => { savedObjectsClient.bulkUpdate = jest.fn(); const so = { attributes: { + expires: moment().add(moment.duration(3, 'm')), idMapping: { 'search-hash': { id: 'search-id', @@ -766,7 +531,7 @@ describe('getSearchStatus', () => { }, }); - await checkRunningSessions( + await checkNonPersistedSessions( { savedObjectsClient, client: mockClient, diff --git a/x-pack/plugins/data_enhanced/server/search/session/check_non_persiseted_sessions.ts b/x-pack/plugins/data_enhanced/server/search/session/check_non_persiseted_sessions.ts new file mode 100644 index 0000000000000..8c75ce91cac6a --- /dev/null +++ b/x-pack/plugins/data_enhanced/server/search/session/check_non_persiseted_sessions.ts @@ -0,0 +1,129 @@ +/* + * 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 { SavedObjectsFindResult } from 'kibana/server'; +import moment from 'moment'; +import { EMPTY } from 'rxjs'; +import { catchError, concatMap } from 'rxjs/operators'; +import { + nodeBuilder, + ENHANCED_ES_SEARCH_STRATEGY, + SEARCH_SESSION_TYPE, + SearchSessionSavedObjectAttributes, + SearchSessionStatus, + KueryNode, +} from '../../../../../../src/plugins/data/common'; +import { checkSearchSessionsByPage, getSearchSessionsPage$ } from './get_search_session_page'; +import { SearchSessionsConfig, CheckSearchSessionsDeps } from './types'; +import { bulkUpdateSessions, getAllSessionsStatusUpdates } from './update_session_status'; + +export const SEARCH_SESSIONS_CLEANUP_TASK_TYPE = 'search_sessions_cleanup'; +export const SEARCH_SESSIONS_CLEANUP_TASK_ID = `data_enhanced_${SEARCH_SESSIONS_CLEANUP_TASK_TYPE}`; + +function isSessionStale( + session: SavedObjectsFindResult, + config: SearchSessionsConfig +) { + const curTime = moment(); + // Delete cancelled sessions immediately + if (session.attributes.status === SearchSessionStatus.CANCELLED) return true; + // Delete if a running session wasn't polled for in the last notTouchedInProgressTimeout OR + // if a completed \ errored \ canceled session wasn't saved for within notTouchedTimeout + return ( + (session.attributes.status === SearchSessionStatus.IN_PROGRESS && + curTime.diff(moment(session.attributes.touched), 'ms') > + config.notTouchedInProgressTimeout.asMilliseconds()) || + (session.attributes.status !== SearchSessionStatus.IN_PROGRESS && + curTime.diff(moment(session.attributes.touched), 'ms') > + config.notTouchedTimeout.asMilliseconds()) + ); +} + +function checkNonPersistedSessionsPage( + deps: CheckSearchSessionsDeps, + config: SearchSessionsConfig, + filter: KueryNode, + page: number +) { + const { logger, client, savedObjectsClient } = deps; + logger.debug(`${SEARCH_SESSIONS_CLEANUP_TASK_TYPE} Fetching sessions from page ${page}`); + return getSearchSessionsPage$(deps, filter, config.pageSize, page).pipe( + concatMap(async (nonPersistedSearchSessions) => { + if (!nonPersistedSearchSessions.total) return nonPersistedSearchSessions; + + logger.debug( + `${SEARCH_SESSIONS_CLEANUP_TASK_TYPE} Found ${nonPersistedSearchSessions.total} sessions, processing ${nonPersistedSearchSessions.saved_objects.length}` + ); + + const updatedSessions = await getAllSessionsStatusUpdates(deps, nonPersistedSearchSessions); + const deletedSessionIds: string[] = []; + + await Promise.all( + nonPersistedSearchSessions.saved_objects.map(async (session) => { + if (isSessionStale(session, config)) { + // delete saved object to free up memory + // TODO: there's a potential rare edge case of deleting an object and then receiving a new trackId for that same session! + // Maybe we want to change state to deleted and cleanup later? + logger.debug(`Deleting stale session | ${session.id}`); + try { + deletedSessionIds.push(session.id); + await savedObjectsClient.delete(SEARCH_SESSION_TYPE, session.id, { + namespace: session.namespaces?.[0], + }); + } catch (e) { + logger.error( + `${SEARCH_SESSIONS_CLEANUP_TASK_TYPE} Error while deleting session ${session.id}: ${e.message}` + ); + } + + // Send a delete request for each async search to ES + Object.keys(session.attributes.idMapping).map(async (searchKey: string) => { + const searchInfo = session.attributes.idMapping[searchKey]; + if (searchInfo.strategy === ENHANCED_ES_SEARCH_STRATEGY) { + try { + await client.asyncSearch.delete({ id: searchInfo.id }); + } catch (e) { + if (e.message !== 'resource_not_found_exception') { + logger.error( + `${SEARCH_SESSIONS_CLEANUP_TASK_TYPE} Error while deleting async_search ${searchInfo.id}: ${e.message}` + ); + } + } + } + }); + } + }) + ); + + const nonDeletedSessions = updatedSessions.filter((updateSession) => { + return deletedSessionIds.indexOf(updateSession.id) === -1; + }); + + await bulkUpdateSessions(deps, nonDeletedSessions); + + return nonPersistedSearchSessions; + }) + ); +} + +export function checkNonPersistedSessions( + deps: CheckSearchSessionsDeps, + config: SearchSessionsConfig +) { + const { logger } = deps; + + const filters = nodeBuilder.is(`${SEARCH_SESSION_TYPE}.attributes.persisted`, 'false'); + + return checkSearchSessionsByPage(checkNonPersistedSessionsPage, deps, config, filters).pipe( + catchError((e) => { + logger.error( + `${SEARCH_SESSIONS_CLEANUP_TASK_TYPE} Error while processing sessions: ${e?.message}` + ); + return EMPTY; + }) + ); +} diff --git a/x-pack/plugins/data_enhanced/server/search/session/check_persisted_sessions.test.ts b/x-pack/plugins/data_enhanced/server/search/session/check_persisted_sessions.test.ts new file mode 100644 index 0000000000000..e0b1b74b57d02 --- /dev/null +++ b/x-pack/plugins/data_enhanced/server/search/session/check_persisted_sessions.test.ts @@ -0,0 +1,76 @@ +/* + * 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 { checkPersistedSessionsProgress } from './check_persisted_sessions'; +import { savedObjectsClientMock } from '../../../../../../src/core/server/mocks'; +import { SearchSessionsConfig } from './types'; +import moment from 'moment'; +import { SavedObjectsClientContract } from '../../../../../../src/core/server'; + +describe('checkPersistedSessionsProgress', () => { + let mockClient: any; + let savedObjectsClient: jest.Mocked; + const config: SearchSessionsConfig = { + enabled: true, + pageSize: 5, + notTouchedInProgressTimeout: moment.duration(1, 'm'), + notTouchedTimeout: moment.duration(5, 'm'), + maxUpdateRetries: 3, + defaultExpiration: moment.duration(7, 'd'), + trackingInterval: moment.duration(10, 's'), + cleanupInterval: moment.duration(10, 's'), + expireInterval: moment.duration(10, 'm'), + monitoringTaskTimeout: moment.duration(5, 'm'), + management: {} as any, + }; + const mockLogger: any = { + debug: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }; + + beforeEach(() => { + savedObjectsClient = savedObjectsClientMock.create(); + mockClient = { + asyncSearch: { + status: jest.fn(), + delete: jest.fn(), + }, + eql: { + status: jest.fn(), + delete: jest.fn(), + }, + }; + }); + + test('fetches only running persisted sessions', async () => { + savedObjectsClient.find.mockResolvedValue({ + saved_objects: [], + total: 0, + } as any); + + await checkPersistedSessionsProgress( + { + savedObjectsClient, + client: mockClient, + logger: mockLogger, + }, + config + ); + + const [findInput] = savedObjectsClient.find.mock.calls[0]; + + expect(findInput.filter.arguments[0].arguments[0].value).toBe( + 'search-session.attributes.persisted' + ); + expect(findInput.filter.arguments[0].arguments[1].value).toBe('true'); + expect(findInput.filter.arguments[1].arguments[0].value).toBe( + 'search-session.attributes.status' + ); + expect(findInput.filter.arguments[1].arguments[1].value).toBe('in_progress'); + }); +}); diff --git a/x-pack/plugins/data_enhanced/server/search/session/check_persisted_sessions.ts b/x-pack/plugins/data_enhanced/server/search/session/check_persisted_sessions.ts new file mode 100644 index 0000000000000..0d51e97952275 --- /dev/null +++ b/x-pack/plugins/data_enhanced/server/search/session/check_persisted_sessions.ts @@ -0,0 +1,72 @@ +/* + * 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 { EMPTY, Observable } from 'rxjs'; +import { catchError, concatMap } from 'rxjs/operators'; +import { + nodeBuilder, + SEARCH_SESSION_TYPE, + SearchSessionStatus, + KueryNode, +} from '../../../../../../src/plugins/data/common'; +import { checkSearchSessionsByPage, getSearchSessionsPage$ } from './get_search_session_page'; +import { SearchSessionsConfig, CheckSearchSessionsDeps, SearchSessionsResponse } from './types'; +import { bulkUpdateSessions, getAllSessionsStatusUpdates } from './update_session_status'; + +export const SEARCH_SESSIONS_TASK_TYPE = 'search_sessions_monitor'; +export const SEARCH_SESSIONS_TASK_ID = `data_enhanced_${SEARCH_SESSIONS_TASK_TYPE}`; + +function checkPersistedSessionsPage( + deps: CheckSearchSessionsDeps, + config: SearchSessionsConfig, + filter: KueryNode, + page: number +): Observable { + const { logger } = deps; + logger.debug(`${SEARCH_SESSIONS_TASK_TYPE} Fetching sessions from page ${page}`); + return getSearchSessionsPage$(deps, filter, config.pageSize, page).pipe( + concatMap(async (persistedSearchSessions) => { + if (!persistedSearchSessions.total) return persistedSearchSessions; + + logger.debug( + `${SEARCH_SESSIONS_TASK_TYPE} Found ${persistedSearchSessions.total} sessions, processing ${persistedSearchSessions.saved_objects.length}` + ); + + const updatedSessions = await getAllSessionsStatusUpdates(deps, persistedSearchSessions); + await bulkUpdateSessions(deps, updatedSessions); + + return persistedSearchSessions; + }) + ); +} + +export function checkPersistedSessionsProgress( + deps: CheckSearchSessionsDeps, + config: SearchSessionsConfig +) { + const { logger } = deps; + + const persistedSessionsFilter = nodeBuilder.and([ + nodeBuilder.is(`${SEARCH_SESSION_TYPE}.attributes.persisted`, 'true'), + nodeBuilder.is( + `${SEARCH_SESSION_TYPE}.attributes.status`, + SearchSessionStatus.IN_PROGRESS.toString() + ), + ]); + + return checkSearchSessionsByPage( + checkPersistedSessionsPage, + deps, + config, + persistedSessionsFilter + ).pipe( + catchError((e) => { + logger.error(`${SEARCH_SESSIONS_TASK_TYPE} Error while processing sessions: ${e?.message}`); + return EMPTY; + }) + ); +} diff --git a/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.ts b/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.ts deleted file mode 100644 index 6787d31ed2b74..0000000000000 --- a/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.ts +++ /dev/null @@ -1,257 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - ElasticsearchClient, - Logger, - SavedObjectsClientContract, - SavedObjectsFindResult, - SavedObjectsUpdateResponse, -} from 'kibana/server'; -import moment from 'moment'; -import { EMPTY, from, Observable } from 'rxjs'; -import { catchError, concatMap } from 'rxjs/operators'; -import { - nodeBuilder, - ENHANCED_ES_SEARCH_STRATEGY, - SEARCH_SESSION_TYPE, - SearchSessionRequestInfo, - SearchSessionSavedObjectAttributes, - SearchSessionStatus, -} from '../../../../../../src/plugins/data/common'; -import { getSearchStatus } from './get_search_status'; -import { getSessionStatus } from './get_session_status'; -import { SearchSessionsConfig, SearchStatus } from './types'; - -export interface CheckRunningSessionsDeps { - savedObjectsClient: SavedObjectsClientContract; - client: ElasticsearchClient; - logger: Logger; -} - -function isSessionStale( - session: SavedObjectsFindResult, - config: SearchSessionsConfig, - logger: Logger -) { - const curTime = moment(); - // Delete if a running session wasn't polled for in the last notTouchedInProgressTimeout OR - // if a completed \ errored \ canceled session wasn't saved for within notTouchedTimeout - return ( - (session.attributes.status === SearchSessionStatus.IN_PROGRESS && - curTime.diff(moment(session.attributes.touched), 'ms') > - config.notTouchedInProgressTimeout.asMilliseconds()) || - (session.attributes.status !== SearchSessionStatus.IN_PROGRESS && - curTime.diff(moment(session.attributes.touched), 'ms') > - config.notTouchedTimeout.asMilliseconds()) - ); -} - -async function updateSessionStatus( - session: SavedObjectsFindResult, - client: ElasticsearchClient, - logger: Logger -) { - let sessionUpdated = false; - - // Check statuses of all running searches - await Promise.all( - Object.keys(session.attributes.idMapping).map(async (searchKey: string) => { - const updateSearchRequest = ( - currentStatus: Pick - ) => { - sessionUpdated = true; - session.attributes.idMapping[searchKey] = { - ...session.attributes.idMapping[searchKey], - ...currentStatus, - }; - }; - - const searchInfo = session.attributes.idMapping[searchKey]; - if (searchInfo.status === SearchStatus.IN_PROGRESS) { - try { - const currentStatus = await getSearchStatus(client, searchInfo.id); - - if (currentStatus.status !== searchInfo.status) { - logger.debug(`search ${searchInfo.id} | status changed to ${currentStatus.status}`); - updateSearchRequest(currentStatus); - } - } catch (e) { - logger.error(e); - updateSearchRequest({ - status: SearchStatus.ERROR, - error: e.message || e.meta.error?.caused_by?.reason, - }); - } - } - }) - ); - - // And only then derive the session's status - const sessionStatus = getSessionStatus(session.attributes); - if (sessionStatus !== session.attributes.status) { - const now = new Date().toISOString(); - session.attributes.status = sessionStatus; - session.attributes.touched = now; - if (sessionStatus === SearchSessionStatus.COMPLETE) { - session.attributes.completed = now; - } else if (session.attributes.completed) { - session.attributes.completed = null; - } - sessionUpdated = true; - } - - return sessionUpdated; -} - -function getSavedSearchSessionsPage$( - { savedObjectsClient, logger }: CheckRunningSessionsDeps, - config: SearchSessionsConfig, - page: number -) { - logger.debug(`Fetching saved search sessions page ${page}`); - return from( - savedObjectsClient.find({ - page, - perPage: config.pageSize, - type: SEARCH_SESSION_TYPE, - namespaces: ['*'], - // process older sessions first - sortField: 'touched', - sortOrder: 'asc', - filter: nodeBuilder.or([ - nodeBuilder.and([ - nodeBuilder.is( - `${SEARCH_SESSION_TYPE}.attributes.status`, - SearchSessionStatus.IN_PROGRESS.toString() - ), - nodeBuilder.is(`${SEARCH_SESSION_TYPE}.attributes.persisted`, 'true'), - ]), - nodeBuilder.is(`${SEARCH_SESSION_TYPE}.attributes.persisted`, 'false'), - ]), - }) - ); -} - -function checkRunningSessionsPage( - deps: CheckRunningSessionsDeps, - config: SearchSessionsConfig, - page: number -) { - const { logger, client, savedObjectsClient } = deps; - return getSavedSearchSessionsPage$(deps, config, page).pipe( - concatMap(async (runningSearchSessionsResponse) => { - if (!runningSearchSessionsResponse.total) return; - - logger.debug( - `Found ${runningSearchSessionsResponse.total} running sessions, processing ${runningSearchSessionsResponse.saved_objects.length} sessions from page ${page}` - ); - - const updatedSessions = new Array< - SavedObjectsFindResult - >(); - - await Promise.all( - runningSearchSessionsResponse.saved_objects.map(async (session) => { - const updated = await updateSessionStatus(session, client, logger); - let deleted = false; - - if (!session.attributes.persisted) { - if (isSessionStale(session, config, logger)) { - // delete saved object to free up memory - // TODO: there's a potential rare edge case of deleting an object and then receiving a new trackId for that same session! - // Maybe we want to change state to deleted and cleanup later? - logger.debug(`Deleting stale session | ${session.id}`); - try { - await savedObjectsClient.delete(SEARCH_SESSION_TYPE, session.id, { - namespace: session.namespaces?.[0], - }); - deleted = true; - } catch (e) { - logger.error( - `Error while deleting stale search session ${session.id}: ${e.message}` - ); - } - - // Send a delete request for each async search to ES - Object.keys(session.attributes.idMapping).map(async (searchKey: string) => { - const searchInfo = session.attributes.idMapping[searchKey]; - if (searchInfo.strategy === ENHANCED_ES_SEARCH_STRATEGY) { - try { - await client.asyncSearch.delete({ id: searchInfo.id }); - } catch (e) { - logger.error( - `Error while deleting async_search ${searchInfo.id}: ${e.message}` - ); - } - } - }); - } - } - - if (updated && !deleted) { - updatedSessions.push(session); - } - }) - ); - - // Do a bulk update - if (updatedSessions.length) { - // If there's an error, we'll try again in the next iteration, so there's no need to check the output. - const updatedResponse = await savedObjectsClient.bulkUpdate( - updatedSessions.map((session) => ({ - ...session, - namespace: session.namespaces?.[0], - })) - ); - - const success: Array> = []; - const fail: Array> = []; - - updatedResponse.saved_objects.forEach((savedObjectResponse) => { - if ('error' in savedObjectResponse) { - fail.push(savedObjectResponse); - logger.error( - `Error while updating search session ${savedObjectResponse?.id}: ${savedObjectResponse.error?.message}` - ); - } else { - success.push(savedObjectResponse); - } - }); - - logger.debug(`Updating search sessions: success: ${success.length}, fail: ${fail.length}`); - } - - return runningSearchSessionsResponse; - }) - ); -} - -export function checkRunningSessions(deps: CheckRunningSessionsDeps, config: SearchSessionsConfig) { - const { logger } = deps; - - const checkRunningSessionsByPage = (nextPage = 1): Observable => - checkRunningSessionsPage(deps, config, nextPage).pipe( - concatMap((result) => { - if (!result || !result.saved_objects || result.saved_objects.length < config.pageSize) { - return EMPTY; - } else { - // TODO: while processing previous page session list might have been changed and we might skip a session, - // because it would appear now on a different "page". - // This isn't critical, as we would pick it up on a next task iteration, but maybe we could improve this somehow - return checkRunningSessionsByPage(result.page + 1); - } - }) - ); - - return checkRunningSessionsByPage().pipe( - catchError((e) => { - logger.error(`Error while processing search sessions: ${e?.message}`); - return EMPTY; - }) - ); -} diff --git a/x-pack/plugins/data_enhanced/server/search/session/expire_persisted_sessions.ts b/x-pack/plugins/data_enhanced/server/search/session/expire_persisted_sessions.ts new file mode 100644 index 0000000000000..e261c324f440f --- /dev/null +++ b/x-pack/plugins/data_enhanced/server/search/session/expire_persisted_sessions.ts @@ -0,0 +1,74 @@ +/* + * 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 { EMPTY, Observable } from 'rxjs'; +import { catchError, concatMap } from 'rxjs/operators'; +import { + nodeBuilder, + SEARCH_SESSION_TYPE, + SearchSessionStatus, + KueryNode, +} from '../../../../../../src/plugins/data/common'; +import { checkSearchSessionsByPage, getSearchSessionsPage$ } from './get_search_session_page'; +import { SearchSessionsConfig, CheckSearchSessionsDeps, SearchSessionsResponse } from './types'; +import { bulkUpdateSessions, getAllSessionsStatusUpdates } from './update_session_status'; + +export const SEARCH_SESSIONS_EXPIRE_TASK_TYPE = 'search_sessions_expire'; +export const SEARCH_SESSIONS_EXPIRE_TASK_ID = `data_enhanced_${SEARCH_SESSIONS_EXPIRE_TASK_TYPE}`; + +function checkSessionExpirationPage( + deps: CheckSearchSessionsDeps, + config: SearchSessionsConfig, + filter: KueryNode, + page: number +): Observable { + const { logger } = deps; + logger.debug(`${SEARCH_SESSIONS_EXPIRE_TASK_TYPE} Fetching sessions from page ${page}`); + return getSearchSessionsPage$(deps, filter, config.pageSize, page).pipe( + concatMap(async (searchSessions) => { + if (!searchSessions.total) return searchSessions; + + logger.debug( + `${SEARCH_SESSIONS_EXPIRE_TASK_TYPE} Found ${searchSessions.total} sessions, processing ${searchSessions.saved_objects.length}` + ); + + const updatedSessions = await getAllSessionsStatusUpdates(deps, searchSessions); + await bulkUpdateSessions(deps, updatedSessions); + + return searchSessions; + }) + ); +} + +export function checkPersistedCompletedSessionExpiration( + deps: CheckSearchSessionsDeps, + config: SearchSessionsConfig +) { + const { logger } = deps; + + const persistedSessionsFilter = nodeBuilder.and([ + nodeBuilder.is(`${SEARCH_SESSION_TYPE}.attributes.persisted`, 'true'), + nodeBuilder.is( + `${SEARCH_SESSION_TYPE}.attributes.status`, + SearchSessionStatus.COMPLETE.toString() + ), + ]); + + return checkSearchSessionsByPage( + checkSessionExpirationPage, + deps, + config, + persistedSessionsFilter + ).pipe( + catchError((e) => { + logger.error( + `${SEARCH_SESSIONS_EXPIRE_TASK_TYPE} Error while processing sessions: ${e?.message}` + ); + return EMPTY; + }) + ); +} diff --git a/x-pack/plugins/data_enhanced/server/search/session/get_search_session_page.test.ts b/x-pack/plugins/data_enhanced/server/search/session/get_search_session_page.test.ts new file mode 100644 index 0000000000000..df2b7d964642d --- /dev/null +++ b/x-pack/plugins/data_enhanced/server/search/session/get_search_session_page.test.ts @@ -0,0 +1,282 @@ +/* + * 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 { checkSearchSessionsByPage, getSearchSessionsPage$ } from './get_search_session_page'; +import { + SearchSessionStatus, + ENHANCED_ES_SEARCH_STRATEGY, +} from '../../../../../../src/plugins/data/common'; +import { savedObjectsClientMock } from '../../../../../../src/core/server/mocks'; +import { SearchSessionsConfig, SearchStatus } from './types'; +import moment from 'moment'; +import { SavedObjectsClientContract } from '../../../../../../src/core/server'; +import { of, Subject, throwError } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; +jest.useFakeTimers(); + +describe('checkSearchSessionsByPage', () => { + const mockClient = {} as any; + let savedObjectsClient: jest.Mocked; + const config: SearchSessionsConfig = { + enabled: true, + pageSize: 5, + management: {} as any, + } as any; + const mockLogger: any = { + debug: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }; + + const emptySO = { + attributes: { + persisted: false, + status: SearchSessionStatus.IN_PROGRESS, + created: moment().subtract(moment.duration(3, 'm')), + touched: moment().subtract(moment.duration(10, 's')), + idMapping: {}, + }, + }; + + beforeEach(() => { + savedObjectsClient = savedObjectsClientMock.create(); + }); + + describe('getSearchSessionsPage$', () => { + test('sorting is by "touched"', async () => { + savedObjectsClient.find.mockResolvedValueOnce({ + saved_objects: [], + total: 0, + } as any); + + await getSearchSessionsPage$( + { + savedObjectsClient, + } as any, + { + type: 'literal', + }, + 1, + 1 + ); + + expect(savedObjectsClient.find).toHaveBeenCalledWith( + expect.objectContaining({ sortField: 'touched', sortOrder: 'asc' }) + ); + }); + }); + + describe('pagination', () => { + test('fetches one page if got empty response', async () => { + const checkFn = jest.fn().mockReturnValue(of(undefined)); + + await checkSearchSessionsByPage( + checkFn, + { + savedObjectsClient, + client: mockClient, + logger: mockLogger, + }, + config, + [] + ).toPromise(); + + expect(checkFn).toHaveBeenCalledTimes(1); + }); + + test('fetches one page if got response with no saved objects', async () => { + const checkFn = jest.fn().mockReturnValue( + of({ + total: 0, + }) + ); + + await checkSearchSessionsByPage( + checkFn, + { + savedObjectsClient, + client: mockClient, + logger: mockLogger, + }, + config, + [] + ).toPromise(); + + expect(checkFn).toHaveBeenCalledTimes(1); + }); + + test('fetches one page if less than page size object are returned', async () => { + const checkFn = jest.fn().mockReturnValue( + of({ + saved_objects: [emptySO, emptySO], + total: 5, + }) + ); + + await checkSearchSessionsByPage( + checkFn, + { + savedObjectsClient, + client: mockClient, + logger: mockLogger, + }, + config, + [] + ).toPromise(); + + expect(checkFn).toHaveBeenCalledTimes(1); + }); + + test('fetches two pages if exactly page size objects are returned', async () => { + let i = 0; + + const checkFn = jest.fn().mockImplementation(() => + of({ + saved_objects: i++ === 0 ? [emptySO, emptySO, emptySO, emptySO, emptySO] : [], + total: 5, + page: i, + }) + ); + + await checkSearchSessionsByPage( + checkFn, + { + savedObjectsClient, + client: mockClient, + logger: mockLogger, + }, + config, + [] + ).toPromise(); + + expect(checkFn).toHaveBeenCalledTimes(2); + + // validate that page number increases + const page1 = checkFn.mock.calls[0][3]; + const page2 = checkFn.mock.calls[1][3]; + expect(page1).toBe(1); + expect(page2).toBe(2); + }); + + test('fetches two pages if page size +1 objects are returned', async () => { + let i = 0; + + const checkFn = jest.fn().mockImplementation(() => + of({ + saved_objects: i++ === 0 ? [emptySO, emptySO, emptySO, emptySO, emptySO] : [emptySO], + total: i === 0 ? 5 : 1, + page: i, + }) + ); + + await checkSearchSessionsByPage( + checkFn, + { + savedObjectsClient, + client: mockClient, + logger: mockLogger, + }, + config, + [] + ).toPromise(); + + expect(checkFn).toHaveBeenCalledTimes(2); + }); + + test('sessions fetched in the beginning are processed even if sessions in the end fail', async () => { + let i = 0; + + const checkFn = jest.fn().mockImplementation(() => { + if (++i === 2) { + return throwError('Fake find error...'); + } + return of({ + saved_objects: + i <= 5 + ? [ + i === 1 + ? { + id: '123', + attributes: { + persisted: false, + status: SearchSessionStatus.IN_PROGRESS, + created: moment().subtract(moment.duration(3, 'm')), + touched: moment().subtract(moment.duration(2, 'm')), + idMapping: { + 'map-key': { + strategy: ENHANCED_ES_SEARCH_STRATEGY, + id: 'async-id', + status: SearchStatus.IN_PROGRESS, + }, + }, + }, + } + : emptySO, + emptySO, + emptySO, + emptySO, + emptySO, + ] + : [], + total: 25, + page: i, + }); + }); + + await checkSearchSessionsByPage( + checkFn, + { + savedObjectsClient, + client: mockClient, + logger: mockLogger, + }, + config, + [] + ) + .toPromise() + .catch(() => {}); + + expect(checkFn).toHaveBeenCalledTimes(2); + }); + + test('fetching is abortable', async () => { + let i = 0; + const abort$ = new Subject(); + + const checkFn = jest.fn().mockImplementation(() => { + if (++i === 2) { + abort$.next(); + } + + return of({ + saved_objects: i <= 5 ? [emptySO, emptySO, emptySO, emptySO, emptySO] : [], + total: 25, + page: i, + }); + }); + + await checkSearchSessionsByPage( + checkFn, + { + savedObjectsClient, + client: mockClient, + logger: mockLogger, + }, + config, + [] + ) + .pipe(takeUntil(abort$)) + .toPromise() + .catch(() => {}); + + jest.runAllTimers(); + + // if not for `abort$` then this would be called 6 times! + expect(checkFn).toHaveBeenCalledTimes(2); + }); + }); +}); diff --git a/x-pack/plugins/data_enhanced/server/search/session/get_search_session_page.ts b/x-pack/plugins/data_enhanced/server/search/session/get_search_session_page.ts new file mode 100644 index 0000000000000..74306bac39f7d --- /dev/null +++ b/x-pack/plugins/data_enhanced/server/search/session/get_search_session_page.ts @@ -0,0 +1,61 @@ +/* + * 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 { SavedObjectsClientContract, Logger } from 'kibana/server'; +import { from, Observable, EMPTY } from 'rxjs'; +import { concatMap } from 'rxjs/operators'; +import { + SearchSessionSavedObjectAttributes, + SEARCH_SESSION_TYPE, + KueryNode, +} from '../../../../../../src/plugins/data/common'; +import { CheckSearchSessionsDeps, CheckSearchSessionsFn, SearchSessionsConfig } from './types'; + +export interface GetSessionsDeps { + savedObjectsClient: SavedObjectsClientContract; + logger: Logger; +} + +export function getSearchSessionsPage$( + { savedObjectsClient }: GetSessionsDeps, + filter: KueryNode, + pageSize: number, + page: number +) { + return from( + savedObjectsClient.find({ + page, + perPage: pageSize, + type: SEARCH_SESSION_TYPE, + namespaces: ['*'], + // process older sessions first + sortField: 'touched', + sortOrder: 'asc', + filter, + }) + ); +} + +export const checkSearchSessionsByPage = ( + checkFn: CheckSearchSessionsFn, + deps: CheckSearchSessionsDeps, + config: SearchSessionsConfig, + filters: any, + nextPage = 1 +): Observable => + checkFn(deps, config, filters, nextPage).pipe( + concatMap((result) => { + if (!result || !result.saved_objects || result.saved_objects.length < config.pageSize) { + return EMPTY; + } else { + // TODO: while processing previous page session list might have been changed and we might skip a session, + // because it would appear now on a different "page". + // This isn't critical, as we would pick it up on a next task iteration, but maybe we could improve this somehow + return checkSearchSessionsByPage(checkFn, deps, config, filters, result.page + 1); + } + }) + ); diff --git a/x-pack/plugins/data_enhanced/server/search/session/index.ts b/x-pack/plugins/data_enhanced/server/search/session/index.ts index deadeb3f8f07a..1e6841211bb66 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/index.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/index.ts @@ -6,4 +6,3 @@ */ export * from './session_service'; -export { registerSearchSessionsTask, scheduleSearchSessionsTasks } from './monitoring_task'; diff --git a/x-pack/plugins/data_enhanced/server/search/session/monitoring_task.ts b/x-pack/plugins/data_enhanced/server/search/session/monitoring_task.ts deleted file mode 100644 index 7b7b1412987be..0000000000000 --- a/x-pack/plugins/data_enhanced/server/search/session/monitoring_task.ts +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { Duration } from 'moment'; -import { filter, takeUntil } from 'rxjs/operators'; -import { BehaviorSubject } from 'rxjs'; -import { - TaskManagerSetupContract, - TaskManagerStartContract, - RunContext, - TaskRunCreatorFunction, -} from '../../../../task_manager/server'; -import { checkRunningSessions } from './check_running_sessions'; -import { CoreSetup, SavedObjectsClient, Logger } from '../../../../../../src/core/server'; -import { ConfigSchema } from '../../../config'; -import { SEARCH_SESSION_TYPE } from '../../../../../../src/plugins/data/common'; -import { DataEnhancedStartDependencies } from '../../type'; - -export const SEARCH_SESSIONS_TASK_TYPE = 'search_sessions_monitor'; -export const SEARCH_SESSIONS_TASK_ID = `data_enhanced_${SEARCH_SESSIONS_TASK_TYPE}`; - -interface SearchSessionTaskDeps { - taskManager: TaskManagerSetupContract; - logger: Logger; - config: ConfigSchema; -} - -function searchSessionRunner( - core: CoreSetup, - { logger, config }: SearchSessionTaskDeps -): TaskRunCreatorFunction { - return ({ taskInstance }: RunContext) => { - const aborted$ = new BehaviorSubject(false); - return { - async run() { - const sessionConfig = config.search.sessions; - const [coreStart] = await core.getStartServices(); - if (!sessionConfig.enabled) { - logger.debug('Search sessions are disabled. Skipping task.'); - return; - } - if (aborted$.getValue()) return; - - const internalRepo = coreStart.savedObjects.createInternalRepository([SEARCH_SESSION_TYPE]); - const internalSavedObjectsClient = new SavedObjectsClient(internalRepo); - await checkRunningSessions( - { - savedObjectsClient: internalSavedObjectsClient, - client: coreStart.elasticsearch.client.asInternalUser, - logger, - }, - sessionConfig - ) - .pipe(takeUntil(aborted$.pipe(filter((aborted) => aborted)))) - .toPromise(); - - return { - state: {}, - }; - }, - cancel: async () => { - aborted$.next(true); - }, - }; - }; -} - -export function registerSearchSessionsTask( - core: CoreSetup, - deps: SearchSessionTaskDeps -) { - deps.taskManager.registerTaskDefinitions({ - [SEARCH_SESSIONS_TASK_TYPE]: { - title: 'Search Sessions Monitor', - createTaskRunner: searchSessionRunner(core, deps), - timeout: `${deps.config.search.sessions.monitoringTaskTimeout.asSeconds()}s`, - }, - }); -} - -export async function unscheduleSearchSessionsTask( - taskManager: TaskManagerStartContract, - logger: Logger -) { - try { - await taskManager.removeIfExists(SEARCH_SESSIONS_TASK_ID); - logger.debug(`Search sessions cleared`); - } catch (e) { - logger.error(`Error clearing task, received ${e.message}`); - } -} - -export async function scheduleSearchSessionsTasks( - taskManager: TaskManagerStartContract, - logger: Logger, - trackingInterval: Duration -) { - await taskManager.removeIfExists(SEARCH_SESSIONS_TASK_ID); - - try { - await taskManager.ensureScheduled({ - id: SEARCH_SESSIONS_TASK_ID, - taskType: SEARCH_SESSIONS_TASK_TYPE, - schedule: { - interval: `${trackingInterval.asSeconds()}s`, - }, - state: {}, - params: {}, - }); - - logger.debug(`Search sessions task, scheduled to run`); - } catch (e) { - logger.error(`Error scheduling task, received ${e.message}`); - } -} diff --git a/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts b/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts index 374dbee2384d5..dd1eafa5d60f8 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts @@ -79,7 +79,9 @@ describe('SearchSessionService', () => { maxUpdateRetries: MAX_UPDATE_RETRIES, defaultExpiration: moment.duration(7, 'd'), monitoringTaskTimeout: moment.duration(5, 'm'), + cleanupInterval: moment.duration(10, 's'), trackingInterval: moment.duration(10, 's'), + expireInterval: moment.duration(10, 'm'), management: {} as any, }, }, @@ -157,7 +159,9 @@ describe('SearchSessionService', () => { maxUpdateRetries: MAX_UPDATE_RETRIES, defaultExpiration: moment.duration(7, 'd'), trackingInterval: moment.duration(10, 's'), + expireInterval: moment.duration(10, 'm'), monitoringTaskTimeout: moment.duration(5, 'm'), + cleanupInterval: moment.duration(10, 's'), management: {} as any, }, }, diff --git a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts index 81a12f607935d..0998c1f42e183 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts @@ -43,11 +43,26 @@ import { createRequestHash } from './utils'; import { ConfigSchema } from '../../../config'; import { registerSearchSessionsTask, - scheduleSearchSessionsTasks, + scheduleSearchSessionsTask, unscheduleSearchSessionsTask, -} from './monitoring_task'; +} from './setup_task'; import { SearchSessionsConfig, SearchStatus } from './types'; import { DataEnhancedStartDependencies } from '../../type'; +import { + checkPersistedSessionsProgress, + SEARCH_SESSIONS_TASK_ID, + SEARCH_SESSIONS_TASK_TYPE, +} from './check_persisted_sessions'; +import { + SEARCH_SESSIONS_CLEANUP_TASK_TYPE, + checkNonPersistedSessions, + SEARCH_SESSIONS_CLEANUP_TASK_ID, +} from './check_non_persiseted_sessions'; +import { + SEARCH_SESSIONS_EXPIRE_TASK_TYPE, + SEARCH_SESSIONS_EXPIRE_TASK_ID, + checkPersistedCompletedSessionExpiration, +} from './expire_persisted_sessions'; export interface SearchSessionDependencies { savedObjectsClient: SavedObjectsClientContract; @@ -89,11 +104,35 @@ export class SearchSessionService } public setup(core: CoreSetup, deps: SetupDependencies) { - registerSearchSessionsTask(core, { + const taskDeps = { config: this.config, taskManager: deps.taskManager, logger: this.logger, - }); + }; + + registerSearchSessionsTask( + core, + taskDeps, + SEARCH_SESSIONS_TASK_TYPE, + 'persisted session progress', + checkPersistedSessionsProgress + ); + + registerSearchSessionsTask( + core, + taskDeps, + SEARCH_SESSIONS_CLEANUP_TASK_TYPE, + 'non persisted session cleanup', + checkNonPersistedSessions + ); + + registerSearchSessionsTask( + core, + taskDeps, + SEARCH_SESSIONS_EXPIRE_TASK_TYPE, + 'complete session expiration', + checkPersistedCompletedSessionExpiration + ); } public async start(core: CoreStart, deps: StartDependencies) { @@ -103,14 +142,37 @@ export class SearchSessionService public stop() {} private setupMonitoring = async (core: CoreStart, deps: StartDependencies) => { + const taskDeps = { + config: this.config, + taskManager: deps.taskManager, + logger: this.logger, + }; + if (this.sessionConfig.enabled) { - scheduleSearchSessionsTasks( - deps.taskManager, - this.logger, + scheduleSearchSessionsTask( + taskDeps, + SEARCH_SESSIONS_TASK_ID, + SEARCH_SESSIONS_TASK_TYPE, this.sessionConfig.trackingInterval ); + + scheduleSearchSessionsTask( + taskDeps, + SEARCH_SESSIONS_CLEANUP_TASK_ID, + SEARCH_SESSIONS_CLEANUP_TASK_TYPE, + this.sessionConfig.cleanupInterval + ); + + scheduleSearchSessionsTask( + taskDeps, + SEARCH_SESSIONS_EXPIRE_TASK_ID, + SEARCH_SESSIONS_EXPIRE_TASK_TYPE, + this.sessionConfig.expireInterval + ); } else { - unscheduleSearchSessionsTask(deps.taskManager, this.logger); + unscheduleSearchSessionsTask(taskDeps, SEARCH_SESSIONS_TASK_ID); + unscheduleSearchSessionsTask(taskDeps, SEARCH_SESSIONS_CLEANUP_TASK_ID); + unscheduleSearchSessionsTask(taskDeps, SEARCH_SESSIONS_EXPIRE_TASK_ID); } }; diff --git a/x-pack/plugins/data_enhanced/server/search/session/setup_task.ts b/x-pack/plugins/data_enhanced/server/search/session/setup_task.ts new file mode 100644 index 0000000000000..a4c9b6039ff64 --- /dev/null +++ b/x-pack/plugins/data_enhanced/server/search/session/setup_task.ts @@ -0,0 +1,121 @@ +/* + * 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 { Duration } from 'moment'; +import { filter, takeUntil } from 'rxjs/operators'; +import { BehaviorSubject } from 'rxjs'; +import { RunContext, TaskRunCreatorFunction } from '../../../../task_manager/server'; +import { CoreSetup, SavedObjectsClient } from '../../../../../../src/core/server'; +import { SEARCH_SESSION_TYPE } from '../../../../../../src/plugins/data/common'; +import { DataEnhancedStartDependencies } from '../../type'; +import { + SearchSessionTaskSetupDeps, + SearchSessionTaskStartDeps, + SearchSessionTaskFn, +} from './types'; + +export function searchSessionTaskRunner( + core: CoreSetup, + deps: SearchSessionTaskSetupDeps, + title: string, + checkFn: SearchSessionTaskFn +): TaskRunCreatorFunction { + const { logger, config } = deps; + return ({ taskInstance }: RunContext) => { + const aborted$ = new BehaviorSubject(false); + return { + async run() { + try { + const sessionConfig = config.search.sessions; + const [coreStart] = await core.getStartServices(); + if (!sessionConfig.enabled) { + logger.debug(`Search sessions are disabled. Skipping task ${title}.`); + return; + } + if (aborted$.getValue()) return; + + const internalRepo = coreStart.savedObjects.createInternalRepository([ + SEARCH_SESSION_TYPE, + ]); + const internalSavedObjectsClient = new SavedObjectsClient(internalRepo); + await checkFn( + { + logger, + client: coreStart.elasticsearch.client.asInternalUser, + savedObjectsClient: internalSavedObjectsClient, + }, + sessionConfig + ) + .pipe(takeUntil(aborted$.pipe(filter((aborted) => aborted)))) + .toPromise(); + + return { + state: {}, + }; + } catch (e) { + logger.error(`An error occurred. Skipping task ${title}.`); + } + }, + cancel: async () => { + aborted$.next(true); + }, + }; + }; +} + +export function registerSearchSessionsTask( + core: CoreSetup, + deps: SearchSessionTaskSetupDeps, + taskType: string, + title: string, + checkFn: SearchSessionTaskFn +) { + deps.taskManager.registerTaskDefinitions({ + [taskType]: { + title, + createTaskRunner: searchSessionTaskRunner(core, deps, title, checkFn), + timeout: `${deps.config.search.sessions.monitoringTaskTimeout.asSeconds()}s`, + }, + }); +} + +export async function unscheduleSearchSessionsTask( + { taskManager, logger }: SearchSessionTaskStartDeps, + taskId: string +) { + try { + await taskManager.removeIfExists(taskId); + logger.debug(`${taskId} cleared`); + } catch (e) { + logger.error(`${taskId} Error clearing task ${e.message}`); + } +} + +export async function scheduleSearchSessionsTask( + { taskManager, logger }: SearchSessionTaskStartDeps, + taskId: string, + taskType: string, + interval: Duration +) { + await taskManager.removeIfExists(taskId); + + try { + await taskManager.ensureScheduled({ + id: taskId, + taskType, + schedule: { + interval: `${interval.asSeconds()}s`, + }, + state: {}, + params: {}, + }); + + logger.debug(`${taskId} scheduled to run`); + } catch (e) { + logger.error(`${taskId} Error scheduling task ${e.message}`); + } +} diff --git a/x-pack/plugins/data_enhanced/server/search/session/types.ts b/x-pack/plugins/data_enhanced/server/search/session/types.ts index 0fa384e55f7d7..eadc3821c1043 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/types.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/types.ts @@ -5,6 +5,18 @@ * 2.0. */ +import { + ElasticsearchClient, + Logger, + SavedObjectsClientContract, + SavedObjectsFindResponse, +} from 'kibana/server'; +import { Observable } from 'rxjs'; +import { KueryNode, SearchSessionSavedObjectAttributes } from 'src/plugins/data/common'; +import { + TaskManagerSetupContract, + TaskManagerStartContract, +} from '../../../../../../x-pack/plugins/task_manager/server'; import { ConfigSchema } from '../../../config'; export enum SearchStatus { @@ -14,3 +26,38 @@ export enum SearchStatus { } export type SearchSessionsConfig = ConfigSchema['search']['sessions']; + +export interface CheckSearchSessionsDeps { + savedObjectsClient: SavedObjectsClientContract; + client: ElasticsearchClient; + logger: Logger; +} + +export interface SearchSessionTaskSetupDeps { + taskManager: TaskManagerSetupContract; + logger: Logger; + config: ConfigSchema; +} + +export interface SearchSessionTaskStartDeps { + taskManager: TaskManagerStartContract; + logger: Logger; + config: ConfigSchema; +} + +export type SearchSessionTaskFn = ( + deps: CheckSearchSessionsDeps, + config: SearchSessionsConfig +) => Observable; + +export type SearchSessionsResponse = SavedObjectsFindResponse< + SearchSessionSavedObjectAttributes, + unknown +>; + +export type CheckSearchSessionsFn = ( + deps: CheckSearchSessionsDeps, + config: SearchSessionsConfig, + filter: KueryNode, + page: number +) => Observable; diff --git a/x-pack/plugins/data_enhanced/server/search/session/update_session_status.test.ts b/x-pack/plugins/data_enhanced/server/search/session/update_session_status.test.ts new file mode 100644 index 0000000000000..485a30fd54951 --- /dev/null +++ b/x-pack/plugins/data_enhanced/server/search/session/update_session_status.test.ts @@ -0,0 +1,323 @@ +/* + * 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 { bulkUpdateSessions, updateSessionStatus } from './update_session_status'; +import { + SearchSessionStatus, + SearchSessionSavedObjectAttributes, +} from '../../../../../../src/plugins/data/common'; +import { savedObjectsClientMock } from '../../../../../../src/core/server/mocks'; +import { SearchStatus } from './types'; +import moment from 'moment'; +import { + SavedObjectsBulkUpdateObject, + SavedObjectsClientContract, + SavedObjectsFindResult, +} from '../../../../../../src/core/server'; + +describe('bulkUpdateSessions', () => { + let mockClient: any; + let savedObjectsClient: jest.Mocked; + const mockLogger: any = { + debug: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }; + + beforeEach(() => { + savedObjectsClient = savedObjectsClientMock.create(); + mockClient = { + asyncSearch: { + status: jest.fn(), + delete: jest.fn(), + }, + eql: { + status: jest.fn(), + delete: jest.fn(), + }, + }; + }); + + describe('updateSessionStatus', () => { + test('updates expired session', async () => { + const so: SavedObjectsFindResult = { + id: '123', + attributes: { + persisted: false, + status: SearchSessionStatus.IN_PROGRESS, + expires: moment().subtract(moment.duration(5, 'd')), + idMapping: { + 'search-hash': { + id: 'search-id', + strategy: 'cool', + status: SearchStatus.IN_PROGRESS, + }, + }, + }, + } as any; + + const updated = await updateSessionStatus( + { + savedObjectsClient, + client: mockClient, + logger: mockLogger, + }, + so + ); + + expect(updated).toBeTruthy(); + expect(so.attributes.status).toBe(SearchSessionStatus.EXPIRED); + }); + + test('does nothing if the search is still running', async () => { + const so = { + id: '123', + attributes: { + persisted: false, + status: SearchSessionStatus.IN_PROGRESS, + created: moment().subtract(moment.duration(3, 'm')), + touched: moment().subtract(moment.duration(10, 's')), + expires: moment().add(moment.duration(5, 'd')), + idMapping: { + 'search-hash': { + id: 'search-id', + strategy: 'cool', + status: SearchStatus.IN_PROGRESS, + }, + }, + }, + } as any; + + mockClient.asyncSearch.status.mockResolvedValue({ + body: { + is_partial: true, + is_running: true, + }, + }); + + const updated = await updateSessionStatus( + { + savedObjectsClient, + client: mockClient, + logger: mockLogger, + }, + so + ); + + expect(updated).toBeFalsy(); + expect(so.attributes.status).toBe(SearchSessionStatus.IN_PROGRESS); + }); + + test("doesn't re-check completed or errored searches", async () => { + const so = { + id: '123', + attributes: { + expires: moment().add(moment.duration(5, 'd')), + status: SearchSessionStatus.ERROR, + idMapping: { + 'search-hash': { + id: 'search-id', + strategy: 'cool', + status: SearchStatus.COMPLETE, + }, + 'another-search-hash': { + id: 'search-id', + strategy: 'cool', + status: SearchStatus.ERROR, + }, + }, + }, + } as any; + + const updated = await updateSessionStatus( + { + savedObjectsClient, + client: mockClient, + logger: mockLogger, + }, + so + ); + + expect(updated).toBeFalsy(); + expect(mockClient.asyncSearch.status).not.toBeCalled(); + }); + + test('updates to complete if the search is done', async () => { + savedObjectsClient.bulkUpdate = jest.fn(); + const so = { + attributes: { + status: SearchSessionStatus.IN_PROGRESS, + touched: '123', + expires: moment().add(moment.duration(5, 'd')), + idMapping: { + 'search-hash': { + id: 'search-id', + strategy: 'cool', + status: SearchStatus.IN_PROGRESS, + }, + }, + }, + } as any; + mockClient.asyncSearch.status.mockResolvedValue({ + body: { + is_partial: false, + is_running: false, + completion_status: 200, + }, + }); + + const updated = await updateSessionStatus( + { + savedObjectsClient, + client: mockClient, + logger: mockLogger, + }, + so + ); + + expect(updated).toBeTruthy(); + + expect(mockClient.asyncSearch.status).toBeCalledWith({ id: 'search-id' }); + expect(so.attributes.status).toBe(SearchSessionStatus.COMPLETE); + expect(so.attributes.status).toBe(SearchSessionStatus.COMPLETE); + expect(so.attributes.touched).not.toBe('123'); + expect(so.attributes.completed).not.toBeUndefined(); + expect(so.attributes.idMapping['search-hash'].status).toBe(SearchStatus.COMPLETE); + expect(so.attributes.idMapping['search-hash'].error).toBeUndefined(); + }); + + test('updates to error if the search is errored', async () => { + savedObjectsClient.bulkUpdate = jest.fn(); + const so = { + attributes: { + expires: moment().add(moment.duration(5, 'd')), + idMapping: { + 'search-hash': { + id: 'search-id', + strategy: 'cool', + status: SearchStatus.IN_PROGRESS, + }, + }, + }, + } as any; + + mockClient.asyncSearch.status.mockResolvedValue({ + body: { + is_partial: false, + is_running: false, + completion_status: 500, + }, + }); + + const updated = await updateSessionStatus( + { + savedObjectsClient, + client: mockClient, + logger: mockLogger, + }, + so + ); + + expect(updated).toBeTruthy(); + expect(so.attributes.status).toBe(SearchSessionStatus.ERROR); + expect(so.attributes.touched).not.toBe('123'); + expect(so.attributes.idMapping['search-hash'].status).toBe(SearchStatus.ERROR); + expect(so.attributes.idMapping['search-hash'].error).toBe( + 'Search completed with a 500 status' + ); + }); + }); + + describe('bulkUpdateSessions', () => { + test('does nothing if there are no open sessions', async () => { + await bulkUpdateSessions( + { + savedObjectsClient, + client: mockClient, + logger: mockLogger, + }, + [] + ); + + expect(savedObjectsClient.bulkUpdate).not.toBeCalled(); + expect(savedObjectsClient.delete).not.toBeCalled(); + }); + + test('updates in space', async () => { + const so = { + namespaces: ['awesome'], + attributes: { + expires: moment().add(moment.duration(5, 'd')), + status: SearchSessionStatus.IN_PROGRESS, + touched: '123', + idMapping: { + 'search-hash': { + id: 'search-id', + strategy: 'cool', + status: SearchStatus.IN_PROGRESS, + }, + }, + }, + } as any; + + savedObjectsClient.bulkUpdate = jest.fn().mockResolvedValue({ + saved_objects: [so], + }); + + await bulkUpdateSessions( + { + savedObjectsClient, + client: mockClient, + logger: mockLogger, + }, + [so] + ); + + const [updateInput] = savedObjectsClient.bulkUpdate.mock.calls[0]; + const updatedAttributes = updateInput[0] as SavedObjectsBulkUpdateObject; + expect(updatedAttributes.namespace).toBe('awesome'); + }); + + test('logs failures', async () => { + const so = { + namespaces: ['awesome'], + attributes: { + expires: moment().add(moment.duration(5, 'd')), + status: SearchSessionStatus.IN_PROGRESS, + touched: '123', + idMapping: { + 'search-hash': { + id: 'search-id', + strategy: 'cool', + status: SearchStatus.IN_PROGRESS, + }, + }, + }, + } as any; + + savedObjectsClient.bulkUpdate = jest.fn().mockResolvedValue({ + saved_objects: [ + { + error: 'nope', + }, + ], + }); + + await bulkUpdateSessions( + { + savedObjectsClient, + client: mockClient, + logger: mockLogger, + }, + [so] + ); + + expect(savedObjectsClient.bulkUpdate).toBeCalledTimes(1); + expect(mockLogger.error).toBeCalledTimes(1); + }); + }); +}); diff --git a/x-pack/plugins/data_enhanced/server/search/session/update_session_status.ts b/x-pack/plugins/data_enhanced/server/search/session/update_session_status.ts new file mode 100644 index 0000000000000..1c484467bef63 --- /dev/null +++ b/x-pack/plugins/data_enhanced/server/search/session/update_session_status.ts @@ -0,0 +1,128 @@ +/* + * 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 { SavedObjectsFindResult, SavedObjectsUpdateResponse } from 'kibana/server'; +import { + SearchSessionRequestInfo, + SearchSessionSavedObjectAttributes, + SearchSessionStatus, +} from '../../../../../../src/plugins/data/common'; +import { getSearchStatus } from './get_search_status'; +import { getSessionStatus } from './get_session_status'; +import { CheckSearchSessionsDeps, SearchSessionsResponse, SearchStatus } from './types'; +import { isSearchSessionExpired } from './utils'; + +export async function updateSessionStatus( + { logger, client }: CheckSearchSessionsDeps, + session: SavedObjectsFindResult +) { + let sessionUpdated = false; + const isExpired = isSearchSessionExpired(session); + + if (!isExpired) { + // Check statuses of all running searches + await Promise.all( + Object.keys(session.attributes.idMapping).map(async (searchKey: string) => { + const updateSearchRequest = ( + currentStatus: Pick + ) => { + sessionUpdated = true; + session.attributes.idMapping[searchKey] = { + ...session.attributes.idMapping[searchKey], + ...currentStatus, + }; + }; + + const searchInfo = session.attributes.idMapping[searchKey]; + if (searchInfo.status === SearchStatus.IN_PROGRESS) { + try { + const currentStatus = await getSearchStatus(client, searchInfo.id); + + if (currentStatus.status !== searchInfo.status) { + logger.debug(`search ${searchInfo.id} | status changed to ${currentStatus.status}`); + updateSearchRequest(currentStatus); + } + } catch (e) { + logger.error(e); + updateSearchRequest({ + status: SearchStatus.ERROR, + error: e.message || e.meta.error?.caused_by?.reason, + }); + } + } + }) + ); + } + + // And only then derive the session's status + const sessionStatus = isExpired + ? SearchSessionStatus.EXPIRED + : getSessionStatus(session.attributes); + if (sessionStatus !== session.attributes.status) { + const now = new Date().toISOString(); + session.attributes.status = sessionStatus; + session.attributes.touched = now; + if (sessionStatus === SearchSessionStatus.COMPLETE) { + session.attributes.completed = now; + } else if (session.attributes.completed) { + session.attributes.completed = null; + } + sessionUpdated = true; + } + + return sessionUpdated; +} + +export async function getAllSessionsStatusUpdates( + deps: CheckSearchSessionsDeps, + searchSessions: SearchSessionsResponse +) { + const updatedSessions = new Array>(); + + await Promise.all( + searchSessions.saved_objects.map(async (session) => { + const updated = await updateSessionStatus(deps, session); + + if (updated) { + updatedSessions.push(session); + } + }) + ); + + return updatedSessions; +} + +export async function bulkUpdateSessions( + { logger, savedObjectsClient }: CheckSearchSessionsDeps, + updatedSessions: Array> +) { + if (updatedSessions.length) { + // If there's an error, we'll try again in the next iteration, so there's no need to check the output. + const updatedResponse = await savedObjectsClient.bulkUpdate( + updatedSessions.map((session) => ({ + ...session, + namespace: session.namespaces?.[0], + })) + ); + + const success: Array> = []; + const fail: Array> = []; + + updatedResponse.saved_objects.forEach((savedObjectResponse) => { + if ('error' in savedObjectResponse) { + fail.push(savedObjectResponse); + logger.error( + `Error while updating search session ${savedObjectResponse?.id}: ${savedObjectResponse.error?.message}` + ); + } else { + success.push(savedObjectResponse); + } + }); + + logger.debug(`Updating search sessions: success: ${success.length}, fail: ${fail.length}`); + } +} diff --git a/x-pack/plugins/data_enhanced/server/search/session/utils.ts b/x-pack/plugins/data_enhanced/server/search/session/utils.ts index 7b1f1a7564626..55c875602694f 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/utils.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/utils.ts @@ -7,6 +7,9 @@ import { createHash } from 'crypto'; import stringify from 'json-stable-stringify'; +import { SavedObjectsFindResult } from 'kibana/server'; +import moment from 'moment'; +import { SearchSessionSavedObjectAttributes } from 'src/plugins/data/common'; /** * Generate the hash for this request so that, in the future, this hash can be used to look up @@ -17,3 +20,9 @@ export function createRequestHash(keys: Record) { const { preference, ...params } = keys; return createHash(`sha256`).update(stringify(params)).digest('hex'); } + +export function isSearchSessionExpired( + session: SavedObjectsFindResult +) { + return moment(session.attributes.expires).isBefore(moment()); +} diff --git a/x-pack/test/api_integration/config.ts b/x-pack/test/api_integration/config.ts index 6708a6d55f402..550148531e2ec 100644 --- a/x-pack/test/api_integration/config.ts +++ b/x-pack/test/api_integration/config.ts @@ -33,6 +33,7 @@ export async function getApiIntegrationConfig({ readConfigFile }: FtrConfigProvi '--xpack.data_enhanced.search.sessions.enabled=true', // enable WIP send to background UI '--xpack.data_enhanced.search.sessions.notTouchedTimeout=15s', // shorten notTouchedTimeout for quicker testing '--xpack.data_enhanced.search.sessions.trackingInterval=5s', // shorten trackingInterval for quicker testing + '--xpack.data_enhanced.search.sessions.cleanupInterval=5s', // shorten cleanupInterval for quicker testing ], }, esTestCluster: { From 6a1e4b8d4de7f50b3f2e42d2d44381ff120fd37a Mon Sep 17 00:00:00 2001 From: Robert Oskamp Date: Thu, 24 Jun 2021 10:58:18 +0200 Subject: [PATCH 04/69] [ML] Functional tests - fix and re-enable close_jobs API tests (#103114) This PR fixes and re-enables the close_jobs API tests after a backend change. --- .../apis/ml/jobs/close_jobs.ts | 273 ++++++++---------- 1 file changed, 124 insertions(+), 149 deletions(-) diff --git a/x-pack/test/api_integration/apis/ml/jobs/close_jobs.ts b/x-pack/test/api_integration/apis/ml/jobs/close_jobs.ts index 4c639d3a166cd..40485205f9fb5 100644 --- a/x-pack/test/api_integration/apis/ml/jobs/close_jobs.ts +++ b/x-pack/test/api_integration/apis/ml/jobs/close_jobs.ts @@ -20,68 +20,6 @@ export default ({ getService }: FtrProviderContext) => { const testSetupJobConfigs = [SINGLE_METRIC_JOB_CONFIG, MULTI_METRIC_JOB_CONFIG]; - const testDataList = [ - { - testTitle: 'as ML Poweruser', - user: USER.ML_POWERUSER, - requestBody: { - jobIds: [SINGLE_METRIC_JOB_CONFIG.job_id, MULTI_METRIC_JOB_CONFIG.job_id], - }, - expected: { - responseCode: 200, - responseBody: { - [SINGLE_METRIC_JOB_CONFIG.job_id]: { closed: true }, - [MULTI_METRIC_JOB_CONFIG.job_id]: { closed: true }, - }, - }, - }, - ]; - - const testDataListFailed = [ - { - testTitle: 'as ML Poweruser', - user: USER.ML_POWERUSER, - requestBody: { - jobIds: [SINGLE_METRIC_JOB_CONFIG.job_id, MULTI_METRIC_JOB_CONFIG.job_id], - }, - expected: { - responseCode: 200, - - responseBody: { - [SINGLE_METRIC_JOB_CONFIG.job_id]: { closed: false, error: { status: 409 } }, - [MULTI_METRIC_JOB_CONFIG.job_id]: { closed: false, error: { status: 409 } }, - }, - }, - }, - ]; - - const testDataListUnauthorized = [ - { - testTitle: 'as ML Unauthorized user', - user: USER.ML_UNAUTHORIZED, - requestBody: { - jobIds: [SINGLE_METRIC_JOB_CONFIG.job_id, MULTI_METRIC_JOB_CONFIG.job_id], - }, - // Note that the jobs and datafeeds are loaded async so the actual error message is not deterministic. - expected: { - responseCode: 403, - error: 'Forbidden', - }, - }, - { - testTitle: 'as ML Viewer', - user: USER.ML_VIEWER, - requestBody: { - jobIds: [SINGLE_METRIC_JOB_CONFIG.job_id, MULTI_METRIC_JOB_CONFIG.job_id], - }, - // Note that the jobs and datafeeds are loaded async so the actual error message is not deterministic. - expected: { - responseCode: 403, - error: 'Forbidden', - }, - }, - ]; - async function runCloseJobsRequest( user: USER, requestBody: object, @@ -97,19 +35,22 @@ export default ({ getService }: FtrProviderContext) => { return body; } - // failing ES snapshot promotion after backend change, see https://github.com/elastic/kibana/issues/103023 - describe.skip('close_jobs', function () { + async function startDatafeedsInRealtime() { + for (const job of testSetupJobConfigs) { + const datafeedId = `datafeed-${job.job_id}`; + await ml.api.startDatafeed(datafeedId, { start: '0' }); + await ml.api.waitForDatafeedState(datafeedId, DATAFEED_STATE.STARTED); + } + } + + describe('close_jobs', function () { before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); await ml.testResources.createIndexPatternIfNeeded('ft_farequote', '@timestamp'); await ml.testResources.setKibanaTimeZoneToUTC(); }); - after(async () => { - await ml.api.cleanMlIndices(); - }); - - it('sets up jobs', async () => { + beforeEach(async () => { for (const job of testSetupJobConfigs) { const datafeedId = `datafeed-${job.job_id}`; await ml.api.createAnomalyDetectionJob(job); @@ -119,98 +60,132 @@ export default ({ getService }: FtrProviderContext) => { datafeed_id: datafeedId, job_id: job.job_id, }); - await ml.api.startDatafeed(datafeedId, { start: '0' }); - await ml.api.waitForDatafeedState(datafeedId, DATAFEED_STATE.STARTED); } }); - describe('rejects request', function () { - for (const testData of testDataListUnauthorized) { - describe('fails to close job ID supplied', function () { - it(`${testData.testTitle}`, async () => { - const body = await runCloseJobsRequest( - testData.user, - testData.requestBody, - testData.expected.responseCode - ); - - expect(body).to.have.property('error').eql(testData.expected.error); - - // ensure jobs are still open - for (const id of testData.requestBody.jobIds) { - await ml.api.waitForJobState(id, JOB_STATE.OPENED); - } - }); - }); + afterEach(async () => { + for (const job of testSetupJobConfigs) { + await ml.api.deleteAnomalyDetectionJobES(job.job_id); } + await ml.api.cleanMlIndices(); }); - describe('close jobs fail because they are running', function () { - for (const testData of testDataListFailed) { - it(`${testData.testTitle}`, async () => { - const body = await runCloseJobsRequest( - testData.user, - testData.requestBody, - testData.expected.responseCode - ); - const expectedResponse = testData.expected.responseBody; - const expectedRspJobIds = Object.keys(expectedResponse).sort((a, b) => - a.localeCompare(b) - ); - const actualRspJobIds = Object.keys(body).sort((a, b) => a.localeCompare(b)); - - expect(actualRspJobIds).to.have.length(expectedRspJobIds.length); - expect(actualRspJobIds).to.eql(expectedRspJobIds); - - expectedRspJobIds.forEach((id) => { - expect(body[id].closed).to.eql(testData.expected.responseBody[id].closed); - expect(body[id].error.status).to.eql(testData.expected.responseBody[id].error.status); - }); - - // ensure jobs are still open - for (const id of testData.requestBody.jobIds) { - await ml.api.waitForJobState(id, JOB_STATE.OPENED); - } - }); + it('rejects request for ML Unauthorized user', async () => { + await startDatafeedsInRealtime(); + + const jobIds = [SINGLE_METRIC_JOB_CONFIG.job_id, MULTI_METRIC_JOB_CONFIG.job_id]; + const body = await runCloseJobsRequest(USER.ML_UNAUTHORIZED, { jobIds }, 403); + + expect(body).to.have.property('error').eql('Forbidden'); + + // ensure jobs are still open + for (const id of jobIds) { + await ml.api.waitForJobState(id, JOB_STATE.OPENED); + } + }); + + it('rejects request for ML Viewer user', async () => { + await startDatafeedsInRealtime(); + + const jobIds = [SINGLE_METRIC_JOB_CONFIG.job_id, MULTI_METRIC_JOB_CONFIG.job_id]; + const body = await runCloseJobsRequest(USER.ML_VIEWER, { jobIds }, 403); + + expect(body).to.have.property('error').eql('Forbidden'); + + // ensure jobs are still open + for (const id of jobIds) { + await ml.api.waitForJobState(id, JOB_STATE.OPENED); + } + }); + + it('succeeds for ML Poweruser with datafeed started', async () => { + await startDatafeedsInRealtime(); + + const jobIds = [SINGLE_METRIC_JOB_CONFIG.job_id, MULTI_METRIC_JOB_CONFIG.job_id]; + const body = await runCloseJobsRequest(USER.ML_POWERUSER, { jobIds }, 200); + + const expectedRspBody = { + [SINGLE_METRIC_JOB_CONFIG.job_id]: { closed: true }, + [MULTI_METRIC_JOB_CONFIG.job_id]: { closed: true }, + }; + const expectedRspJobIds = Object.keys(expectedRspBody).sort((a, b) => a.localeCompare(b)); + const actualRspJobIds = Object.keys(body).sort((a, b) => a.localeCompare(b)); + + expect(actualRspJobIds).to.have.length(expectedRspJobIds.length); + expect(actualRspJobIds).to.eql(expectedRspJobIds); + + expectedRspJobIds.forEach((id) => { + expect(body[id].closed).to.eql(expectedRspBody[id].closed); + }); + + // datafeeds should be stopped automatically + for (const id of jobIds) { + await ml.api.waitForDatafeedState(`datafeed-${id}`, DATAFEED_STATE.STOPPED); + } + + // ensure jobs are actually closed + for (const id of jobIds) { + await ml.api.waitForJobState(id, JOB_STATE.CLOSED); } }); - describe('stops datafeeds', function () { - it('stops datafeeds', async () => { - for (const job of testSetupJobConfigs) { - const datafeedId = `datafeed-${job.job_id}`; - await ml.api.stopDatafeed(datafeedId); - await ml.api.waitForDatafeedState(datafeedId, DATAFEED_STATE.STOPPED); - } + it('succeeds for ML Poweruser with datafeed stopped', async () => { + const jobIds = [SINGLE_METRIC_JOB_CONFIG.job_id, MULTI_METRIC_JOB_CONFIG.job_id]; + const body = await runCloseJobsRequest(USER.ML_POWERUSER, { jobIds }, 200); + + const expectedRspBody = { + [SINGLE_METRIC_JOB_CONFIG.job_id]: { closed: true }, + [MULTI_METRIC_JOB_CONFIG.job_id]: { closed: true }, + }; + const expectedRspJobIds = Object.keys(expectedRspBody).sort((a, b) => a.localeCompare(b)); + const actualRspJobIds = Object.keys(body).sort((a, b) => a.localeCompare(b)); + + expect(actualRspJobIds).to.have.length(expectedRspJobIds.length); + expect(actualRspJobIds).to.eql(expectedRspJobIds); + + expectedRspJobIds.forEach((id) => { + expect(body[id].closed).to.eql(expectedRspBody[id].closed); }); + + // datafeeds should still be stopped + for (const id of jobIds) { + await ml.api.waitForDatafeedState(`datafeed-${id}`, DATAFEED_STATE.STOPPED); + } + + // ensure jobs are actually closed + for (const id of jobIds) { + await ml.api.waitForJobState(id, JOB_STATE.CLOSED); + } }); - describe('close jobs succeed', function () { - for (const testData of testDataList) { - it(`${testData.testTitle}`, async () => { - const body = await runCloseJobsRequest( - testData.user, - testData.requestBody, - testData.expected.responseCode - ); - const expectedResponse = testData.expected.responseBody; - const expectedRspJobIds = Object.keys(expectedResponse).sort((a, b) => - a.localeCompare(b) - ); - const actualRspJobIds = Object.keys(body).sort((a, b) => a.localeCompare(b)); - - expect(actualRspJobIds).to.have.length(expectedRspJobIds.length); - expect(actualRspJobIds).to.eql(expectedRspJobIds); - - expectedRspJobIds.forEach((id) => { - expect(body[id].closed).to.eql(testData.expected.responseBody[id].closed); - }); - - // ensure jobs are now closed - for (const id of testData.requestBody.jobIds) { - await ml.api.waitForJobState(id, JOB_STATE.CLOSED); - } - }); + it('succeeds for ML Poweruser with job already closed', async () => { + const jobIds = [SINGLE_METRIC_JOB_CONFIG.job_id, MULTI_METRIC_JOB_CONFIG.job_id]; + await runCloseJobsRequest(USER.ML_POWERUSER, { jobIds }, 200); + + const body = await runCloseJobsRequest(USER.ML_POWERUSER, { jobIds }, 200); + + const expectedRspBody = { + [SINGLE_METRIC_JOB_CONFIG.job_id]: { closed: true }, + [MULTI_METRIC_JOB_CONFIG.job_id]: { closed: true }, + }; + const expectedRspJobIds = Object.keys(expectedRspBody).sort((a, b) => a.localeCompare(b)); + const actualRspJobIds = Object.keys(body).sort((a, b) => a.localeCompare(b)); + + expect(actualRspJobIds).to.have.length(expectedRspJobIds.length); + expect(actualRspJobIds).to.eql(expectedRspJobIds); + + expectedRspJobIds.forEach((id) => { + expect(body[id].closed).to.eql(expectedRspBody[id].closed); + }); + + // datafeeds should still be stopped + for (const id of jobIds) { + await ml.api.waitForDatafeedState(`datafeed-${id}`, DATAFEED_STATE.STOPPED); + } + + // jobs should still be closed + for (const id of jobIds) { + await ml.api.waitForJobState(id, JOB_STATE.CLOSED); } }); }); From 8298b78a62fddda129e91323e2771b1039929671 Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Thu, 24 Jun 2021 11:23:41 +0200 Subject: [PATCH 05/69] [Discover] Unskip and improve sidebar filter functional test (#102986) --- test/functional/apps/discover/_sidebar.ts | 3 +-- test/functional/page_objects/discover_page.ts | 5 ++++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/test/functional/apps/discover/_sidebar.ts b/test/functional/apps/discover/_sidebar.ts index 8179f4e44e8b8..d8701261126c4 100644 --- a/test/functional/apps/discover/_sidebar.ts +++ b/test/functional/apps/discover/_sidebar.ts @@ -14,8 +14,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'discover', 'timePicker']); const testSubjects = getService('testSubjects'); - // Failing: See https://github.com/elastic/kibana/issues/101449 - describe.skip('discover sidebar', function describeIndexTests() { + describe('discover sidebar', function describeIndexTests() { before(async function () { await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/discover'); diff --git a/test/functional/page_objects/discover_page.ts b/test/functional/page_objects/discover_page.ts index 65b899d2e2fb0..dc3a04568316e 100644 --- a/test/functional/page_objects/discover_page.ts +++ b/test/functional/page_objects/discover_page.ts @@ -448,7 +448,10 @@ export class DiscoverPageObject extends FtrService { public async closeSidebarFieldFilter() { await this.testSubjects.click('toggleFieldFilterButton'); - await this.testSubjects.missingOrFail('filterSelectionPanel'); + + await this.retry.waitFor('sidebar filter closed', async () => { + return !(await this.testSubjects.exists('filterSelectionPanel')); + }); } public async waitForChartLoadingComplete(renderCount: number) { From 507ab0e8d042d80c7ad56160416bc1abf404b8ab Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Thu, 24 Jun 2021 11:29:23 +0200 Subject: [PATCH 06/69] [Discover] Add right permission for unmapped field test in cloud env (#102853) --- .../_indexpattern_with_unmapped_fields.ts | 15 ++++++++++----- test/functional/config.js | 15 +++++++++++++++ 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/test/functional/apps/discover/_indexpattern_with_unmapped_fields.ts b/test/functional/apps/discover/_indexpattern_with_unmapped_fields.ts index e986429a15d26..264885490cdfc 100644 --- a/test/functional/apps/discover/_indexpattern_with_unmapped_fields.ts +++ b/test/functional/apps/discover/_indexpattern_with_unmapped_fields.ts @@ -12,26 +12,31 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); - const log = getService('log'); + const security = getService('security'); const retry = getService('retry'); const PageObjects = getPageObjects(['common', 'timePicker', 'discover']); describe('index pattern with unmapped fields', () => { before(async () => { await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/unmapped_fields'); + await security.testUser.setRoles(['kibana_admin', 'test-index-unmapped-fields']); + const fromTime = 'Jan 20, 2021 @ 00:00:00.000'; + const toTime = 'Jan 25, 2021 @ 00:00:00.000'; + await kibanaServer.uiSettings.replace({ defaultIndex: 'test-index-unmapped-fields', 'discover:searchFieldsFromSource': false, + 'timepicker:timeDefaults': `{ "from": "${fromTime}", "to": "${toTime}"}`, }); - log.debug('discover'); - const fromTime = 'Jan 20, 2021 @ 00:00:00.000'; - const toTime = 'Jan 25, 2021 @ 00:00:00.000'; + await PageObjects.common.navigateToApp('discover'); - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); }); after(async () => { await esArchiver.unload('test/functional/fixtures/es_archiver/unmapped_fields'); + await kibanaServer.uiSettings.unset('defaultIndex'); + await kibanaServer.uiSettings.unset('discover:searchFieldsFromSource'); + await kibanaServer.uiSettings.unset('timepicker:timeDefaults'); }); it('unmapped fields exist on a new saved search', async () => { diff --git a/test/functional/config.js b/test/functional/config.js index bab1148cf372a..b28a9fd36c1c0 100644 --- a/test/functional/config.js +++ b/test/functional/config.js @@ -292,6 +292,21 @@ export default async function ({ readConfigFile }) { kibana: [], }, + 'test-index-unmapped-fields': { + elasticsearch: { + cluster: [], + indices: [ + { + names: ['test-index-unmapped-fields'], + privileges: ['read', 'view_index_metadata'], + field_security: { grant: ['*'], except: [] }, + }, + ], + run_as: [], + }, + kibana: [], + }, + animals: { elasticsearch: { cluster: [], From 1d2cebafd22017c9e6998c675b6010967fe9505a Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Thu, 24 Jun 2021 12:33:35 +0300 Subject: [PATCH 07/69] [VisTypePie] Use a different advanced setting for pie charts (#103049) * Different switch for pie * Remove unused translations Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- docs/management/advanced-options.asciidoc | 3 + .../server/collectors/management/schema.ts | 4 ++ .../server/collectors/management/types.ts | 1 + src/plugins/telemetry/schema/oss_plugins.json | 6 ++ src/plugins/vis_type_pie/common/index.ts | 1 + src/plugins/vis_type_pie/kibana.json | 2 + src/plugins/vis_type_pie/public/plugin.ts | 4 +- src/plugins/vis_type_pie/server/index.ts | 10 ++++ src/plugins/vis_type_pie/server/plugin.ts | 56 +++++++++++++++++++ src/plugins/vis_type_vislib/public/plugin.ts | 24 ++++---- src/plugins/vis_type_xy/common/index.ts | 2 + src/plugins/vis_type_xy/kibana.json | 2 + src/plugins/vis_type_xy/public/plugin.ts | 2 +- src/plugins/vis_type_xy/server/index.ts | 10 ++++ src/plugins/vis_type_xy/server/plugin.ts | 56 +++++++++++++++++++ .../visualizations/common/constants.ts | 1 - src/plugins/visualizations/server/plugin.ts | 23 +------- .../apps/dashboard/dashboard_state.ts | 1 + test/functional/apps/dashboard/index.ts | 2 + .../apps/getting_started/_shakespeare.ts | 1 + test/functional/apps/getting_started/index.ts | 2 + test/functional/apps/visualize/index.ts | 2 + test/functional/config.js | 1 + .../page_objects/visualize_chart_page.ts | 3 +- .../functional/page_objects/visualize_page.ts | 1 + .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - x-pack/test/functional/config.js | 1 + 28 files changed, 183 insertions(+), 42 deletions(-) create mode 100644 src/plugins/vis_type_pie/server/index.ts create mode 100644 src/plugins/vis_type_pie/server/plugin.ts create mode 100644 src/plugins/vis_type_xy/server/index.ts create mode 100644 src/plugins/vis_type_xy/server/plugin.ts diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index 853180ec816e9..66a23ee189ae1 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -482,6 +482,9 @@ of buckets to try to represent. [[visualization-visualize-chartslibrary]]`visualization:visualize:legacyChartsLibrary`:: Enables the legacy charts library for aggregation-based area, line, and bar charts in *Visualize*. +[[visualization-visualize-pieChartslibrary]]`visualization:visualize:legacyPieChartsLibrary`:: +Enables the legacy charts library for aggregation-based pie charts in *Visualize*. + [[visualization-colormapping]]`visualization:colorMapping`:: **This setting is deprecated and will not be supported as of 8.0.** Maps values to specific colors in charts using the *Compatibility* palette. diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts index a6b79a9e2c009..ff637b6686612 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts @@ -396,6 +396,10 @@ export const stackManagementSchema: MakeSchemaFrom = { type: 'boolean', _meta: { description: 'Non-default value of setting.' }, }, + 'visualization:visualize:legacyPieChartsLibrary': { + type: 'boolean', + _meta: { description: 'Non-default value of setting.' }, + }, 'doc_table:legacy': { type: 'boolean', _meta: { description: 'Non-default value of setting.' }, diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts index 8448b359ce607..b59abc3aa7158 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts @@ -26,6 +26,7 @@ export interface UsageStats { 'autocomplete:useTimeRange': boolean; 'search:timeout': number; 'visualization:visualize:legacyChartsLibrary': boolean; + 'visualization:visualize:legacyPieChartsLibrary': boolean; 'doc_table:legacy': boolean; 'discover:modifyColumnsOnSwitch': boolean; 'discover:searchFieldsFromSource': boolean; diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index 99c6dcb40e57d..496335a3b0dc8 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -8594,6 +8594,12 @@ "description": "Non-default value of setting." } }, + "visualization:visualize:legacyPieChartsLibrary": { + "type": "boolean", + "_meta": { + "description": "Non-default value of setting." + } + }, "doc_table:legacy": { "type": "boolean", "_meta": { diff --git a/src/plugins/vis_type_pie/common/index.ts b/src/plugins/vis_type_pie/common/index.ts index 1aa1680530b32..a02a2b2ba10f2 100644 --- a/src/plugins/vis_type_pie/common/index.ts +++ b/src/plugins/vis_type_pie/common/index.ts @@ -7,3 +7,4 @@ */ export const DEFAULT_PERCENT_DECIMALS = 2; +export const LEGACY_PIE_CHARTS_LIBRARY = 'visualization:visualize:legacyPieChartsLibrary'; diff --git a/src/plugins/vis_type_pie/kibana.json b/src/plugins/vis_type_pie/kibana.json index ee312fd19e8d5..eebefc42681b7 100644 --- a/src/plugins/vis_type_pie/kibana.json +++ b/src/plugins/vis_type_pie/kibana.json @@ -2,8 +2,10 @@ "id": "visTypePie", "version": "kibana", "ui": true, + "server": true, "requiredPlugins": ["charts", "data", "expressions", "visualizations", "usageCollection"], "requiredBundles": ["visDefaultEditor"], + "extraPublicDirs": ["common/index"], "owner": { "name": "Kibana App", "githubTeam": "kibana-app" diff --git a/src/plugins/vis_type_pie/public/plugin.ts b/src/plugins/vis_type_pie/public/plugin.ts index 440a3a75a2eb1..787f49c19aca3 100644 --- a/src/plugins/vis_type_pie/public/plugin.ts +++ b/src/plugins/vis_type_pie/public/plugin.ts @@ -12,7 +12,7 @@ import { Plugin as ExpressionsPublicPlugin } from '../../expressions/public'; import { ChartsPluginSetup } from '../../charts/public'; import { UsageCollectionSetup } from '../../usage_collection/public'; import { DataPublicPluginStart } from '../../data/public'; -import { LEGACY_CHARTS_LIBRARY } from '../../visualizations/common/constants'; +import { LEGACY_PIE_CHARTS_LIBRARY } from '../common'; import { pieLabels as pieLabelsExpressionFunction } from './expression_functions/pie_labels'; import { createPieVisFn } from './pie_fn'; import { getPieVisRenderer } from './pie_renderer'; @@ -43,7 +43,7 @@ export class VisTypePiePlugin { core: CoreSetup, { expressions, visualizations, charts, usageCollection }: VisTypePieSetupDependencies ) { - if (!core.uiSettings.get(LEGACY_CHARTS_LIBRARY, false)) { + if (!core.uiSettings.get(LEGACY_PIE_CHARTS_LIBRARY, false)) { const getStartDeps = async () => { const [coreStart, deps] = await core.getStartServices(); return { diff --git a/src/plugins/vis_type_pie/server/index.ts b/src/plugins/vis_type_pie/server/index.ts new file mode 100644 index 0000000000000..201071fbb5fca --- /dev/null +++ b/src/plugins/vis_type_pie/server/index.ts @@ -0,0 +1,10 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { VisTypePieServerPlugin } from './plugin'; + +export const plugin = () => new VisTypePieServerPlugin(); diff --git a/src/plugins/vis_type_pie/server/plugin.ts b/src/plugins/vis_type_pie/server/plugin.ts new file mode 100644 index 0000000000000..48576bdff5d33 --- /dev/null +++ b/src/plugins/vis_type_pie/server/plugin.ts @@ -0,0 +1,56 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import { schema } from '@kbn/config-schema'; + +import { CoreSetup, Plugin, UiSettingsParams } from 'kibana/server'; + +import { LEGACY_PIE_CHARTS_LIBRARY } from '../common'; + +export const getUiSettingsConfig: () => Record> = () => ({ + // TODO: Remove this when vis_type_vislib is removed + // https://github.com/elastic/kibana/issues/56143 + [LEGACY_PIE_CHARTS_LIBRARY]: { + name: i18n.translate('visTypePie.advancedSettings.visualization.legacyPieChartsLibrary.name', { + defaultMessage: 'Pie legacy charts library', + }), + requiresPageReload: true, + value: false, + description: i18n.translate( + 'visTypePie.advancedSettings.visualization.legacyPieChartsLibrary.description', + { + defaultMessage: 'Enables legacy charts library for pie charts in visualize.', + } + ), + deprecation: { + message: i18n.translate( + 'visTypePie.advancedSettings.visualization.legacyPieChartsLibrary.deprecation', + { + defaultMessage: + 'The legacy charts library for pie in visualize is deprecated and will not be supported as of 8.0.', + } + ), + docLinksKey: 'visualizationSettings', + }, + category: ['visualization'], + schema: schema.boolean(), + }, +}); + +export class VisTypePieServerPlugin implements Plugin { + public setup(core: CoreSetup) { + core.uiSettings.register(getUiSettingsConfig()); + + return {}; + } + + public start() { + return {}; + } +} diff --git a/src/plugins/vis_type_vislib/public/plugin.ts b/src/plugins/vis_type_vislib/public/plugin.ts index 52faf8a74778c..cdc02aacafa3b 100644 --- a/src/plugins/vis_type_vislib/public/plugin.ts +++ b/src/plugins/vis_type_vislib/public/plugin.ts @@ -13,7 +13,8 @@ import { VisualizationsSetup } from '../../visualizations/public'; import { ChartsPluginSetup } from '../../charts/public'; import { DataPublicPluginStart } from '../../data/public'; import { KibanaLegacyStart } from '../../kibana_legacy/public'; -import { LEGACY_CHARTS_LIBRARY } from '../../visualizations/common/constants'; +import { LEGACY_CHARTS_LIBRARY } from '../../vis_type_xy/common/index'; +import { LEGACY_PIE_CHARTS_LIBRARY } from '../../vis_type_pie/common/index'; import { createVisTypeVislibVisFn } from './vis_type_vislib_vis_fn'; import { createPieVisFn } from './pie_fn'; @@ -50,17 +51,18 @@ export class VisTypeVislibPlugin core: VisTypeVislibCoreSetup, { expressions, visualizations, charts }: VisTypeVislibPluginSetupDependencies ) { - if (!core.uiSettings.get(LEGACY_CHARTS_LIBRARY, false)) { - // Register only non-replaced vis types - convertedTypeDefinitions.forEach(visualizations.createBaseVisualization); - expressions.registerRenderer(getVislibVisRenderer(core, charts)); - expressions.registerFunction(createVisTypeVislibVisFn()); - } else { - // Register all vis types - visLibVisTypeDefinitions.forEach(visualizations.createBaseVisualization); + const typeDefinitions = !core.uiSettings.get(LEGACY_CHARTS_LIBRARY, false) + ? convertedTypeDefinitions + : visLibVisTypeDefinitions; + // register vislib XY axis charts + typeDefinitions.forEach(visualizations.createBaseVisualization); + expressions.registerRenderer(getVislibVisRenderer(core, charts)); + expressions.registerFunction(createVisTypeVislibVisFn()); + + if (core.uiSettings.get(LEGACY_PIE_CHARTS_LIBRARY, false)) { + // register vislib pie chart visualizations.createBaseVisualization(pieVisTypeDefinition); - expressions.registerRenderer(getVislibVisRenderer(core, charts)); - [createVisTypeVislibVisFn(), createPieVisFn()].forEach(expressions.registerFunction); + expressions.registerFunction(createPieVisFn()); } } diff --git a/src/plugins/vis_type_xy/common/index.ts b/src/plugins/vis_type_xy/common/index.ts index f17bc8476d9a6..a80946f7c62fa 100644 --- a/src/plugins/vis_type_xy/common/index.ts +++ b/src/plugins/vis_type_xy/common/index.ts @@ -19,3 +19,5 @@ export enum ChartType { * Type of xy visualizations */ export type XyVisType = ChartType | 'horizontal_bar'; + +export const LEGACY_CHARTS_LIBRARY = 'visualization:visualize:legacyChartsLibrary'; diff --git a/src/plugins/vis_type_xy/kibana.json b/src/plugins/vis_type_xy/kibana.json index 1d7fd6a0813b4..c25f035fb6d4b 100644 --- a/src/plugins/vis_type_xy/kibana.json +++ b/src/plugins/vis_type_xy/kibana.json @@ -2,8 +2,10 @@ "id": "visTypeXy", "version": "kibana", "ui": true, + "server": true, "requiredPlugins": ["charts", "data", "expressions", "visualizations", "usageCollection"], "requiredBundles": ["kibanaUtils", "visDefaultEditor"], + "extraPublicDirs": ["common/index"], "owner": { "name": "Kibana App", "githubTeam": "kibana-app" diff --git a/src/plugins/vis_type_xy/public/plugin.ts b/src/plugins/vis_type_xy/public/plugin.ts index e8d53127765b4..b595d3172f143 100644 --- a/src/plugins/vis_type_xy/public/plugin.ts +++ b/src/plugins/vis_type_xy/public/plugin.ts @@ -23,7 +23,7 @@ import { } from './services'; import { visTypesDefinitions } from './vis_types'; -import { LEGACY_CHARTS_LIBRARY } from '../../visualizations/common/constants'; +import { LEGACY_CHARTS_LIBRARY } from '../common/'; import { xyVisRenderer } from './vis_renderer'; import * as expressionFunctions from './expression_functions'; diff --git a/src/plugins/vis_type_xy/server/index.ts b/src/plugins/vis_type_xy/server/index.ts new file mode 100644 index 0000000000000..a27ac49c0ea49 --- /dev/null +++ b/src/plugins/vis_type_xy/server/index.ts @@ -0,0 +1,10 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { VisTypeXyServerPlugin } from './plugin'; + +export const plugin = () => new VisTypeXyServerPlugin(); diff --git a/src/plugins/vis_type_xy/server/plugin.ts b/src/plugins/vis_type_xy/server/plugin.ts new file mode 100644 index 0000000000000..46d6531204c24 --- /dev/null +++ b/src/plugins/vis_type_xy/server/plugin.ts @@ -0,0 +1,56 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import { schema } from '@kbn/config-schema'; + +import { CoreSetup, Plugin, UiSettingsParams } from 'kibana/server'; + +import { LEGACY_CHARTS_LIBRARY } from '../common'; + +export const getUiSettingsConfig: () => Record> = () => ({ + // TODO: Remove this when vis_type_vislib is removed + // https://github.com/elastic/kibana/issues/56143 + [LEGACY_CHARTS_LIBRARY]: { + name: i18n.translate('visTypeXy.advancedSettings.visualization.legacyChartsLibrary.name', { + defaultMessage: 'XY axis legacy charts library', + }), + requiresPageReload: true, + value: false, + description: i18n.translate( + 'visTypeXy.advancedSettings.visualization.legacyChartsLibrary.description', + { + defaultMessage: 'Enables legacy charts library for area, line and bar charts in visualize.', + } + ), + deprecation: { + message: i18n.translate( + 'visTypeXy.advancedSettings.visualization.legacyChartsLibrary.deprecation', + { + defaultMessage: + 'The legacy charts library for area, line and bar charts in visualize is deprecated and will not be supported as of 7.16.', + } + ), + docLinksKey: 'visualizationSettings', + }, + category: ['visualization'], + schema: schema.boolean(), + }, +}); + +export class VisTypeXyServerPlugin implements Plugin { + public setup(core: CoreSetup) { + core.uiSettings.register(getUiSettingsConfig()); + + return {}; + } + + public start() { + return {}; + } +} diff --git a/src/plugins/visualizations/common/constants.ts b/src/plugins/visualizations/common/constants.ts index a33e74b498a2c..a8a0963ac8948 100644 --- a/src/plugins/visualizations/common/constants.ts +++ b/src/plugins/visualizations/common/constants.ts @@ -7,4 +7,3 @@ */ export const VISUALIZE_ENABLE_LABS_SETTING = 'visualize:enableLabs'; -export const LEGACY_CHARTS_LIBRARY = 'visualization:visualize:legacyChartsLibrary'; diff --git a/src/plugins/visualizations/server/plugin.ts b/src/plugins/visualizations/server/plugin.ts index 1fec63f2bb45a..5a5a80b2689d6 100644 --- a/src/plugins/visualizations/server/plugin.ts +++ b/src/plugins/visualizations/server/plugin.ts @@ -18,7 +18,7 @@ import { Logger, } from '../../../core/server'; -import { VISUALIZE_ENABLE_LABS_SETTING, LEGACY_CHARTS_LIBRARY } from '../common/constants'; +import { VISUALIZE_ENABLE_LABS_SETTING } from '../common/constants'; import { visualizationSavedObjectType } from './saved_objects'; @@ -58,27 +58,6 @@ export class VisualizationsPlugin category: ['visualization'], schema: schema.boolean(), }, - // TODO: Remove this when vis_type_vislib is removed - // https://github.com/elastic/kibana/issues/56143 - [LEGACY_CHARTS_LIBRARY]: { - name: i18n.translate( - 'visualizations.advancedSettings.visualization.legacyChartsLibrary.name', - { - defaultMessage: 'Legacy charts library', - } - ), - requiresPageReload: true, - value: false, - description: i18n.translate( - 'visualizations.advancedSettings.visualization.legacyChartsLibrary.description', - { - defaultMessage: - 'Enables legacy charts library for area, line, bar, pie charts in visualize.', - } - ), - category: ['visualization'], - schema: schema.boolean(), - }, }); if (plugins.usageCollection) { diff --git a/test/functional/apps/dashboard/dashboard_state.ts b/test/functional/apps/dashboard/dashboard_state.ts index 047681e1a8ace..6c259f5a71efa 100644 --- a/test/functional/apps/dashboard/dashboard_state.ts +++ b/test/functional/apps/dashboard/dashboard_state.ts @@ -53,6 +53,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { if (isNewChartsLibraryEnabled) { await kibanaServer.uiSettings.update({ 'visualization:visualize:legacyChartsLibrary': false, + 'visualization:visualize:legacyPieChartsLibrary': false, }); await browser.refresh(); } diff --git a/test/functional/apps/dashboard/index.ts b/test/functional/apps/dashboard/index.ts index 4b83b2ac92deb..e4dc04282e4ac 100644 --- a/test/functional/apps/dashboard/index.ts +++ b/test/functional/apps/dashboard/index.ts @@ -123,6 +123,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { await loadLogstash(); await kibanaServer.uiSettings.update({ 'visualization:visualize:legacyChartsLibrary': false, + 'visualization:visualize:legacyPieChartsLibrary': false, }); await browser.refresh(); }); @@ -131,6 +132,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { await unloadLogstash(); await kibanaServer.uiSettings.update({ 'visualization:visualize:legacyChartsLibrary': true, + 'visualization:visualize:legacyPieChartsLibrary': true, }); await browser.refresh(); }); diff --git a/test/functional/apps/getting_started/_shakespeare.ts b/test/functional/apps/getting_started/_shakespeare.ts index 945c1fdcbdcf4..ae6841b85c98d 100644 --- a/test/functional/apps/getting_started/_shakespeare.ts +++ b/test/functional/apps/getting_started/_shakespeare.ts @@ -57,6 +57,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { if (isNewChartsLibraryEnabled) { await kibanaServer.uiSettings.update({ 'visualization:visualize:legacyChartsLibrary': false, + 'visualization:visualize:legacyPieChartsLibrary': false, }); await browser.refresh(); } diff --git a/test/functional/apps/getting_started/index.ts b/test/functional/apps/getting_started/index.ts index b75a30037d065..4c1c052ef15a2 100644 --- a/test/functional/apps/getting_started/index.ts +++ b/test/functional/apps/getting_started/index.ts @@ -24,6 +24,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { before(async () => { await kibanaServer.uiSettings.update({ 'visualization:visualize:legacyChartsLibrary': false, + 'visualization:visualize:legacyPieChartsLibrary': false, }); await browser.refresh(); }); @@ -31,6 +32,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { after(async () => { await kibanaServer.uiSettings.update({ 'visualization:visualize:legacyChartsLibrary': true, + 'visualization:visualize:legacyPieChartsLibrary': true, }); await browser.refresh(); }); diff --git a/test/functional/apps/visualize/index.ts b/test/functional/apps/visualize/index.ts index cecd206abd1db..bc6160eba3846 100644 --- a/test/functional/apps/visualize/index.ts +++ b/test/functional/apps/visualize/index.ts @@ -31,6 +31,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { before(async () => { await kibanaServer.uiSettings.update({ 'visualization:visualize:legacyChartsLibrary': false, + 'visualization:visualize:legacyPieChartsLibrary': false, }); await browser.refresh(); }); @@ -38,6 +39,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { after(async () => { await kibanaServer.uiSettings.update({ 'visualization:visualize:legacyChartsLibrary': true, + 'visualization:visualize:legacyPieChartsLibrary': true, }); await browser.refresh(); }); diff --git a/test/functional/config.js b/test/functional/config.js index b28a9fd36c1c0..670488003e56c 100644 --- a/test/functional/config.js +++ b/test/functional/config.js @@ -58,6 +58,7 @@ export default async function ({ readConfigFile }) { 'accessibility:disableAnimations': true, 'dateFormat:tz': 'UTC', 'visualization:visualize:legacyChartsLibrary': true, + 'visualization:visualize:legacyPieChartsLibrary': true, }, }, diff --git a/test/functional/page_objects/visualize_chart_page.ts b/test/functional/page_objects/visualize_chart_page.ts index c8587f4ffd346..64b8c363fa6c2 100644 --- a/test/functional/page_objects/visualize_chart_page.ts +++ b/test/functional/page_objects/visualize_chart_page.ts @@ -37,7 +37,8 @@ export class VisualizeChartPageObject extends FtrService { public async isNewChartsLibraryEnabled(): Promise { const legacyChartsLibrary = Boolean( - await this.kibanaServer.uiSettings.get('visualization:visualize:legacyChartsLibrary') + (await this.kibanaServer.uiSettings.get('visualization:visualize:legacyChartsLibrary')) && + (await this.kibanaServer.uiSettings.get('visualization:visualize:legacyPieChartsLibrary')) ) ?? true; const enabled = !legacyChartsLibrary; this.log.debug(`-- isNewChartsLibraryEnabled = ${enabled}`); diff --git a/test/functional/page_objects/visualize_page.ts b/test/functional/page_objects/visualize_page.ts index a11a254509e7a..e930406cdcce8 100644 --- a/test/functional/page_objects/visualize_page.ts +++ b/test/functional/page_objects/visualize_page.ts @@ -57,6 +57,7 @@ export class VisualizePageObject extends FtrService { defaultIndex: 'logstash-*', [UI_SETTINGS.FORMAT_BYTES_DEFAULT_PATTERN]: '0,0.[000]b', 'visualization:visualize:legacyChartsLibrary': !isNewLibrary, + 'visualization:visualize:legacyPieChartsLibrary': !isNewLibrary, }); } diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index e73bb19627b8f..87813b64f1f20 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -4941,8 +4941,6 @@ "visTypePie.editors.pie.showLabelsLabel": "ラベルを表示", "visTypePie.editors.pie.showTopLevelOnlyLabel": "トップレベルのみ表示", "visTypePie.editors.pie.showValuesLabel": "値を表示", - "visualizations.advancedSettings.visualization.legacyChartsLibrary.description": "Visualizeでエリア、折れ線、棒グラフのレガシーグラフライブラリを有効にします。", - "visualizations.advancedSettings.visualization.legacyChartsLibrary.name": "レガシーグラフライブラリ", "visualizations.advancedSettings.visualizeEnableLabsText": "ユーザーが実験的なビジュアライゼーションを作成、表示、編集できるようになります。無効の場合、\n ユーザーは本番準備が整ったビジュアライゼーションのみを利用できます。", "visualizations.advancedSettings.visualizeEnableLabsTitle": "実験的なビジュアライゼーションを有効にする", "visualizations.disabledLabVisualizationLink": "ドキュメンテーションを表示", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 51f6378f481e2..4e98de541ce60 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -4968,8 +4968,6 @@ "visTypePie.editors.pie.showLabelsLabel": "显示标签", "visTypePie.editors.pie.showTopLevelOnlyLabel": "仅显示顶级", "visTypePie.editors.pie.showValuesLabel": "显示值", - "visualizations.advancedSettings.visualization.legacyChartsLibrary.description": "在 Visualize 中启用面积图、折线图和条形图的旧版图表库。", - "visualizations.advancedSettings.visualization.legacyChartsLibrary.name": "旧版图表库", "visualizations.advancedSettings.visualizeEnableLabsText": "允许用户创建、查看和编辑实验性可视化。如果禁用,\n 仅被视为生产就绪的可视化可供用户使用。", "visualizations.advancedSettings.visualizeEnableLabsTitle": "启用实验性可视化", "visualizations.disabledLabVisualizationLink": "阅读文档", diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.js index cf05bd6e15898..2c3a3c93e2a0a 100644 --- a/x-pack/test/functional/config.js +++ b/x-pack/test/functional/config.js @@ -103,6 +103,7 @@ export default async function ({ readConfigFile }) { 'accessibility:disableAnimations': true, 'dateFormat:tz': 'UTC', 'visualization:visualize:legacyChartsLibrary': true, + 'visualization:visualize:legacyPieChartsLibrary': true, }, }, // the apps section defines the urls that From 59d422394aad7124cf07efbd651fed944e73f0e5 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Thu, 24 Jun 2021 11:43:33 +0200 Subject: [PATCH 08/69] [Lens] Move empty string handling into field formatter (#102877) --- .../common/field_formats/converters/string.ts | 7 ++ .../vis_type_pie/public/utils/get_layers.ts | 6 - .../public/components/detailed_tooltip.tsx | 4 +- .../public/components/xy_settings.tsx | 3 +- .../vis_type_xy/public/config/get_axis.ts | 4 +- .../public/utils/get_series_name_fn.ts | 13 +- .../components/table_actions.ts | 6 +- .../heatmap_visualization/chart_component.tsx | 5 +- .../rename_columns.test.ts | 23 ---- .../indexpattern_datasource/rename_columns.ts | 14 +-- .../pie_visualization/render_function.tsx | 3 +- .../legend_action_popover.tsx | 5 +- x-pack/plugins/lens/public/utils.test.ts | 118 ------------------ x-pack/plugins/lens/public/utils.ts | 36 ------ .../public/xy_visualization/expression.tsx | 3 +- .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - .../discover/__snapshots__/reporting.snap | 4 +- 18 files changed, 23 insertions(+), 235 deletions(-) delete mode 100644 x-pack/plugins/lens/public/utils.test.ts diff --git a/src/plugins/data/common/field_formats/converters/string.ts b/src/plugins/data/common/field_formats/converters/string.ts index ec92d75910522..64367df5d90dd 100644 --- a/src/plugins/data/common/field_formats/converters/string.ts +++ b/src/plugins/data/common/field_formats/converters/string.ts @@ -13,6 +13,10 @@ import { FieldFormat } from '../field_format'; import { TextContextTypeConvert, FIELD_FORMAT_IDS } from '../types'; import { shortenDottedString } from '../../utils'; +export const emptyLabel = i18n.translate('data.fieldFormats.string.emptyLabel', { + defaultMessage: '(empty)', +}); + const TRANSFORM_OPTIONS = [ { kind: false, @@ -103,6 +107,9 @@ export class StringFormat extends FieldFormat { } textConvert: TextContextTypeConvert = (val) => { + if (val === '') { + return emptyLabel; + } switch (this.param('transform')) { case 'lower': return String(val).toLowerCase(); diff --git a/src/plugins/vis_type_pie/public/utils/get_layers.ts b/src/plugins/vis_type_pie/public/utils/get_layers.ts index 27dcf2d379811..5a82871bf3688 100644 --- a/src/plugins/vis_type_pie/public/utils/get_layers.ts +++ b/src/plugins/vis_type_pie/public/utils/get_layers.ts @@ -6,7 +6,6 @@ * Side Public License, v 1. */ -import { i18n } from '@kbn/i18n'; import { Datum, PartitionFillLabel, @@ -125,11 +124,6 @@ export const getLayers = ( }, showAccessor: (d: Datum) => d !== EMPTY_SLICE, nodeLabel: (d: unknown) => { - if (d === '') { - return i18n.translate('visTypePie.emptyLabelValue', { - defaultMessage: '(empty)', - }); - } if (col.format) { const formattedLabel = formatter.deserialize(col.format).convert(d) ?? ''; if (visParams.labels.truncate && formattedLabel.length <= visParams.labels.truncate) { diff --git a/src/plugins/vis_type_xy/public/components/detailed_tooltip.tsx b/src/plugins/vis_type_xy/public/components/detailed_tooltip.tsx index c9ed82fcf58e5..fb6b4bb41d9ba 100644 --- a/src/plugins/vis_type_xy/public/components/detailed_tooltip.tsx +++ b/src/plugins/vis_type_xy/public/components/detailed_tooltip.tsx @@ -19,7 +19,6 @@ import { import { Aspects } from '../types'; import './_detailed_tooltip.scss'; -import { fillEmptyValue } from '../utils/get_series_name_fn'; import { COMPLEX_SPLIT_ACCESSOR, isRangeAggType } from '../utils/accessors'; interface TooltipData { @@ -100,8 +99,7 @@ export const getTooltipData = ( return data; }; -const renderData = ({ label, value: rawValue }: TooltipData, index: number) => { - const value = fillEmptyValue(rawValue); +const renderData = ({ label, value }: TooltipData, index: number) => { return label && value ? ( diff --git a/src/plugins/vis_type_xy/public/components/xy_settings.tsx b/src/plugins/vis_type_xy/public/components/xy_settings.tsx index 8922f512522a0..8d6a7eecdfe52 100644 --- a/src/plugins/vis_type_xy/public/components/xy_settings.tsx +++ b/src/plugins/vis_type_xy/public/components/xy_settings.tsx @@ -29,7 +29,6 @@ import { renderEndzoneTooltip } from '../../../charts/public'; import { getThemeService, getUISettings } from '../services'; import { VisConfig } from '../types'; -import { fillEmptyValue } from '../utils/get_series_name_fn'; declare global { interface Window { @@ -134,7 +133,7 @@ export const XYSettings: FC = ({ }; const headerValueFormatter: TickFormatter | undefined = xAxis.ticks?.formatter - ? (value) => fillEmptyValue(xAxis.ticks?.formatter?.(value)) ?? '' + ? (value) => xAxis.ticks?.formatter?.(value) ?? '' : undefined; const headerFormatter = isTimeChart && xDomain && adjustedXDomain diff --git a/src/plugins/vis_type_xy/public/config/get_axis.ts b/src/plugins/vis_type_xy/public/config/get_axis.ts index 08b17c882eea6..71d33cc20d057 100644 --- a/src/plugins/vis_type_xy/public/config/get_axis.ts +++ b/src/plugins/vis_type_xy/public/config/get_axis.ts @@ -27,7 +27,6 @@ import { YScaleType, SeriesParam, } from '../types'; -import { fillEmptyValue } from '../utils/get_series_name_fn'; export function getAxis( { type, title: axisTitle, labels, scale: axisScale, ...axis }: CategoryAxis, @@ -90,8 +89,7 @@ function getLabelFormatter( } return (value: any) => { - const formattedStringValue = `${formatter ? formatter(value) : value}`; - const finalValue = fillEmptyValue(formattedStringValue); + const finalValue = `${formatter ? formatter(value) : value}`; if (finalValue.length > truncate) { return `${finalValue.slice(0, truncate)}...`; diff --git a/src/plugins/vis_type_xy/public/utils/get_series_name_fn.ts b/src/plugins/vis_type_xy/public/utils/get_series_name_fn.ts index 0e54650e22f75..137f8a5558010 100644 --- a/src/plugins/vis_type_xy/public/utils/get_series_name_fn.ts +++ b/src/plugins/vis_type_xy/public/utils/get_series_name_fn.ts @@ -8,21 +8,10 @@ import { memoize } from 'lodash'; -import { i18n } from '@kbn/i18n'; import { XYChartSeriesIdentifier, SeriesName } from '@elastic/charts'; import { VisConfig } from '../types'; -const emptyTextLabel = i18n.translate('visTypeXy.emptyTextColumnValue', { - defaultMessage: '(empty)', -}); - -/** - * Returns empty values - */ -export const fillEmptyValue = (value: T) => - value === '' ? emptyTextLabel : value; - function getSplitValues( splitAccessors: XYChartSeriesIdentifier['splitAccessors'], seriesAspects?: VisConfig['aspects']['series'] @@ -36,7 +25,7 @@ function getSplitValues( const split = (seriesAspects ?? []).find(({ accessor }) => accessor === key); splitValues.push(split?.formatter ? split?.formatter(value) : value); }); - return splitValues.map(fillEmptyValue); + return splitValues; } export const getSeriesNameFn = (aspects: VisConfig['aspects'], multipleY = false) => diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/table_actions.ts b/x-pack/plugins/lens/public/datatable_visualization/components/table_actions.ts index 0d44ae3aa6dec..8615ed6536316 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/components/table_actions.ts +++ b/x-pack/plugins/lens/public/datatable_visualization/components/table_actions.ts @@ -15,8 +15,6 @@ import type { LensToggleAction, } from './types'; import { ColumnConfig } from './table_basic'; - -import { desanitizeFilterContext } from '../../utils'; import { getOriginalId } from '../transpose_helpers'; export const createGridResizeHandler = ( @@ -92,7 +90,7 @@ export const createGridFilterHandler = ( timeFieldName, }; - onClickValue(desanitizeFilterContext(data)); + onClickValue(data); }; export const createTransposeColumnFilterHandler = ( @@ -125,7 +123,7 @@ export const createTransposeColumnFilterHandler = ( timeFieldName, }; - onClickValue(desanitizeFilterContext(data)); + onClickValue(data); }; export const createGridSortingConfig = ( diff --git a/x-pack/plugins/lens/public/heatmap_visualization/chart_component.tsx b/x-pack/plugins/lens/public/heatmap_visualization/chart_component.tsx index 3048f3b3db580..8214d5ba129d4 100644 --- a/x-pack/plugins/lens/public/heatmap_visualization/chart_component.tsx +++ b/x-pack/plugins/lens/public/heatmap_visualization/chart_component.tsx @@ -21,7 +21,6 @@ import { VisualizationContainer } from '../visualization_container'; import { HeatmapRenderProps } from './types'; import './index.scss'; import { LensBrushEvent, LensFilterEvent } from '../types'; -import { desanitizeFilterContext } from '../utils'; import { EmptyPlaceholder } from '../shared_components'; import { LensIconChartHeatmap } from '../assets/chart_heatmap'; @@ -117,7 +116,7 @@ export const HeatmapComponent: FC = ({ })), timeFieldName, }; - onClickValue(desanitizeFilterContext(context)); + onClickValue(context); }) as ElementClickListener; const onBrushEnd = (e: HeatmapBrushEvent) => { @@ -164,7 +163,7 @@ export const HeatmapComponent: FC = ({ })), timeFieldName, }; - onClickValue(desanitizeFilterContext(context)); + onClickValue(context); } }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/rename_columns.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/rename_columns.test.ts index 0750b99db5f67..5654a599c5e27 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/rename_columns.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/rename_columns.test.ts @@ -83,29 +83,6 @@ describe('rename_columns', () => { `); }); - it('should replace "" with a visible value', () => { - const input: Datatable = { - type: 'datatable', - columns: [{ id: 'a', name: 'A', meta: { type: 'string' } }], - rows: [{ a: '' }], - }; - - const idMap = { - a: { - id: 'a', - label: 'Austrailia', - }, - }; - - const result = renameColumns.fn( - input, - { idMap: JSON.stringify(idMap) }, - createMockExecutionContext() - ); - - expect(result.rows[0].a).toEqual('(empty)'); - }); - it('should keep columns which are not mapped', () => { const input: Datatable = { type: 'datatable', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/rename_columns.ts b/x-pack/plugins/lens/public/indexpattern_datasource/rename_columns.ts index 89c63880248d0..a16756126c030 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/rename_columns.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/rename_columns.ts @@ -49,9 +49,9 @@ export const renameColumns: ExpressionFunctionDefinition< Object.entries(row).forEach(([id, value]) => { if (id in idMap) { - mappedRow[idMap[id].id] = sanitizeValue(value); + mappedRow[idMap[id].id] = value; } else { - mappedRow[id] = sanitizeValue(value); + mappedRow[id] = value; } }); @@ -86,13 +86,3 @@ function getColumnName(originalColumn: OriginalColumn, newColumn: DatatableColum return originalColumn.label; } - -function sanitizeValue(value: unknown) { - if (value === '') { - return i18n.translate('xpack.lens.indexpattern.emptyTextColumnValue', { - defaultMessage: '(empty)', - }); - } - - return value; -} diff --git a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx index f329cfe1bb8b9..2e5a06b4f705f 100644 --- a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx @@ -31,7 +31,6 @@ import { PieExpressionProps } from './types'; import { getSliceValue, getFilterContext } from './render_helpers'; import { EmptyPlaceholder } from '../shared_components'; import './visualization.scss'; -import { desanitizeFilterContext } from '../utils'; import { ChartsPluginSetup, PaletteRegistry, @@ -254,7 +253,7 @@ export function PieComponent( const onElementClickHandler: ElementClickListener = (args) => { const context = getFilterContext(args[0][0] as LayerValue[], groups, firstTable); - onClickValue(desanitizeFilterContext(context)); + onClickValue(context); }; return ( diff --git a/x-pack/plugins/lens/public/shared_components/legend_action_popover.tsx b/x-pack/plugins/lens/public/shared_components/legend_action_popover.tsx index e344cb5289f51..5027629ef6ae5 100644 --- a/x-pack/plugins/lens/public/shared_components/legend_action_popover.tsx +++ b/x-pack/plugins/lens/public/shared_components/legend_action_popover.tsx @@ -9,7 +9,6 @@ import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiContextMenuPanelDescriptor, EuiIcon, EuiPopover, EuiContextMenu } from '@elastic/eui'; import type { LensFilterEvent } from '../types'; -import { desanitizeFilterContext } from '../utils'; export interface LegendActionPopoverProps { /** @@ -45,7 +44,7 @@ export const LegendActionPopover: React.FunctionComponent, onClick: () => { setPopoverOpen(false); - onFilter(desanitizeFilterContext(context)); + onFilter(context); }, }, { @@ -56,7 +55,7 @@ export const LegendActionPopover: React.FunctionComponent, onClick: () => { setPopoverOpen(false); - onFilter(desanitizeFilterContext({ ...context, negate: true })); + onFilter({ ...context, negate: true }); }, }, ], diff --git a/x-pack/plugins/lens/public/utils.test.ts b/x-pack/plugins/lens/public/utils.test.ts deleted file mode 100644 index 76597870b3beb..0000000000000 --- a/x-pack/plugins/lens/public/utils.test.ts +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { LensFilterEvent } from './types'; -import { desanitizeFilterContext } from './utils'; -import { Datatable } from '../../../../src/plugins/expressions/common'; - -describe('desanitizeFilterContext', () => { - it(`When filtered value equals '(empty)' replaces it with '' in table and in value.`, () => { - const table: Datatable = { - type: 'datatable', - rows: [ - { - 'f903668f-1175-4705-a5bd-713259d10326': 1589414640000, - '5d5446b2-72e8-4f86-91e0-88380f0fa14c': '(empty)', - 'col-1-9f0b6f88-c399-43a0-a993-0ad943c9af25': 1, - }, - { - 'f903668f-1175-4705-a5bd-713259d10326': 1589414670000, - 'col-1-9f0b6f88-c399-43a0-a993-0ad943c9af25': 0, - }, - { - 'f903668f-1175-4705-a5bd-713259d10326': 1589414880000, - '5d5446b2-72e8-4f86-91e0-88380f0fa14c': '123123123', - 'col-1-9f0b6f88-c399-43a0-a993-0ad943c9af25': 1, - }, - { - 'f903668f-1175-4705-a5bd-713259d10326': 1589414910000, - '5d5446b2-72e8-4f86-91e0-88380f0fa14c': '(empty)', - 'col-1-9f0b6f88-c399-43a0-a993-0ad943c9af25': 1, - }, - ], - columns: [ - { - id: 'f903668f-1175-4705-a5bd-713259d10326', - name: 'order_date per 30 seconds', - meta: { type: 'date' }, - }, - { - id: '5d5446b2-72e8-4f86-91e0-88380f0fa14c', - name: 'Top values of customer_phone', - meta: { type: 'string' }, - }, - { - id: '9f0b6f88-c399-43a0-a993-0ad943c9af25', - name: 'Count of records', - meta: { type: 'number' }, - }, - ], - }; - - const contextWithEmptyValue: LensFilterEvent['data'] = { - data: [ - { - row: 3, - column: 0, - value: 1589414910000, - table, - }, - { - row: 0, - column: 1, - value: '(empty)', - table, - }, - ], - timeFieldName: 'order_date', - }; - - const desanitizedFilterContext = desanitizeFilterContext(contextWithEmptyValue); - - expect(desanitizedFilterContext).toEqual({ - data: [ - { - row: 3, - column: 0, - value: 1589414910000, - table, - }, - { - value: '', - row: 0, - column: 1, - table: { - rows: [ - { - 'f903668f-1175-4705-a5bd-713259d10326': 1589414640000, - '5d5446b2-72e8-4f86-91e0-88380f0fa14c': '', - 'col-1-9f0b6f88-c399-43a0-a993-0ad943c9af25': 1, - }, - { - 'f903668f-1175-4705-a5bd-713259d10326': 1589414670000, - 'col-1-9f0b6f88-c399-43a0-a993-0ad943c9af25': 0, - }, - { - 'f903668f-1175-4705-a5bd-713259d10326': 1589414880000, - '5d5446b2-72e8-4f86-91e0-88380f0fa14c': '123123123', - 'col-1-9f0b6f88-c399-43a0-a993-0ad943c9af25': 1, - }, - { - 'f903668f-1175-4705-a5bd-713259d10326': 1589414910000, - '5d5446b2-72e8-4f86-91e0-88380f0fa14c': '(empty)', - 'col-1-9f0b6f88-c399-43a0-a993-0ad943c9af25': 1, - }, - ], - columns: table.columns, - type: 'datatable', - }, - }, - ], - timeFieldName: 'order_date', - }); - }); -}); diff --git a/x-pack/plugins/lens/public/utils.ts b/x-pack/plugins/lens/public/utils.ts index 2706fe977c68e..1c4b2c67f96fc 100644 --- a/x-pack/plugins/lens/public/utils.ts +++ b/x-pack/plugins/lens/public/utils.ts @@ -9,42 +9,6 @@ import { i18n } from '@kbn/i18n'; import { IndexPattern, IndexPatternsContract, TimefilterContract } from 'src/plugins/data/public'; import { IUiSettingsClient } from 'kibana/public'; import moment from 'moment-timezone'; -import { LensFilterEvent } from './types'; - -/** replaces the value `(empty) to empty string for proper filtering` */ -export const desanitizeFilterContext = ( - context: LensFilterEvent['data'] -): LensFilterEvent['data'] => { - const emptyTextValue = i18n.translate('xpack.lens.indexpattern.emptyTextColumnValue', { - defaultMessage: '(empty)', - }); - const result: LensFilterEvent['data'] = { - ...context, - data: context.data.map((point) => - point.value === emptyTextValue - ? { - ...point, - value: '', - table: { - ...point.table, - rows: point.table.rows.map((row, index) => - index === point.row - ? { - ...row, - [point.table.columns[point.column].id]: '', - } - : row - ), - }, - } - : point - ), - }; - if (context.timeFieldName) { - result.timeFieldName = context.timeFieldName; - } - return result; -}; export function getVisualizeGeoFieldMessage(fieldType: string) { return i18n.translate('xpack.lens.visualizeGeoFieldMessage', { diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.tsx index 1de5cf6b30533..3fe98282a18b0 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression.tsx @@ -53,7 +53,6 @@ import { SeriesLayer, } from '../../../../../src/plugins/charts/public'; import { EmptyPlaceholder } from '../shared_components'; -import { desanitizeFilterContext } from '../utils'; import { fittingFunctionDefinitions, getFitOptions } from './fitting_functions'; import { getAxesConfiguration, GroupsConfiguration, validateExtent } from './axes_configuration'; import { getColorAssignments } from './color_assignment'; @@ -575,7 +574,7 @@ export function XYChart({ })), timeFieldName: xDomain && isDateField ? xAxisFieldName : undefined, }; - onClickValue(desanitizeFilterContext(context)); + onClickValue(context); }; const brushHandler: BrushEndListener = ({ x }) => { diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 87813b64f1f20..837716ec9dd5a 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -5055,7 +5055,6 @@ "visTypeXy.editors.pointSeries.thresholdLine.valueLabel": "しきい値", "visTypeXy.editors.pointSeries.thresholdLine.widthLabel": "線の幅", "visTypeXy.editors.pointSeries.thresholdLineSettingsTitle": "しきい線", - "visTypeXy.emptyTextColumnValue": " (空) ", "visTypeXy.fittingFunctionsTitle.carry": "最後 (ギャップを最後の値で埋める) ", "visTypeXy.fittingFunctionsTitle.linear": "線形 (ギャップを線で埋める) ", "visTypeXy.fittingFunctionsTitle.lookahead": "次 (ギャップを次の値で埋める) ", @@ -12574,7 +12573,6 @@ "xpack.lens.indexPattern.emptyDimensionButton": "空のディメンション", "xpack.lens.indexPattern.emptyFieldsLabel": "空のフィールド", "xpack.lens.indexPattern.emptyFieldsLabelHelp": "空のフィールドには、フィルターに基づく最初の 500 件のドキュメントの値が含まれていませんでした。", - "xpack.lens.indexpattern.emptyTextColumnValue": " (空) ", "xpack.lens.indexPattern.existenceErrorAriaLabel": "存在の取り込みに失敗しました", "xpack.lens.indexPattern.existenceErrorLabel": "フィールド情報を読み込めません", "xpack.lens.indexPattern.existenceTimeoutAriaLabel": "存在の取り込みがタイムアウトしました", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 4e98de541ce60..0192566db0731 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -5083,7 +5083,6 @@ "visTypeXy.editors.pointSeries.thresholdLine.valueLabel": "阈值", "visTypeXy.editors.pointSeries.thresholdLine.widthLabel": "线条宽度", "visTypeXy.editors.pointSeries.thresholdLineSettingsTitle": "阈值线条", - "visTypeXy.emptyTextColumnValue": " (空) ", "visTypeXy.fittingFunctionsTitle.carry": "最后一个 (使用最后一个值填充缺口) ", "visTypeXy.fittingFunctionsTitle.linear": "线 (使用线填充缺口) ", "visTypeXy.fittingFunctionsTitle.lookahead": "下一个 (使用下一个值填充缺口) ", @@ -12744,7 +12743,6 @@ "xpack.lens.indexPattern.emptyDimensionButton": "空维度", "xpack.lens.indexPattern.emptyFieldsLabel": "空字段", "xpack.lens.indexPattern.emptyFieldsLabelHelp": "空字段在基于您的筛选的前 500 个文档中不包含任何值。", - "xpack.lens.indexpattern.emptyTextColumnValue": " (空) ", "xpack.lens.indexPattern.existenceErrorAriaLabel": "现有内容提取失败", "xpack.lens.indexPattern.existenceErrorLabel": "无法加载字段信息", "xpack.lens.indexPattern.existenceTimeoutAriaLabel": "现有内容提取超时", diff --git a/x-pack/test/functional/apps/discover/__snapshots__/reporting.snap b/x-pack/test/functional/apps/discover/__snapshots__/reporting.snap index baa49cb6f9d81..c7666bf00dd53 100644 --- a/x-pack/test/functional/apps/discover/__snapshots__/reporting.snap +++ b/x-pack/test/functional/apps/discover/__snapshots__/reporting.snap @@ -65,7 +65,7 @@ exports[`discover Discover CSV Export Generate CSV: archived search generates a exports[`discover Discover CSV Export Generate CSV: new search generates a report from a new search with data: default 1`] = ` "\\"_id\\",\\"_index\\",\\"_score\\",\\"_type\\",category,\\"category.keyword\\",currency,\\"customer_first_name\\",\\"customer_first_name.keyword\\",\\"customer_full_name\\",\\"customer_full_name.keyword\\",\\"customer_gender\\",\\"customer_id\\",\\"customer_last_name\\",\\"customer_last_name.keyword\\",\\"customer_phone\\",\\"day_of_week\\",\\"day_of_week_i\\",email,\\"geoip.city_name\\",\\"geoip.continent_name\\",\\"geoip.country_iso_code\\",\\"geoip.location\\",\\"geoip.region_name\\",manufacturer,\\"manufacturer.keyword\\",\\"order_date\\",\\"order_id\\",\\"products._id\\",\\"products._id.keyword\\",\\"products.base_price\\",\\"products.base_unit_price\\",\\"products.category\\",\\"products.category.keyword\\",\\"products.created_on\\",\\"products.discount_amount\\",\\"products.discount_percentage\\",\\"products.manufacturer\\",\\"products.manufacturer.keyword\\",\\"products.min_price\\",\\"products.price\\",\\"products.product_id\\",\\"products.product_name\\",\\"products.product_name.keyword\\",\\"products.quantity\\",\\"products.sku\\",\\"products.tax_amount\\",\\"products.taxful_price\\",\\"products.taxless_price\\",\\"products.unit_discount_amount\\",sku,\\"taxful_total_price\\",\\"taxless_total_price\\",\\"total_quantity\\",\\"total_unique_products\\",type,user -3AMtOW0BH63Xcmy432DJ,ecommerce,\\"-\\",\\"-\\",\\"Men's Shoes, Men's Clothing, Women's Accessories, Men's Accessories\\",\\"Men's Shoes, Men's Clothing, Women's Accessories, Men's Accessories\\",EUR,\\"Sultan Al\\",\\"Sultan Al\\",\\"Sultan Al Boone\\",\\"Sultan Al Boone\\",MALE,19,Boone,Boone,,Saturday,5,\\"sultan al@boone-family.zzz\\",\\"Abu Dhabi\\",Asia,AE,\\"{ +3AMtOW0BH63Xcmy432DJ,ecommerce,\\"-\\",\\"-\\",\\"Men's Shoes, Men's Clothing, Women's Accessories, Men's Accessories\\",\\"Men's Shoes, Men's Clothing, Women's Accessories, Men's Accessories\\",EUR,\\"Sultan Al\\",\\"Sultan Al\\",\\"Sultan Al Boone\\",\\"Sultan Al Boone\\",MALE,19,Boone,Boone,\\"(empty)\\",Saturday,5,\\"sultan al@boone-family.zzz\\",\\"Abu Dhabi\\",Asia,AE,\\"{ \\"\\"coordinates\\"\\": [ 54.4, 24.5 @@ -77,7 +77,7 @@ exports[`discover Discover CSV Export Generate CSV: new search generates a repor exports[`discover Discover CSV Export Generate CSV: new search generates a report from a new search with data: discover:searchFieldsFromSource 1`] = ` "\\"_id\\",\\"_index\\",\\"_score\\",\\"_type\\",category,\\"category.keyword\\",currency,\\"customer_first_name\\",\\"customer_first_name.keyword\\",\\"customer_full_name\\",\\"customer_full_name.keyword\\",\\"customer_gender\\",\\"customer_id\\",\\"customer_last_name\\",\\"customer_last_name.keyword\\",\\"customer_phone\\",\\"day_of_week\\",\\"day_of_week_i\\",email,\\"geoip.city_name\\",\\"geoip.continent_name\\",\\"geoip.country_iso_code\\",\\"geoip.location\\",\\"geoip.region_name\\",manufacturer,\\"manufacturer.keyword\\",\\"order_date\\",\\"order_id\\",\\"products._id\\",\\"products._id.keyword\\",\\"products.base_price\\",\\"products.base_unit_price\\",\\"products.category\\",\\"products.category.keyword\\",\\"products.created_on\\",\\"products.discount_amount\\",\\"products.discount_percentage\\",\\"products.manufacturer\\",\\"products.manufacturer.keyword\\",\\"products.min_price\\",\\"products.price\\",\\"products.product_id\\",\\"products.product_name\\",\\"products.product_name.keyword\\",\\"products.quantity\\",\\"products.sku\\",\\"products.tax_amount\\",\\"products.taxful_price\\",\\"products.taxless_price\\",\\"products.unit_discount_amount\\",sku,\\"taxful_total_price\\",\\"taxless_total_price\\",\\"total_quantity\\",\\"total_unique_products\\",type,user -3AMtOW0BH63Xcmy432DJ,ecommerce,\\"-\\",\\"-\\",\\"Men's Shoes, Men's Clothing, Women's Accessories, Men's Accessories\\",\\"Men's Shoes, Men's Clothing, Women's Accessories, Men's Accessories\\",EUR,\\"Sultan Al\\",\\"Sultan Al\\",\\"Sultan Al Boone\\",\\"Sultan Al Boone\\",MALE,19,Boone,Boone,,Saturday,5,\\"sultan al@boone-family.zzz\\",\\"Abu Dhabi\\",Asia,AE,\\"{ +3AMtOW0BH63Xcmy432DJ,ecommerce,\\"-\\",\\"-\\",\\"Men's Shoes, Men's Clothing, Women's Accessories, Men's Accessories\\",\\"Men's Shoes, Men's Clothing, Women's Accessories, Men's Accessories\\",EUR,\\"Sultan Al\\",\\"Sultan Al\\",\\"Sultan Al Boone\\",\\"Sultan Al Boone\\",MALE,19,Boone,Boone,\\"(empty)\\",Saturday,5,\\"sultan al@boone-family.zzz\\",\\"Abu Dhabi\\",Asia,AE,\\"{ \\"\\"coordinates\\"\\": [ 54.4, 24.5 From 4c2449fd2860ad7757fd210788563f2bede86354 Mon Sep 17 00:00:00 2001 From: Ignacio Rivas Date: Thu, 24 Jun 2021 11:55:28 +0200 Subject: [PATCH 09/69] [Ingest pipelines] add extract_device_type to user agent processor (#100986) * testing layouts * fix copy for beta badge * replace hardcoded text with i18n strings * avoid updating types and just replace label * Small cr changes * get rid of style prop and just use a smaller badge --- .../__jest__/processors/processor.helpers.tsx | 3 + .../__jest__/processors/user_agent.test.tsx | 125 ++++++++++++++++++ .../common_fields/properties_field.tsx | 13 +- .../processor_form/processors/user_agent.tsx | 62 ++++++++- 4 files changed, 188 insertions(+), 15 deletions(-) create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/user_agent.test.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/processor.helpers.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/processor.helpers.tsx index 15e8c323b1308..5f6ace2069410 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/processor.helpers.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/processor.helpers.tsx @@ -156,6 +156,9 @@ type TestSubject = | 'separatorValueField.input' | 'quoteValueField.input' | 'emptyValueField.input' + | 'extractDeviceTypeSwitch.input' + | 'propertiesValueField' + | 'regexFileField.input' | 'valueFieldInput' | 'mediaTypeSelectorField' | 'ignoreEmptyField.input' diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/user_agent.test.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/user_agent.test.tsx new file mode 100644 index 0000000000000..fa1c24c9dfb39 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/user_agent.test.tsx @@ -0,0 +1,125 @@ +/* + * 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 { act } from 'react-dom/test-utils'; +import { setup, SetupResult, getProcessorValue } from './processor.helpers'; + +// Default parameter values automatically added to the user agent processor when saved +const defaultUserAgentParameters = { + if: undefined, + regex_file: undefined, + properties: undefined, + description: undefined, + ignore_missing: undefined, + ignore_failure: undefined, + extract_device_type: undefined, +}; + +const USER_AGENT_TYPE = 'user_agent'; + +describe('Processor: User Agent', () => { + let onUpdate: jest.Mock; + let testBed: SetupResult; + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + beforeEach(async () => { + onUpdate = jest.fn(); + + await act(async () => { + testBed = await setup({ + value: { + processors: [], + }, + onFlyoutOpen: jest.fn(), + onUpdate, + }); + }); + + testBed.component.update(); + + // Open flyout to add new processor + testBed.actions.addProcessor(); + // Add type (the other fields are not visible until a type is selected) + await testBed.actions.addProcessorType(USER_AGENT_TYPE); + }); + + test('prevents form submission if required fields are not provided', async () => { + const { + actions: { saveNewProcessor }, + form, + } = testBed; + + // Click submit button with only the processor type defined + await saveNewProcessor(); + + // Expect form error as "field" is required parameter + expect(form.getErrorsMessages()).toEqual(['A field value is required.']); + }); + + test('saves with just the default parameter value', async () => { + const { + actions: { saveNewProcessor }, + form, + } = testBed; + + // Add "field" value (required) + form.setInputValue('fieldNameField.input', 'field_1'); + // Save the field + await saveNewProcessor(); + + const processors = getProcessorValue(onUpdate, USER_AGENT_TYPE); + expect(processors[0][USER_AGENT_TYPE]).toEqual({ + ...defaultUserAgentParameters, + field: 'field_1', + }); + }); + + test('allows optional parameters to be set', async () => { + const { + actions: { saveNewProcessor }, + form, + find, + component, + } = testBed; + + // Add "field" value (required) + form.setInputValue('fieldNameField.input', 'field_1'); + + // Set optional parameteres + form.setInputValue('targetField.input', 'target_field'); + form.setInputValue('regexFileField.input', 'hello*'); + form.toggleEuiSwitch('ignoreMissingSwitch.input'); + form.toggleEuiSwitch('ignoreFailureSwitch.input'); + form.toggleEuiSwitch('extractDeviceTypeSwitch.input'); + await act(async () => { + find('propertiesValueField').simulate('change', [{ label: 'os' }]); + }); + component.update(); + + // Save the field with new changes + await saveNewProcessor(); + + const processors = getProcessorValue(onUpdate, USER_AGENT_TYPE); + expect(processors[0][USER_AGENT_TYPE]).toEqual({ + ...defaultUserAgentParameters, + field: 'field_1', + target_field: 'target_field', + properties: ['os'], + regex_file: 'hello*', + extract_device_type: true, + ignore_missing: true, + ignore_failure: true, + }); + }); +}); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/common_fields/properties_field.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/common_fields/properties_field.tsx index dd52375a19436..c8a50cf64484e 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/common_fields/properties_field.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/common_fields/properties_field.tsx @@ -6,9 +6,9 @@ */ import React, { FunctionComponent } from 'react'; +import { EuiComboBoxProps } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { EuiComboBoxOptionOption } from '@elastic/eui'; import { ComboBoxField, FIELD_TYPES, UseField } from '../../../../../../../shared_imports'; import { FieldsConfig, to } from '../shared'; @@ -29,10 +29,10 @@ const fieldsConfig: FieldsConfig = { interface Props { helpText?: React.ReactNode; - propertyOptions?: EuiComboBoxOptionOption[]; + euiFieldProps?: EuiComboBoxProps; } -export const PropertiesField: FunctionComponent = ({ helpText, propertyOptions }) => { +export const PropertiesField: FunctionComponent = ({ helpText, euiFieldProps }) => { return ( = ({ helpText, propertyOp }} component={ComboBoxField} path="fields.properties" - componentProps={{ - euiFieldProps: { - options: propertyOptions || [], - noSuggestions: !propertyOptions, - }, - }} + componentProps={{ euiFieldProps }} /> ); }; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/user_agent.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/user_agent.tsx index 893e52bcc0073..2b5a68f799b7e 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/user_agent.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/user_agent.tsx @@ -6,20 +6,20 @@ */ import React, { FunctionComponent } from 'react'; -import { EuiCode } from '@elastic/eui'; +import { EuiCode, EuiBetaBadge, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { EuiComboBoxOptionOption } from '@elastic/eui'; -import { FIELD_TYPES, UseField, Field } from '../../../../../../shared_imports'; +import { FIELD_TYPES, ToggleField, UseField, Field } from '../../../../../../shared_imports'; -import { FieldsConfig, from } from './shared'; +import { FieldsConfig, from, to } from './shared'; import { IgnoreMissingField } from './common_fields/ignore_missing_field'; import { FieldNameField } from './common_fields/field_name_field'; import { TargetField } from './common_fields/target_field'; import { PropertiesField } from './common_fields/properties_field'; -const propertyOptions: EuiComboBoxOptionOption[] = [ +const propertyOptions: Array> = [ { label: 'name' }, { label: 'os' }, { label: 'device' }, @@ -47,6 +47,18 @@ const fieldsConfig: FieldsConfig = { } ), }, + extract_device_type: { + type: FIELD_TYPES.TOGGLE, + defaultValue: false, + deserializer: to.booleanOrUndef, + serializer: from.undefinedIfValue(false), + helpText: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.userAgentForm.extractDeviceTypeFieldHelpText', + { + defaultMessage: 'Extracts device type from the user agent string.', + } + ), + }, }; export const UserAgent: FunctionComponent = () => { @@ -59,7 +71,12 @@ export const UserAgent: FunctionComponent = () => { )} /> - + { 'xpack.ingestPipelines.pipelineEditor.userAgentForm.propertiesFieldHelpText', { defaultMessage: 'Properties added to the target field.' } )} - propertyOptions={propertyOptions} + euiFieldProps={{ + options: propertyOptions, + noSuggestions: false, + 'data-test-subj': 'propertiesValueField', + }} + /> + + + + + + + + + + ), + }} /> From 9a1f5a4a7a81603f1b11ff722c3c1c3707e00ca9 Mon Sep 17 00:00:00 2001 From: Peter Pisljar Date: Thu, 24 Jun 2021 12:23:41 +0200 Subject: [PATCH 10/69] switching to peggy (#103169) --- packages/kbn-interpreter/BUILD.bazel | 8 ++++---- .../grammar/{grammar.pegjs => grammar.peggy} | 0 2 files changed, 4 insertions(+), 4 deletions(-) rename packages/kbn-interpreter/grammar/{grammar.pegjs => grammar.peggy} (100%) diff --git a/packages/kbn-interpreter/BUILD.bazel b/packages/kbn-interpreter/BUILD.bazel index 4492faabfdf81..c29faf65638ca 100644 --- a/packages/kbn-interpreter/BUILD.bazel +++ b/packages/kbn-interpreter/BUILD.bazel @@ -1,5 +1,5 @@ load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") -load("@npm//pegjs:index.bzl", "pegjs") +load("@npm//peggy:index.bzl", "peggy") load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") PKG_BASE_NAME = "kbn-interpreter" @@ -37,10 +37,10 @@ TYPES_DEPS = [ DEPS = SRC_DEPS + TYPES_DEPS -pegjs( +peggy( name = "grammar", data = [ - ":grammar/grammar.pegjs" + ":grammar/grammar.peggy" ], output_dir = True, args = [ @@ -48,7 +48,7 @@ pegjs( "expression,argument", "-o", "$(@D)/index.js", - "./%s/grammar/grammar.pegjs" % package_name() + "./%s/grammar/grammar.peggy" % package_name() ], ) diff --git a/packages/kbn-interpreter/grammar/grammar.pegjs b/packages/kbn-interpreter/grammar/grammar.peggy similarity index 100% rename from packages/kbn-interpreter/grammar/grammar.pegjs rename to packages/kbn-interpreter/grammar/grammar.peggy From fa71c6d7ac5abfcf79428fc120561d7cc9beb22f Mon Sep 17 00:00:00 2001 From: Pete Harverson Date: Thu, 24 Jun 2021 11:31:30 +0100 Subject: [PATCH 11/69] [ML] Transforms: Converts management pages to new layout (#102648) * [ML] Transforms: Converts management pages to new layout * [ML] Fix vertical centering of error state in app Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/transform/public/app/app.tsx | 36 ++-- .../public/app/components/section_error.tsx | 18 +- .../components/with_privileges.tsx | 44 ++--- .../clone_transform_section.tsx | 77 +++++---- .../create_transform_section.tsx | 73 ++++---- .../transform_list.test.tsx.snap | 57 ++++--- .../transform_list/transform_list.tsx | 54 +++--- .../transform_management_section.tsx | 156 ++++++++++-------- 8 files changed, 287 insertions(+), 228 deletions(-) diff --git a/x-pack/plugins/transform/public/app/app.tsx b/x-pack/plugins/transform/public/app/app.tsx index d4936783a0297..9219f29e4d9f0 100644 --- a/x-pack/plugins/transform/public/app/app.tsx +++ b/x-pack/plugins/transform/public/app/app.tsx @@ -10,7 +10,7 @@ import { render, unmountComponentAtNode } from 'react-dom'; import { Router, Route, Switch } from 'react-router-dom'; import { ScopedHistory } from 'kibana/public'; -import { EuiErrorBoundary } from '@elastic/eui'; +import { EuiErrorBoundary, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -35,7 +35,7 @@ export const App: FC<{ history: ScopedHistory }> = ({ history }) => { title={ } error={apiError} @@ -44,21 +44,23 @@ export const App: FC<{ history: ScopedHistory }> = ({ history }) => { } return ( -
- - - - - - - -
+ + + + + + + + + + + ); }; diff --git a/x-pack/plugins/transform/public/app/components/section_error.tsx b/x-pack/plugins/transform/public/app/components/section_error.tsx index 2af0c19fb8817..964c13d775d4b 100644 --- a/x-pack/plugins/transform/public/app/components/section_error.tsx +++ b/x-pack/plugins/transform/public/app/components/section_error.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiCallOut } from '@elastic/eui'; +import { EuiEmptyPrompt, EuiPageContent } from '@elastic/eui'; import React from 'react'; interface Props { @@ -23,9 +23,17 @@ export const SectionError: React.FunctionComponent = ({ const errorMessage = error?.message ?? JSON.stringify(error, null, 2); return ( - -
{errorMessage}
- {actions ? actions : null} -
+ + {title}} + body={ +

+

{errorMessage}
+ {actions ? actions : null} +

+ } + /> +
); }; diff --git a/x-pack/plugins/transform/public/app/lib/authorization/components/with_privileges.tsx b/x-pack/plugins/transform/public/app/lib/authorization/components/with_privileges.tsx index ef009e6a125e7..cdf4407b4233f 100644 --- a/x-pack/plugins/transform/public/app/lib/authorization/components/with_privileges.tsx +++ b/x-pack/plugins/transform/public/app/lib/authorization/components/with_privileges.tsx @@ -7,7 +7,7 @@ import React, { useContext, FC } from 'react'; -import { EuiPageContent } from '@elastic/eui'; +import { EuiFlexItem, EuiFlexGroup, EuiPageContent } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -74,27 +74,31 @@ const MissingClusterPrivileges: FC = ({ missingPrivileges, privilegesCount, }) => ( - - - } - message={ - + + + + } + message={ + + } /> - } - /> - + + + ); export const PrivilegesWrapper: FC<{ privileges: string | string[] }> = ({ diff --git a/x-pack/plugins/transform/public/app/sections/clone_transform/clone_transform_section.tsx b/x-pack/plugins/transform/public/app/sections/clone_transform/clone_transform_section.tsx index e4ecc0418d782..8aecf403186c5 100644 --- a/x-pack/plugins/transform/public/app/sections/clone_transform/clone_transform_section.tsx +++ b/x-pack/plugins/transform/public/app/sections/clone_transform/clone_transform_section.tsx @@ -15,12 +15,9 @@ import { i18n } from '@kbn/i18n'; import { EuiButtonEmpty, EuiCallOut, - EuiFlexGroup, - EuiFlexItem, - EuiPageContent, EuiPageContentBody, + EuiPageHeader, EuiSpacer, - EuiTitle, } from '@elastic/eui'; import { APP_CREATE_TRANSFORM_CLUSTER_PRIVILEGES } from '../../../../common/constants'; @@ -105,37 +102,38 @@ export const CloneTransformSection: FC = ({ match, location }) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + const docsLink = ( + + + + ); + return ( - - - - -

- -

-
- - - - - -
-
- - - {typeof errorMessage !== 'undefined' && ( + + } + rightSideItems={[docsLink]} + bottomBorder + /> + + + + + {typeof errorMessage !== 'undefined' && ( + <> = ({ match, location }) => { >
{JSON.stringify(errorMessage)}
- )} - {searchItems !== undefined && isInitialized === true && transformConfig !== undefined && ( - - )} -
-
+ + + )} + {searchItems !== undefined && isInitialized === true && transformConfig !== undefined && ( + + )} +
); }; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/create_transform_section.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/create_transform_section.tsx index b88eb8ce48601..d736bd60f2df6 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/create_transform_section.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/create_transform_section.tsx @@ -13,12 +13,9 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButtonEmpty, EuiCallOut, - EuiFlexGroup, - EuiFlexItem, - EuiPageContent, EuiPageContentBody, + EuiPageHeader, EuiSpacer, - EuiTitle, } from '@elastic/eui'; import { APP_CREATE_TRANSFORM_CLUSTER_PRIVILEGES } from '../../../../common/constants'; @@ -42,42 +39,44 @@ export const CreateTransformSection: FC = ({ match }) => { const { error: searchItemsError, searchItems } = useSearchItems(match.params.savedObjectId); + const docsLink = ( + + + + ); + return ( - - - - -

- -

-
- - - - - -
-
- - - {searchItemsError !== undefined && ( + + } + rightSideItems={[docsLink]} + bottomBorder + /> + + + + + {searchItemsError !== undefined && ( + <> - )} - {searchItems !== undefined && } - -
+ + + )} + {searchItems !== undefined && } +
); }; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/transform_list.test.tsx.snap b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/transform_list.test.tsx.snap index e2de4c0ea1f6c..cf80421711355 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/transform_list.test.tsx.snap +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/transform_list.test.tsx.snap @@ -1,23 +1,42 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Transform: Transform List Minimal initialization 1`] = ` - - Create your first transform - , - ] - } - data-test-subj="transformNoTransformsFound" - title={ -

- No transforms found -

- } -/> + + + + + + Create your first transform + , + ] + } + data-test-subj="transformNoTransformsFound" + title={ +

+ No transforms found +

+ } + /> +
+
+
`; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.tsx index bacf8f9deccae..ab30f4793a315 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.tsx @@ -10,12 +10,15 @@ import React, { MouseEventHandler, FC, useContext, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { + EuiButton, EuiButtonEmpty, EuiButtonIcon, EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, + EuiPageContent, EuiPopover, + EuiSpacer, EuiTitle, EuiInMemoryTable, EuiSearchBarProps, @@ -135,27 +138,36 @@ export const TransformList: FC = ({ if (transforms.length === 0) { return ( - - {i18n.translate('xpack.transform.list.emptyPromptTitle', { - defaultMessage: 'No transforms found', - })} - - } - actions={[ - - {i18n.translate('xpack.transform.list.emptyPromptButtonText', { - defaultMessage: 'Create your first transform', - })} - , - ]} - data-test-subj="transformNoTransformsFound" - /> + + + + + + {i18n.translate('xpack.transform.list.emptyPromptTitle', { + defaultMessage: 'No transforms found', + })} + + } + actions={[ + + {i18n.translate('xpack.transform.list.emptyPromptButtonText', { + defaultMessage: 'Create your first transform', + })} + , + ]} + data-test-subj="transformNoTransformsFound" + /> + + + ); } diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/transform_management_section.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/transform_management_section.tsx index cc4c502f21eb5..2479d34f1579a 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/transform_management_section.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/transform_management_section.tsx @@ -5,23 +5,21 @@ * 2.0. */ -import React, { FC, Fragment, useEffect, useState } from 'react'; +import React, { FC, useEffect, useState } from 'react'; -import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButtonEmpty, - EuiCallOut, + EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, EuiLoadingContent, EuiModal, EuiPageContent, EuiPageContentBody, + EuiPageHeader, EuiSpacer, - EuiText, - EuiTitle, } from '@elastic/eui'; import { APP_GET_TRANSFORM_CLUSTER_PRIVILEGES } from '../../../../common/constants'; @@ -77,73 +75,91 @@ export const TransformManagement: FC = () => { setSavedObjectId(id); }; + const docsLink = ( + + + + ); + return ( - - - - - -

- -

-
- - - - - -
-
- - - + <> + - - - - - {!isInitialized && } - {isInitialized && ( - <> - - - {typeof errorMessage !== 'undefined' && ( - -
{JSON.stringify(errorMessage)}
-
- )} - {typeof errorMessage === 'undefined' && ( - - )} - - )} -
-
+ + } + description={ + + } + rightSideItems={[docsLink]} + bottomBorder + /> + + + + + {!isInitialized && } + {isInitialized && ( + <> + + + {typeof errorMessage !== 'undefined' && ( + + + + + + + + } + body={ +

+

{JSON.stringify(errorMessage)}
+

+ } + actions={[]} + /> +
+
+
+ )} + {typeof errorMessage === 'undefined' && ( + + )} + + )} +
+ {isSearchSelectionVisible && ( { )} -
+ ); }; From d44f9fe6e6fa85ca411ac9c9e89c7e6711d09753 Mon Sep 17 00:00:00 2001 From: Nathan L Smith Date: Thu, 24 Jun 2021 06:29:53 -0500 Subject: [PATCH 12/69] Use observability plugin breadcrumbs in APM (#103168) Both APM and Observability plugins have a `useBreadcrumbs` hook. APM's takes the whole list of route definitions, creates the whole path of breadcrumbs, and has an effect to set the breadcrumbs and the page title. The Observability plugin's `useBreadcrumbs` just takes an array of breadcrumb objects, adds onclick handlers for them, and has an effect to set the breadcrumbs and the page title. Rename APM's `useBreadcrumbs` to `useApmBreadcrumbs`. It still constructs the path based on the routes and the current route, but then just calls out to the Observability plugin's `useBreadcrumbs` to do the breadcrumb and title setting. Now all APM breadcrumbs begin with "Observability" which links to the Observability overview, but the rest of them remain the same. --- .../public/components/routing/app_root.tsx | 4 +- .../apm_plugin/mock_apm_plugin_context.tsx | 1 + ....test.tsx => use_apm_breadcrumbs.test.tsx} | 58 ++++--------------- ..._breadcrumbs.ts => use_apm_breadcrumbs.ts} | 29 ++-------- 4 files changed, 18 insertions(+), 74 deletions(-) rename x-pack/plugins/apm/public/hooks/{use_breadcrumbs.test.tsx => use_apm_breadcrumbs.test.tsx} (79%) rename x-pack/plugins/apm/public/hooks/{use_breadcrumbs.ts => use_apm_breadcrumbs.ts} (85%) diff --git a/x-pack/plugins/apm/public/components/routing/app_root.tsx b/x-pack/plugins/apm/public/components/routing/app_root.tsx index 2bb387ae315ff..8fc59a01eeca0 100644 --- a/x-pack/plugins/apm/public/components/routing/app_root.tsx +++ b/x-pack/plugins/apm/public/components/routing/app_root.tsx @@ -24,7 +24,7 @@ import { } from '../../context/apm_plugin/apm_plugin_context'; import { LicenseProvider } from '../../context/license/license_context'; import { UrlParamsProvider } from '../../context/url_params_context/url_params_context'; -import { useBreadcrumbs } from '../../hooks/use_breadcrumbs'; +import { useApmBreadcrumbs } from '../../hooks/use_apm_breadcrumbs'; import { ApmPluginStartDeps } from '../../plugin'; import { HeaderMenuPortal } from '../../../../observability/public'; import { ApmHeaderActionMenu } from '../shared/apm_header_action_menu'; @@ -79,7 +79,7 @@ export function ApmAppRoot({ } function MountApmHeaderActionMenu() { - useBreadcrumbs(apmRouteConfig); + useApmBreadcrumbs(apmRouteConfig); const { setHeaderActionMenu } = useApmPluginContext().appMountParameters; return ( diff --git a/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx b/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx index a16f81826636b..bcc1932dde7cb 100644 --- a/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx +++ b/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx @@ -44,6 +44,7 @@ const mockCore = { ml: {}, }, currentAppId$: new Observable(), + getUrlForApp: (appId: string) => '', navigateToUrl: (url: string) => {}, }, chrome: { diff --git a/x-pack/plugins/apm/public/hooks/use_breadcrumbs.test.tsx b/x-pack/plugins/apm/public/hooks/use_apm_breadcrumbs.test.tsx similarity index 79% rename from x-pack/plugins/apm/public/hooks/use_breadcrumbs.test.tsx rename to x-pack/plugins/apm/public/hooks/use_apm_breadcrumbs.test.tsx index 64990651b52bb..1cdb84c324750 100644 --- a/x-pack/plugins/apm/public/hooks/use_breadcrumbs.test.tsx +++ b/x-pack/plugins/apm/public/hooks/use_apm_breadcrumbs.test.tsx @@ -15,14 +15,15 @@ import { mockApmPluginContextValue, MockApmPluginContextWrapper, } from '../context/apm_plugin/mock_apm_plugin_context'; -import { useBreadcrumbs } from './use_breadcrumbs'; +import { useApmBreadcrumbs } from './use_apm_breadcrumbs'; +import { useBreadcrumbs } from '../../../observability/public'; + +jest.mock('../../../observability/public'); function createWrapper(path: string) { return ({ children }: { children?: ReactNode }) => { const value = (produce(mockApmPluginContextValue, (draft) => { draft.core.application.navigateToUrl = (url: string) => Promise.resolve(); - draft.core.chrome.docTitle.change = changeTitle; - draft.core.chrome.setBreadcrumbs = setBreadcrumbs; }) as unknown) as ApmPluginContextValue; return ( @@ -36,27 +37,18 @@ function createWrapper(path: string) { } function mountBreadcrumb(path: string) { - renderHook(() => useBreadcrumbs(apmRouteConfig), { + renderHook(() => useApmBreadcrumbs(apmRouteConfig), { wrapper: createWrapper(path), }); } -const changeTitle = jest.fn(); -const setBreadcrumbs = jest.fn(); - -describe('useBreadcrumbs', () => { - it('changes the page title', () => { - mountBreadcrumb('/'); - - expect(changeTitle).toHaveBeenCalledWith(['APM']); - }); - +describe('useApmBreadcrumbs', () => { test('/services/:serviceName/errors/:groupId', () => { mountBreadcrumb( '/services/opbeans-node/errors/myGroupId?kuery=myKuery&rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0' ); - expect(setBreadcrumbs).toHaveBeenCalledWith( + expect(useBreadcrumbs).toHaveBeenCalledWith( expect.arrayContaining([ expect.objectContaining({ text: 'APM', @@ -81,20 +73,12 @@ describe('useBreadcrumbs', () => { expect.objectContaining({ text: 'myGroupId', href: undefined }), ]) ); - - expect(changeTitle).toHaveBeenCalledWith([ - 'myGroupId', - 'Errors', - 'opbeans-node', - 'Services', - 'APM', - ]); }); test('/services/:serviceName/errors', () => { mountBreadcrumb('/services/opbeans-node/errors?kuery=myKuery'); - expect(setBreadcrumbs).toHaveBeenCalledWith( + expect(useBreadcrumbs).toHaveBeenCalledWith( expect.arrayContaining([ expect.objectContaining({ text: 'APM', @@ -111,19 +95,12 @@ describe('useBreadcrumbs', () => { expect.objectContaining({ text: 'Errors', href: undefined }), ]) ); - - expect(changeTitle).toHaveBeenCalledWith([ - 'Errors', - 'opbeans-node', - 'Services', - 'APM', - ]); }); test('/services/:serviceName/transactions', () => { mountBreadcrumb('/services/opbeans-node/transactions?kuery=myKuery'); - expect(setBreadcrumbs).toHaveBeenCalledWith( + expect(useBreadcrumbs).toHaveBeenCalledWith( expect.arrayContaining([ expect.objectContaining({ text: 'APM', @@ -140,13 +117,6 @@ describe('useBreadcrumbs', () => { expect.objectContaining({ text: 'Transactions', href: undefined }), ]) ); - - expect(changeTitle).toHaveBeenCalledWith([ - 'Transactions', - 'opbeans-node', - 'Services', - 'APM', - ]); }); test('/services/:serviceName/transactions/view?transactionName=my-transaction-name', () => { @@ -154,7 +124,7 @@ describe('useBreadcrumbs', () => { '/services/opbeans-node/transactions/view?kuery=myKuery&transactionName=my-transaction-name' ); - expect(setBreadcrumbs).toHaveBeenCalledWith( + expect(useBreadcrumbs).toHaveBeenCalledWith( expect.arrayContaining([ expect.objectContaining({ text: 'APM', @@ -179,13 +149,5 @@ describe('useBreadcrumbs', () => { }), ]) ); - - expect(changeTitle).toHaveBeenCalledWith([ - 'my-transaction-name', - 'Transactions', - 'opbeans-node', - 'Services', - 'APM', - ]); }); }); diff --git a/x-pack/plugins/apm/public/hooks/use_breadcrumbs.ts b/x-pack/plugins/apm/public/hooks/use_apm_breadcrumbs.ts similarity index 85% rename from x-pack/plugins/apm/public/hooks/use_breadcrumbs.ts rename to x-pack/plugins/apm/public/hooks/use_apm_breadcrumbs.ts index d907c27319d26..d64bcadf79577 100644 --- a/x-pack/plugins/apm/public/hooks/use_breadcrumbs.ts +++ b/x-pack/plugins/apm/public/hooks/use_apm_breadcrumbs.ts @@ -7,14 +7,15 @@ import { History, Location } from 'history'; import { ChromeBreadcrumb } from 'kibana/public'; -import { MouseEvent, ReactNode, useEffect } from 'react'; +import { MouseEvent } from 'react'; import { + match as Match, matchPath, RouteComponentProps, useHistory, - match as Match, useLocation, } from 'react-router-dom'; +import { useBreadcrumbs } from '../../../observability/public'; import { APMRouteDefinition, BreadcrumbTitle } from '../application/routes'; import { getAPMHref } from '../components/shared/Links/apm/APMLink'; import { useApmPluginContext } from '../context/apm_plugin/use_apm_plugin_context'; @@ -164,33 +165,17 @@ function routeDefinitionsToBreadcrumbs({ return breadcrumbs; } -/** - * Get an array for a page title from a list of breadcrumbs - */ -function getTitleFromBreadcrumbs(breadcrumbs: ChromeBreadcrumb[]): string[] { - function removeNonStrings(item: ReactNode): item is string { - return typeof item === 'string'; - } - - return breadcrumbs - .map(({ text }) => text) - .reverse() - .filter(removeNonStrings); -} - /** * Determine the breadcrumbs from the routes, set them, and update the page * title when the route changes. */ -export function useBreadcrumbs(routes: APMRouteDefinition[]) { +export function useApmBreadcrumbs(routes: APMRouteDefinition[]) { const history = useHistory(); const location = useLocation(); const { search } = location; const { core } = useApmPluginContext(); const { basePath } = core.http; const { navigateToUrl } = core.application; - const { docTitle, setBreadcrumbs } = core.chrome; - const changeTitle = docTitle.change; function wrappedGetAPMHref(path: string) { return getAPMHref({ basePath, path, search }); @@ -206,10 +191,6 @@ export function useBreadcrumbs(routes: APMRouteDefinition[]) { wrappedGetAPMHref, navigateToUrl, }); - const title = getTitleFromBreadcrumbs(breadcrumbs); - useEffect(() => { - changeTitle(title); - setBreadcrumbs(breadcrumbs); - }, [breadcrumbs, changeTitle, location, title, setBreadcrumbs]); + useBreadcrumbs(breadcrumbs); } From 7a3d61fb671fe549444dcb266b46d4664d8f3dc5 Mon Sep 17 00:00:00 2001 From: Dzmitry Lemechko Date: Thu, 24 Jun 2021 13:45:39 +0200 Subject: [PATCH 13/69] [load testing] adjust ES heap size (#101906) * [load testing] increase es heap to 2g * update default simulation * [heap size] 4g * [heap size] 6g * [load testing] es heap size 8g Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/test/load/config.ts | 1 + x-pack/test/load/runner.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/load/config.ts b/x-pack/test/load/config.ts index 514440fd73f46..8f8708d155fb1 100644 --- a/x-pack/test/load/config.ts +++ b/x-pack/test/load/config.ts @@ -30,6 +30,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { esTestCluster: { ...xpackFunctionalTestsConfig.get('esTestCluster'), serverArgs: [...xpackFunctionalTestsConfig.get('esTestCluster.serverArgs')], + esJavaOpts: '-Xms8g -Xmx8g', }, kbnTestServer: { diff --git a/x-pack/test/load/runner.ts b/x-pack/test/load/runner.ts index 3e7a4817eeef1..2d379391b2089 100644 --- a/x-pack/test/load/runner.ts +++ b/x-pack/test/load/runner.ts @@ -18,7 +18,7 @@ const simulationPackage = 'org.kibanaLoadTest.simulation'; const simulationFIleExtension = '.scala'; const gatlingProjectRootPath: string = process.env.GATLING_PROJECT_PATH || resolve(REPO_ROOT, '../kibana-load-testing'); -const simulationEntry: string = process.env.GATLING_SIMULATIONS || 'DemoJourney'; +const simulationEntry: string = process.env.GATLING_SIMULATIONS || 'branch.DemoJourney'; if (!Fs.existsSync(gatlingProjectRootPath)) { throw createFlagError( From 9b9c47b269a437dad69ddb384b2ea6b124a60e5b Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Thu, 24 Jun 2021 14:45:15 +0200 Subject: [PATCH 14/69] [Fleet] Fix double policy header layout (#103076) * Fix double policy header layout - Use the default page title without tabs while loading the add integration view * remove unused import Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../fleet/public/applications/fleet/app.tsx | 30 +++++++++----- .../fleet/layouts/{ => default}/default.tsx | 35 +++------------- .../layouts/default/default_page_title.tsx | 40 +++++++++++++++++++ .../fleet/layouts/default/index.ts | 9 +++++ .../applications/fleet/layouts/index.tsx | 2 +- 5 files changed, 75 insertions(+), 41 deletions(-) rename x-pack/plugins/fleet/public/applications/fleet/layouts/{ => default}/default.tsx (65%) create mode 100644 x-pack/plugins/fleet/public/applications/fleet/layouts/default/default_page_title.tsx create mode 100644 x-pack/plugins/fleet/public/applications/fleet/layouts/default/index.ts diff --git a/x-pack/plugins/fleet/public/applications/fleet/app.tsx b/x-pack/plugins/fleet/public/applications/fleet/app.tsx index c4cc4d92f5d95..8be6232733def 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/app.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/app.tsx @@ -5,12 +5,13 @@ * 2.0. */ +import type { FunctionComponent } from 'react'; import React, { memo, useEffect, useState } from 'react'; import type { AppMountParameters } from 'kibana/public'; import { EuiCode, EuiEmptyPrompt, EuiErrorBoundary, EuiPanel, EuiPortal } from '@elastic/eui'; import type { History } from 'history'; import { createHashHistory } from 'history'; -import { Router, Redirect, Route, Switch } from 'react-router-dom'; +import { Router, Redirect, Route, Switch, useRouteMatch } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import styled from 'styled-components'; @@ -39,7 +40,7 @@ import { Error, Loading, SettingFlyout, FleetSetupLoading } from './components'; import type { UIExtensionsStorage } from './types'; import { FLEET_ROUTING_PATHS } from './constants'; -import { DefaultLayout, WithoutHeaderLayout } from './layouts'; +import { DefaultLayout, DefaultPageTitle, WithoutHeaderLayout, WithHeaderLayout } from './layouts'; import { AgentPolicyApp } from './sections/agent_policy'; import { DataStreamApp } from './sections/data_stream'; import { AgentsApp } from './sections/agents'; @@ -48,11 +49,18 @@ import { EnrollmentTokenListPage } from './sections/agents/enrollment_token_list const FEEDBACK_URL = 'https://ela.st/fleet-feedback'; -const ErrorLayout = ({ children }: { children: JSX.Element }) => ( +const ErrorLayout: FunctionComponent<{ isAddIntegrationsPath: boolean }> = ({ + isAddIntegrationsPath, + children, +}) => ( - - {children} - + {isAddIntegrationsPath ? ( + }>{children} + ) : ( + + {children} + + )} ); @@ -71,6 +79,8 @@ export const WithPermissionsAndSetup: React.FC = memo(({ children }) => { const [isInitialized, setIsInitialized] = useState(false); const [initializationError, setInitializationError] = useState(null); + const isAddIntegrationsPath = !!useRouteMatch(FLEET_ROUTING_PATHS.add_integration_to_policy); + useEffect(() => { (async () => { setIsPermissionsLoading(false); @@ -109,7 +119,7 @@ export const WithPermissionsAndSetup: React.FC = memo(({ children }) => { if (isPermissionsLoading || permissionsError) { return ( - + {isPermissionsLoading ? ( ) : permissionsError === 'REQUEST_ERROR' ? ( @@ -168,7 +178,7 @@ export const WithPermissionsAndSetup: React.FC = memo(({ children }) => { if (!isInitialized || initializationError) { return ( - + {initializationError ? ( - - - + diff --git a/x-pack/plugins/fleet/public/applications/fleet/layouts/default.tsx b/x-pack/plugins/fleet/public/applications/fleet/layouts/default/default.tsx similarity index 65% rename from x-pack/plugins/fleet/public/applications/fleet/layouts/default.tsx rename to x-pack/plugins/fleet/public/applications/fleet/layouts/default/default.tsx index f312ff374d792..c6ef212b3995e 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/layouts/default.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/layouts/default/default.tsx @@ -6,12 +6,13 @@ */ import React from 'react'; -import { EuiText, EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import type { Section } from '../sections'; -import { useLink, useConfig } from '../hooks'; -import { WithHeaderLayout } from '../../../layouts'; +import type { Section } from '../../sections'; +import { useLink, useConfig } from '../../hooks'; +import { WithHeaderLayout } from '../../../../layouts'; + +import { DefaultPageTitle } from './default_page_title'; interface Props { section?: Section; @@ -24,31 +25,7 @@ export const DefaultLayout: React.FunctionComponent = ({ section, childre return ( - - - - -

- -

-
-
-
-
- - -

- -

-
-
- - } + leftColumn={} tabs={[ { name: ( diff --git a/x-pack/plugins/fleet/public/applications/fleet/layouts/default/default_page_title.tsx b/x-pack/plugins/fleet/public/applications/fleet/layouts/default/default_page_title.tsx new file mode 100644 index 0000000000000..e525a059b7837 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/layouts/default/default_page_title.tsx @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { FunctionComponent } from 'react'; +import React from 'react'; + +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiText } from '@elastic/eui'; + +export const DefaultPageTitle: FunctionComponent = () => { + return ( + + + + + +

+ +

+
+
+
+
+ + +

+ +

+
+
+
+ ); +}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/layouts/default/index.ts b/x-pack/plugins/fleet/public/applications/fleet/layouts/default/index.ts new file mode 100644 index 0000000000000..9b0d3ee06138f --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/layouts/default/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { DefaultLayout } from './default'; +export { DefaultPageTitle } from './default_page_title'; diff --git a/x-pack/plugins/fleet/public/applications/fleet/layouts/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/layouts/index.tsx index 71cb8d3aeeb36..0c07f1ffecb79 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/layouts/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/layouts/index.tsx @@ -7,4 +7,4 @@ export * from '../../../layouts'; -export { DefaultLayout } from './default'; +export { DefaultLayout, DefaultPageTitle } from './default'; From 2a8f3eb2f921cfb1d6a5faaac59cbac5aac24910 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Thu, 24 Jun 2021 14:45:35 +0200 Subject: [PATCH 15/69] [Fleet] Fix staleness bug in "Add agent" flyout (#103095) * * Fix stale enrollment api token bug * Refactored naming * raise the state of the selected enrollment api key to parent to avoid state sync issues * removed consts for onKeyChange and selectedApiKeyId * fix typo Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- ...advanced_agent_authentication_settings.tsx | 38 ++++++++---------- .../agent_policy_selection.tsx | 40 ++++++++++--------- .../managed_instructions.tsx | 11 ++--- .../agent_enrollment_flyout/steps.tsx | 12 ++++-- 4 files changed, 52 insertions(+), 49 deletions(-) diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/advanced_agent_authentication_settings.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/advanced_agent_authentication_settings.tsx index 25602b7e108fd..96fab27a55050 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/advanced_agent_authentication_settings.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/advanced_agent_authentication_settings.tsx @@ -21,20 +21,19 @@ import { interface Props { agentPolicyId?: string; + selectedApiKeyId?: string; onKeyChange: (key?: string) => void; } export const AdvancedAgentAuthenticationSettings: FunctionComponent = ({ agentPolicyId, + selectedApiKeyId, onKeyChange, }) => { const { notifications } = useStartServices(); const [enrollmentAPIKeys, setEnrollmentAPIKeys] = useState( [] ); - // TODO: Remove this piece of state since we don't need it here. The currently selected enrollment API key only - // needs to live on the form - const [selectedEnrollmentApiKey, setSelectedEnrollmentApiKey] = useState(); const [isLoadingEnrollmentKey, setIsLoadingEnrollmentKey] = useState(false); const [isAuthenticationSettingsOpen, setIsAuthenticationSettingsOpen] = useState(false); @@ -51,7 +50,7 @@ export const AdvancedAgentAuthenticationSettings: FunctionComponent = ({ return; } setEnrollmentAPIKeys([res.data.item]); - setSelectedEnrollmentApiKey(res.data.item.id); + onKeyChange(res.data.item.id); notifications.toasts.addSuccess( i18n.translate('xpack.fleet.newEnrollmentKey.keyCreatedToasts', { defaultMessage: 'Enrollment token created', @@ -66,15 +65,6 @@ export const AdvancedAgentAuthenticationSettings: FunctionComponent = ({ } }; - useEffect( - function triggerOnKeyChangeEffect() { - if (onKeyChange) { - onKeyChange(selectedEnrollmentApiKey); - } - }, - [onKeyChange, selectedEnrollmentApiKey] - ); - useEffect( function useEnrollmentKeysForAgentPolicyEffect() { if (!agentPolicyId) { @@ -97,9 +87,13 @@ export const AdvancedAgentAuthenticationSettings: FunctionComponent = ({ throw new Error('No data while fetching enrollment API keys'); } - setEnrollmentAPIKeys( - res.data.list.filter((key) => key.policy_id === agentPolicyId && key.active === true) + const enrollmentAPIKeysResponse = res.data.list.filter( + (key) => key.policy_id === agentPolicyId && key.active === true ); + + setEnrollmentAPIKeys(enrollmentAPIKeysResponse); + // Default to the first enrollment key if there is one. + onKeyChange(enrollmentAPIKeysResponse[0]?.id); } catch (error) { notifications.toasts.addError(error, { title: 'Error', @@ -108,21 +102,21 @@ export const AdvancedAgentAuthenticationSettings: FunctionComponent = ({ } fetchEnrollmentAPIKeys(); }, - [agentPolicyId, notifications.toasts] + [onKeyChange, agentPolicyId, notifications.toasts] ); useEffect( function useDefaultEnrollmentKeyForAgentPolicyEffect() { if ( - !selectedEnrollmentApiKey && + !selectedApiKeyId && enrollmentAPIKeys.length > 0 && enrollmentAPIKeys[0].policy_id === agentPolicyId ) { const enrollmentAPIKeyId = enrollmentAPIKeys[0].id; - setSelectedEnrollmentApiKey(enrollmentAPIKeyId); + onKeyChange(enrollmentAPIKeyId); } }, - [enrollmentAPIKeys, selectedEnrollmentApiKey, agentPolicyId] + [enrollmentAPIKeys, selectedApiKeyId, agentPolicyId, onKeyChange] ); return ( <> @@ -139,14 +133,14 @@ export const AdvancedAgentAuthenticationSettings: FunctionComponent = ({ {isAuthenticationSettingsOpen && ( <> - {enrollmentAPIKeys.length && selectedEnrollmentApiKey ? ( + {enrollmentAPIKeys.length && selectedApiKeyId ? ( ({ value: key.id, text: key.name, }))} - value={selectedEnrollmentApiKey || undefined} + value={selectedApiKeyId || undefined} prepend={ = ({ } onChange={(e) => { - setSelectedEnrollmentApiKey(e.target.value); + onKeyChange(e.target.value); }} /> ) : ( diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_policy_selection.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_policy_selection.tsx index f92b2d4825935..d9d1aa2e77f86 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_policy_selection.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_policy_selection.tsx @@ -22,6 +22,7 @@ type Props = { } & ( | { withKeySelection: true; + selectedApiKeyId?: string; onKeyChange?: (key?: string) => void; } | { @@ -31,9 +32,9 @@ type Props = { const resolveAgentId = ( agentPolicies?: AgentPolicy[], - selectedAgentId?: string + selectedAgentPolicyId?: string ): undefined | string => { - if (agentPolicies && agentPolicies.length && !selectedAgentId) { + if (agentPolicies && agentPolicies.length && !selectedAgentPolicyId) { if (agentPolicies.length === 1) { return agentPolicies[0].id; } @@ -44,33 +45,33 @@ const resolveAgentId = ( } } - return selectedAgentId; + return selectedAgentPolicyId; }; export const EnrollmentStepAgentPolicy: React.FC = (props) => { - const { withKeySelection, agentPolicies, onAgentPolicyChange, excludeFleetServer } = props; - const onKeyChange = props.withKeySelection && props.onKeyChange; - const [selectedAgentId, setSelectedAgentId] = useState( + const { agentPolicies, onAgentPolicyChange, excludeFleetServer } = props; + + const [selectedAgentPolicyId, setSelectedAgentPolicyId] = useState( () => resolveAgentId(agentPolicies, undefined) // no agent id selected yet ); useEffect( function triggerOnAgentPolicyChangeEffect() { if (onAgentPolicyChange) { - onAgentPolicyChange(selectedAgentId); + onAgentPolicyChange(selectedAgentPolicyId); } }, - [selectedAgentId, onAgentPolicyChange] + [selectedAgentPolicyId, onAgentPolicyChange] ); useEffect( function useDefaultAgentPolicyEffect() { - const resolvedId = resolveAgentId(agentPolicies, selectedAgentId); - if (resolvedId !== selectedAgentId) { - setSelectedAgentId(resolvedId); + const resolvedId = resolveAgentId(agentPolicies, selectedAgentPolicyId); + if (resolvedId !== selectedAgentPolicyId) { + setSelectedAgentPolicyId(resolvedId); } }, - [agentPolicies, selectedAgentId] + [agentPolicies, selectedAgentPolicyId] ); return ( @@ -90,25 +91,26 @@ export const EnrollmentStepAgentPolicy: React.FC = (props) => { value: agentPolicy.id, text: agentPolicy.name, }))} - value={selectedAgentId || undefined} - onChange={(e) => setSelectedAgentId(e.target.value)} + value={selectedAgentPolicyId || undefined} + onChange={(e) => setSelectedAgentPolicyId(e.target.value)} aria-label={i18n.translate('xpack.fleet.enrollmentStepAgentPolicy.policySelectAriaLabel', { defaultMessage: 'Agent policy', })} /> - {selectedAgentId && ( + {selectedAgentPolicyId && ( )} - {withKeySelection && onKeyChange && ( + {props.withKeySelection && props.onKeyChange && ( <> )} diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/managed_instructions.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/managed_instructions.tsx index 919f0c3052db9..efae8db377f7f 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/managed_instructions.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/managed_instructions.tsx @@ -62,10 +62,10 @@ export const ManagedInstructions = React.memo( ({ agentPolicy, agentPolicies, viewDataStepContent }) => { const fleetStatus = useFleetStatus(); - const [selectedAPIKeyId, setSelectedAPIKeyId] = useState(); + const [selectedApiKeyId, setSelectedAPIKeyId] = useState(); const [isFleetServerPolicySelected, setIsFleetServerPolicySelected] = useState(false); - const apiKey = useGetOneEnrollmentAPIKey(selectedAPIKeyId); + const apiKey = useGetOneEnrollmentAPIKey(selectedApiKeyId); const settings = useGetSettings(); const fleetServerInstructions = useFleetServerInstructions(apiKey?.data?.item?.policy_id); @@ -84,10 +84,11 @@ export const ManagedInstructions = React.memo( !agentPolicy ? AgentPolicySelectionStep({ agentPolicies, + selectedApiKeyId, setSelectedAPIKeyId, setIsFleetServerPolicySelected, }) - : AgentEnrollmentKeySelectionStep({ agentPolicy, setSelectedAPIKeyId }), + : AgentEnrollmentKeySelectionStep({ agentPolicy, selectedApiKeyId, setSelectedAPIKeyId }), ]; if (isFleetServerPolicySelected) { baseSteps.push( @@ -101,7 +102,7 @@ export const ManagedInstructions = React.memo( title: i18n.translate('xpack.fleet.agentEnrollment.stepEnrollAndRunAgentTitle', { defaultMessage: 'Enroll and start the Elastic Agent', }), - children: selectedAPIKeyId && apiKey.data && ( + children: selectedApiKeyId && apiKey.data && ( ), }); @@ -115,7 +116,7 @@ export const ManagedInstructions = React.memo( }, [ agentPolicy, agentPolicies, - selectedAPIKeyId, + selectedApiKeyId, apiKey.data, isFleetServerPolicySelected, settings.data?.item?.fleet_server_hosts, diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps.tsx index 03cff88e63969..8b12994473e34 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps.tsx @@ -49,14 +49,16 @@ export const DownloadStep = () => { export const AgentPolicySelectionStep = ({ agentPolicies, - setSelectedAPIKeyId, setSelectedPolicyId, - setIsFleetServerPolicySelected, + selectedApiKeyId, + setSelectedAPIKeyId, excludeFleetServer, + setIsFleetServerPolicySelected, }: { agentPolicies?: AgentPolicy[]; - setSelectedAPIKeyId?: (key?: string) => void; setSelectedPolicyId?: (policyId?: string) => void; + selectedApiKeyId?: string; + setSelectedAPIKeyId?: (key?: string) => void; setIsFleetServerPolicySelected?: (selected: boolean) => void; excludeFleetServer?: boolean; }) => { @@ -99,6 +101,7 @@ export const AgentPolicySelectionStep = ({ void; }) => { return { @@ -132,6 +137,7 @@ export const AgentEnrollmentKeySelectionStep = ({ From a50d94908c3202ba462aac8421b3f9acb333f4d2 Mon Sep 17 00:00:00 2001 From: Scotty Bollinger Date: Thu, 24 Jun 2021 07:57:49 -0500 Subject: [PATCH 16/69] [Enterprise Search] Add User management feature (#103173) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Rename method to close both flyouts This is shared with the forthcoming user flyouts closeRoleMappingFlyout -> closeUsersAndRolesFlyout * Add logic for elasticsearch users and single user role mappings * Add logic for various form states - Showing and hiding flyouts - Select and text input values - User created state to turn flyout into a success message state * Add User server routes * Add logic for saving a user * Add User components * Add User list and User flyout to RoleMappings view * Fix path * Rename things - Users & roles -> Users and roles - roleId -> roleMappingId (matches other places in code) - also added a missing prop to the actions col * Set default group when modal closed The UI sets the default group on page load but did not cover the case where the user has chosen a group in a previous interaction and the closed the flyout. This commit adds a method that resets that state when the flyout is closed Part of porting of https://github.com/elastic/ent-search/pull/3865 Specifically: https://github.com/elastic/ent-search/commit/a4131b95dab7c0df97bd78e660f25e09ac3e7cec * Adds tooltip for external attribute This was missed from the design Part of porting of https://github.com/elastic/ent-search/pull/3865 Specifically: https://github.com/elastic/ent-search/commit/03aa349cab4fb32069b64ab8c51a7252ba52e805 * Fix invitations link * Fix incorrect role type Role-> RoleTypes 🤷🏽‍♀️ * Add EuiPortal to Flyout Wasn’t needed in ent-search; already done for RomeMappingFlyout. Hide whitespace changes plskthx * Auth provider deprecation warning in mapping UI Since we're moving fully into Kibana, we're losing our concept of auth providers. In 8.0, role mappings the specify an auth provider will no longer work, so this adds a small deprecation warning in the role mappings table. https://github.com/elastic/ent-search/pull/3885 * Email is no longer required After a slack discussion, it was determined that email should be optional. This commit also fixes another instance of the App Search role type being wrong. * Existing users’ usernames should not be editable * Use EuiLink instead of anchor * Add validation tests * Change URL for users_and_roles Need to change folder and file names but will punt until after 7.14FF I did throw in updating the logic file path * Remove unused import Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../app_search/components/layout/nav.test.tsx | 4 +- .../app_search/components/layout/nav.tsx | 4 +- .../components/role_mappings/role_mapping.tsx | 4 +- .../role_mappings/role_mappings.test.tsx | 42 ++- .../role_mappings/role_mappings.tsx | 31 ++ .../role_mappings/role_mappings_logic.test.ts | 274 +++++++++++++++- .../role_mappings/role_mappings_logic.ts | 181 ++++++++++- .../components/role_mappings/user.test.tsx | 124 +++++++ .../components/role_mappings/user.tsx | 106 ++++++ .../applications/app_search/index.test.tsx | 2 +- .../public/applications/app_search/index.tsx | 6 +- .../public/applications/app_search/routes.ts | 2 +- .../shared/role_mapping/constants.ts | 18 +- .../role_mapping/role_mapping_flyout.test.tsx | 4 +- .../role_mapping/role_mapping_flyout.tsx | 10 +- .../role_mapping/role_mappings_table.test.tsx | 12 +- .../role_mapping/role_mappings_table.tsx | 42 ++- .../role_mapping/user_added_info.test.tsx | 100 +++++- .../shared/role_mapping/user_added_info.tsx | 6 +- .../shared/role_mapping/user_flyout.tsx | 39 ++- .../role_mapping/user_invitation_callout.tsx | 2 +- .../role_mapping/user_selector.test.tsx | 3 +- .../shared/role_mapping/user_selector.tsx | 5 +- .../shared/role_mapping/users_table.tsx | 5 +- .../components/layout/nav.test.tsx | 4 +- .../components/layout/nav.tsx | 6 +- .../workplace_search/constants.ts | 2 +- .../applications/workplace_search/index.tsx | 4 +- .../applications/workplace_search/routes.ts | 2 +- .../views/role_mappings/role_mapping.tsx | 4 +- .../role_mappings/role_mappings.test.tsx | 40 ++- .../views/role_mappings/role_mappings.tsx | 31 ++ .../role_mappings/role_mappings_logic.test.ts | 302 ++++++++++++++++-- .../role_mappings/role_mappings_logic.ts | 192 ++++++++++- .../views/role_mappings/user.test.tsx | 123 +++++++ .../views/role_mappings/user.tsx | 103 ++++++ .../routes/app_search/role_mappings.test.ts | 49 +++ .../server/routes/app_search/role_mappings.ts | 26 ++ .../workplace_search/role_mappings.test.ts | 49 +++ .../routes/workplace_search/role_mappings.ts | 29 ++ 40 files changed, 1868 insertions(+), 124 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/user.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/user.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/user.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/user.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/layout/nav.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/layout/nav.test.tsx index c9f5452e254e1..ce4a118bef095 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/layout/nav.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/layout/nav.test.tsx @@ -100,8 +100,8 @@ describe('useAppSearchNav', () => { }, { id: 'usersRoles', - name: 'Users & roles', - href: '/role_mappings', + name: 'Users and roles', + href: '/users_and_roles', }, ], }, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/layout/nav.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/layout/nav.tsx index c3b8ec642233b..793a36f48fe82 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/layout/nav.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/layout/nav.tsx @@ -13,7 +13,7 @@ import { generateNavLink } from '../../../shared/layout'; import { ROLE_MAPPINGS_TITLE } from '../../../shared/role_mapping/constants'; import { AppLogic } from '../../app_logic'; -import { ENGINES_PATH, SETTINGS_PATH, CREDENTIALS_PATH, ROLE_MAPPINGS_PATH } from '../../routes'; +import { ENGINES_PATH, SETTINGS_PATH, CREDENTIALS_PATH, USERS_AND_ROLES_PATH } from '../../routes'; import { CREDENTIALS_TITLE } from '../credentials'; import { useEngineNav } from '../engine/engine_nav'; import { ENGINES_TITLE } from '../engines'; @@ -57,7 +57,7 @@ export const useAppSearchNav = () => { navItems.push({ id: 'usersRoles', name: ROLE_MAPPINGS_TITLE, - ...generateNavLink({ to: ROLE_MAPPINGS_PATH }), + ...generateNavLink({ to: USERS_AND_ROLES_PATH }), }); } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mapping.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mapping.tsx index b6a9dd72cfd05..dbebd8e46a219 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mapping.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mapping.tsx @@ -28,7 +28,7 @@ export const RoleMapping: React.FC = () => { handleAuthProviderChange, handleRoleChange, handleSaveMapping, - closeRoleMappingFlyout, + closeUsersAndRolesFlyout, } = useActions(RoleMappingsLogic); const { @@ -68,7 +68,7 @@ export const RoleMapping: React.FC = () => { 0} error={roleMappingErrors}> diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings.test.tsx index 308022ccb2e5a..64bf41a50a2f0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings.test.tsx @@ -12,26 +12,39 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { RoleMappingsTable, RoleMappingsHeading } from '../../../shared/role_mapping'; -import { wsRoleMapping } from '../../../shared/role_mapping/__mocks__/roles'; +import { + RoleMappingsTable, + RoleMappingsHeading, + UsersHeading, + UsersEmptyPrompt, +} from '../../../shared/role_mapping'; +import { + asRoleMapping, + asSingleUserRoleMapping, +} from '../../../shared/role_mapping/__mocks__/roles'; import { RoleMapping } from './role_mapping'; import { RoleMappings } from './role_mappings'; +import { User } from './user'; describe('RoleMappings', () => { const initializeRoleMappings = jest.fn(); const initializeRoleMapping = jest.fn(); + const initializeSingleUserRoleMapping = jest.fn(); const handleDeleteMapping = jest.fn(); const mockValues = { - roleMappings: [wsRoleMapping], + roleMappings: [asRoleMapping], dataLoading: false, multipleAuthProvidersConfig: false, + singleUserRoleMappings: [asSingleUserRoleMapping], + singleUserRoleMappingFlyoutOpen: false, }; beforeEach(() => { setMockActions({ initializeRoleMappings, initializeRoleMapping, + initializeSingleUserRoleMapping, handleDeleteMapping, }); setMockValues(mockValues); @@ -50,10 +63,31 @@ describe('RoleMappings', () => { expect(wrapper.find(RoleMapping)).toHaveLength(1); }); - it('handles onClick', () => { + it('renders User flyout', () => { + setMockValues({ ...mockValues, singleUserRoleMappingFlyoutOpen: true }); + const wrapper = shallow(); + + expect(wrapper.find(User)).toHaveLength(1); + }); + + it('handles RoleMappingsHeading onClick', () => { const wrapper = shallow(); wrapper.find(RoleMappingsHeading).prop('onClick')(); expect(initializeRoleMapping).toHaveBeenCalled(); }); + + it('handles UsersHeading onClick', () => { + const wrapper = shallow(); + wrapper.find(UsersHeading).prop('onClick')(); + + expect(initializeSingleUserRoleMapping).toHaveBeenCalled(); + }); + + it('handles empty users state', () => { + setMockValues({ ...mockValues, singleUserRoleMappings: [] }); + const wrapper = shallow(); + + expect(wrapper.find(UsersEmptyPrompt)).toHaveLength(1); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings.tsx index 03e2ae67eca9e..3e692aa48623e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings.tsx @@ -9,11 +9,16 @@ import React, { useEffect } from 'react'; import { useActions, useValues } from 'kea'; +import { EuiSpacer } from '@elastic/eui'; + import { APP_SEARCH_PLUGIN } from '../../../../../common/constants'; import { RoleMappingsTable, RoleMappingsHeading, RolesEmptyPrompt, + UsersTable, + UsersHeading, + UsersEmptyPrompt, } from '../../../shared/role_mapping'; import { ROLE_MAPPINGS_TITLE } from '../../../shared/role_mapping/constants'; @@ -23,6 +28,7 @@ import { AppSearchPageTemplate } from '../layout'; import { ROLE_MAPPINGS_ENGINE_ACCESS_HEADING } from './constants'; import { RoleMapping } from './role_mapping'; import { RoleMappingsLogic } from './role_mappings_logic'; +import { User } from './user'; const ROLES_DOCS_LINK = `${DOCS_PREFIX}/security-and-users.html`; @@ -31,14 +37,17 @@ export const RoleMappings: React.FC = () => { enableRoleBasedAccess, initializeRoleMappings, initializeRoleMapping, + initializeSingleUserRoleMapping, handleDeleteMapping, resetState, } = useActions(RoleMappingsLogic); const { roleMappings, + singleUserRoleMappings, multipleAuthProvidersConfig, dataLoading, roleMappingFlyoutOpen, + singleUserRoleMappingFlyoutOpen, } = useValues(RoleMappingsLogic); useEffect(() => { @@ -46,6 +55,8 @@ export const RoleMappings: React.FC = () => { return resetState; }, []); + const hasUsers = singleUserRoleMappings.length > 0; + const rolesEmptyState = ( { ); + const usersTable = ( + + ); + + const usersSection = ( + <> + initializeSingleUserRoleMapping()} /> + + {hasUsers ? usersTable : } + + ); + return ( { emptyState={rolesEmptyState} > {roleMappingFlyoutOpen && } + {singleUserRoleMappingFlyoutOpen && } {roleMappingsSection} + + {usersSection} ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.test.ts index 6985f213d1dd5..16b44e9ec1f11 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.test.ts @@ -15,11 +15,18 @@ import { engines } from '../../__mocks__/engines.mock'; import { nextTick } from '@kbn/test/jest'; -import { asRoleMapping } from '../../../shared/role_mapping/__mocks__/roles'; +import { elasticsearchUsers } from '../../../shared/role_mapping/__mocks__/elasticsearch_users'; + +import { + asRoleMapping, + asSingleUserRoleMapping, +} from '../../../shared/role_mapping/__mocks__/roles'; import { ANY_AUTH_PROVIDER } from '../../../shared/role_mapping/constants'; import { RoleMappingsLogic } from './role_mappings_logic'; +const emptyUser = { username: '', email: '' }; + describe('RoleMappingsLogic', () => { const { http } = mockHttpValues; const { clearFlashMessages, flashAPIErrors, setSuccessMessage } = mockFlashMessageHelpers; @@ -28,6 +35,8 @@ describe('RoleMappingsLogic', () => { attributes: [], availableAuthProviders: [], elasticsearchRoles: [], + elasticsearchUser: emptyUser, + elasticsearchUsers: [], roleMapping: null, roleMappingFlyoutOpen: false, roleMappings: [], @@ -43,6 +52,12 @@ describe('RoleMappingsLogic', () => { selectedAuthProviders: [ANY_AUTH_PROVIDER], selectedOptions: [], roleMappingErrors: [], + singleUserRoleMapping: null, + singleUserRoleMappings: [], + singleUserRoleMappingFlyoutOpen: false, + userCreated: false, + userFormIsNewUser: true, + userFormUserIsExisting: true, }; const mappingsServerProps = { @@ -53,6 +68,8 @@ describe('RoleMappingsLogic', () => { availableEngines: engines, elasticsearchRoles: [], hasAdvancedRoles: false, + singleUserRoleMappings: [asSingleUserRoleMapping], + elasticsearchUsers, }; beforeEach(() => { @@ -83,7 +100,19 @@ describe('RoleMappingsLogic', () => { elasticsearchRoles: mappingsServerProps.elasticsearchRoles, selectedEngines: new Set(), selectedOptions: [], + elasticsearchUsers, + elasticsearchUser: elasticsearchUsers[0], + singleUserRoleMappings: [asSingleUserRoleMapping], + }); + }); + + it('handles fallback if no elasticsearch users present', () => { + RoleMappingsLogic.actions.setRoleMappingsData({ + ...mappingsServerProps, + elasticsearchUsers: [], }); + + expect(RoleMappingsLogic.values.elasticsearchUser).toEqual(emptyUser); }); }); @@ -94,6 +123,26 @@ describe('RoleMappingsLogic', () => { expect(RoleMappingsLogic.values.dataLoading).toEqual(false); }); + describe('setElasticsearchUser', () => { + it('sets user', () => { + RoleMappingsLogic.actions.setElasticsearchUser(elasticsearchUsers[0]); + + expect(RoleMappingsLogic.values.elasticsearchUser).toEqual(elasticsearchUsers[0]); + }); + + it('handles fallback if no user present', () => { + RoleMappingsLogic.actions.setElasticsearchUser(undefined); + + expect(RoleMappingsLogic.values.elasticsearchUser).toEqual(emptyUser); + }); + }); + + it('setSingleUserRoleMapping', () => { + RoleMappingsLogic.actions.setSingleUserRoleMapping(asSingleUserRoleMapping); + + expect(RoleMappingsLogic.values.singleUserRoleMapping).toEqual(asSingleUserRoleMapping); + }); + it('handleRoleChange', () => { RoleMappingsLogic.actions.handleRoleChange('dev'); @@ -152,6 +201,12 @@ describe('RoleMappingsLogic', () => { }); }); + it('setUserExistingRadioValue', () => { + RoleMappingsLogic.actions.setUserExistingRadioValue(false); + + expect(RoleMappingsLogic.values.userFormUserIsExisting).toEqual(false); + }); + describe('handleAttributeSelectorChange', () => { const elasticsearchRoles = ['foo', 'bar']; @@ -174,6 +229,8 @@ describe('RoleMappingsLogic', () => { attributeName: 'role', elasticsearchRoles, selectedEngines: new Set(), + elasticsearchUsers, + singleUserRoleMappings: [asSingleUserRoleMapping], }); }); @@ -260,16 +317,59 @@ describe('RoleMappingsLogic', () => { expect(clearFlashMessages).toHaveBeenCalled(); }); - it('closeRoleMappingFlyout', () => { + it('openSingleUserRoleMappingFlyout', () => { + mount(mappingsServerProps); + RoleMappingsLogic.actions.openSingleUserRoleMappingFlyout(); + + expect(RoleMappingsLogic.values.singleUserRoleMappingFlyoutOpen).toEqual(true); + expect(clearFlashMessages).toHaveBeenCalled(); + }); + + it('closeUsersAndRolesFlyout', () => { mount({ ...mappingsServerProps, roleMappingFlyoutOpen: true, }); - RoleMappingsLogic.actions.closeRoleMappingFlyout(); + RoleMappingsLogic.actions.closeUsersAndRolesFlyout(); expect(RoleMappingsLogic.values.roleMappingFlyoutOpen).toEqual(false); expect(clearFlashMessages).toHaveBeenCalled(); }); + + it('setElasticsearchUsernameValue', () => { + const username = 'newName'; + RoleMappingsLogic.actions.setElasticsearchUsernameValue(username); + + expect(RoleMappingsLogic.values).toEqual({ + ...DEFAULT_VALUES, + elasticsearchUser: { + ...RoleMappingsLogic.values.elasticsearchUser, + username, + }, + }); + }); + + it('setElasticsearchEmailValue', () => { + const email = 'newEmail@foo.cats'; + RoleMappingsLogic.actions.setElasticsearchEmailValue(email); + + expect(RoleMappingsLogic.values).toEqual({ + ...DEFAULT_VALUES, + elasticsearchUser: { + ...RoleMappingsLogic.values.elasticsearchUser, + email, + }, + }); + }); + + it('setUserCreated', () => { + RoleMappingsLogic.actions.setUserCreated(); + + expect(RoleMappingsLogic.values).toEqual({ + ...DEFAULT_VALUES, + userCreated: true, + }); + }); }); describe('listeners', () => { @@ -335,6 +435,39 @@ describe('RoleMappingsLogic', () => { }); }); + describe('initializeSingleUserRoleMapping', () => { + let setElasticsearchUserSpy: jest.MockedFunction; + let setRoleMappingSpy: jest.MockedFunction; + let setSingleUserRoleMappingSpy: jest.MockedFunction; + beforeEach(() => { + setElasticsearchUserSpy = jest.spyOn(RoleMappingsLogic.actions, 'setElasticsearchUser'); + setRoleMappingSpy = jest.spyOn(RoleMappingsLogic.actions, 'setRoleMapping'); + setSingleUserRoleMappingSpy = jest.spyOn( + RoleMappingsLogic.actions, + 'setSingleUserRoleMapping' + ); + }); + + it('should handle the new user state and only set an empty mapping', () => { + RoleMappingsLogic.actions.initializeSingleUserRoleMapping(); + + expect(setElasticsearchUserSpy).not.toHaveBeenCalled(); + expect(setRoleMappingSpy).not.toHaveBeenCalled(); + expect(setSingleUserRoleMappingSpy).toHaveBeenCalledWith(undefined); + }); + + it('should handle an existing user state and set mapping', () => { + RoleMappingsLogic.actions.setRoleMappingsData(mappingsServerProps); + RoleMappingsLogic.actions.initializeSingleUserRoleMapping( + asSingleUserRoleMapping.roleMapping.id + ); + + expect(setElasticsearchUserSpy).toHaveBeenCalled(); + expect(setRoleMappingSpy).toHaveBeenCalled(); + expect(setSingleUserRoleMappingSpy).toHaveBeenCalledWith(asSingleUserRoleMapping); + }); + }); + describe('handleSaveMapping', () => { const body = { roleType: 'owner', @@ -430,6 +563,94 @@ describe('RoleMappingsLogic', () => { }); }); + describe('handleSaveUser', () => { + it('calls API and refreshes list when new mapping', async () => { + const initializeRoleMappingsSpy = jest.spyOn( + RoleMappingsLogic.actions, + 'initializeRoleMappings' + ); + const setUserCreatedSpy = jest.spyOn(RoleMappingsLogic.actions, 'setUserCreated'); + const setSingleUserRoleMappingSpy = jest.spyOn( + RoleMappingsLogic.actions, + 'setSingleUserRoleMapping' + ); + RoleMappingsLogic.actions.setRoleMappingsData(mappingsServerProps); + + http.post.mockReturnValue(Promise.resolve(mappingsServerProps)); + RoleMappingsLogic.actions.handleSaveUser(); + + expect(http.post).toHaveBeenCalledWith('/api/app_search/single_user_role_mapping', { + body: JSON.stringify({ + roleMapping: { + engines: [], + roleType: 'owner', + accessAllEngines: true, + }, + elasticsearchUser: { + username: elasticsearchUsers[0].username, + email: elasticsearchUsers[0].email, + }, + }), + }); + await nextTick(); + + expect(initializeRoleMappingsSpy).toHaveBeenCalled(); + expect(setUserCreatedSpy).toHaveBeenCalled(); + expect(setSingleUserRoleMappingSpy).toHaveBeenCalled(); + }); + + it('calls API and refreshes list when existing mapping', async () => { + const initializeRoleMappingsSpy = jest.spyOn( + RoleMappingsLogic.actions, + 'initializeRoleMappings' + ); + RoleMappingsLogic.actions.setSingleUserRoleMapping(asSingleUserRoleMapping); + RoleMappingsLogic.actions.handleAccessAllEnginesChange(false); + + http.put.mockReturnValue(Promise.resolve(mappingsServerProps)); + RoleMappingsLogic.actions.handleSaveUser(); + + expect(http.post).toHaveBeenCalledWith('/api/app_search/single_user_role_mapping', { + body: JSON.stringify({ + roleMapping: { + engines: [], + roleType: 'owner', + accessAllEngines: false, + id: asSingleUserRoleMapping.roleMapping.id, + }, + elasticsearchUser: { + username: '', + email: '', + }, + }), + }); + await nextTick(); + + expect(initializeRoleMappingsSpy).toHaveBeenCalled(); + }); + + it('handles error', async () => { + const setRoleMappingErrorsSpy = jest.spyOn( + RoleMappingsLogic.actions, + 'setRoleMappingErrors' + ); + + http.post.mockReturnValue( + Promise.reject({ + body: { + attributes: { + errors: ['this is an error'], + }, + }, + }) + ); + RoleMappingsLogic.actions.handleSaveUser(); + await nextTick(); + + expect(setRoleMappingErrorsSpy).toHaveBeenCalledWith(['this is an error']); + }); + }); + describe('handleDeleteMapping', () => { const roleMappingId = 'r1'; @@ -458,5 +679,52 @@ describe('RoleMappingsLogic', () => { expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); }); + + describe('handleUsernameSelectChange', () => { + it('sets elasticsearchUser when match found', () => { + RoleMappingsLogic.actions.setRoleMappingsData(mappingsServerProps); + const setElasticsearchUserSpy = jest.spyOn( + RoleMappingsLogic.actions, + 'setElasticsearchUser' + ); + RoleMappingsLogic.actions.handleUsernameSelectChange(elasticsearchUsers[0].username); + + expect(setElasticsearchUserSpy).toHaveBeenCalledWith(elasticsearchUsers[0]); + }); + + it('does not set elasticsearchUser when no match found', () => { + RoleMappingsLogic.actions.setRoleMappingsData(mappingsServerProps); + const setElasticsearchUserSpy = jest.spyOn( + RoleMappingsLogic.actions, + 'setElasticsearchUser' + ); + RoleMappingsLogic.actions.handleUsernameSelectChange('bogus'); + + expect(setElasticsearchUserSpy).not.toHaveBeenCalled(); + }); + }); + + describe('setUserExistingRadioValue', () => { + it('handles existing user', () => { + RoleMappingsLogic.actions.setRoleMappingsData(mappingsServerProps); + const setElasticsearchUserSpy = jest.spyOn( + RoleMappingsLogic.actions, + 'setElasticsearchUser' + ); + RoleMappingsLogic.actions.setUserExistingRadioValue(true); + + expect(setElasticsearchUserSpy).toHaveBeenCalledWith(elasticsearchUsers[0]); + }); + + it('handles new user', () => { + const setElasticsearchUserSpy = jest.spyOn( + RoleMappingsLogic.actions, + 'setElasticsearchUser' + ); + RoleMappingsLogic.actions.setUserExistingRadioValue(false); + + expect(setElasticsearchUserSpy).toHaveBeenCalledWith(emptyUser); + }); + }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.ts index e2ef75897528c..0b57e1d08a294 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.ts @@ -16,7 +16,7 @@ import { } from '../../../shared/flash_messages'; import { HttpLogic } from '../../../shared/http'; import { ANY_AUTH_PROVIDER } from '../../../shared/role_mapping/constants'; -import { AttributeName } from '../../../shared/types'; +import { AttributeName, SingleUserRoleMapping, ElasticsearchUser } from '../../../shared/types'; import { ASRoleMapping, RoleTypes } from '../../types'; import { roleHasScopedEngines } from '../../utils/role/has_scoped_engines'; import { Engine } from '../engine/types'; @@ -27,20 +27,25 @@ import { ROLE_MAPPING_UPDATED_MESSAGE, } from './constants'; +type UserMapping = SingleUserRoleMapping; + interface RoleMappingsServerDetails { roleMappings: ASRoleMapping[]; attributes: string[]; authProviders: string[]; availableEngines: Engine[]; elasticsearchRoles: string[]; + elasticsearchUsers: ElasticsearchUser[]; hasAdvancedRoles: boolean; multipleAuthProvidersConfig: boolean; + singleUserRoleMappings: UserMapping[]; } const getFirstAttributeName = (roleMapping: ASRoleMapping) => Object.entries(roleMapping.rules)[0][0] as AttributeName; const getFirstAttributeValue = (roleMapping: ASRoleMapping) => Object.entries(roleMapping.rules)[0][1] as AttributeName; +const emptyUser = { username: '', email: '' } as ElasticsearchUser; interface RoleMappingsActions { handleAccessAllEnginesChange(selected: boolean): { selected: boolean }; @@ -53,21 +58,34 @@ interface RoleMappingsActions { handleDeleteMapping(roleMappingId: string): { roleMappingId: string }; handleEngineSelectionChange(engineNames: string[]): { engineNames: string[] }; handleRoleChange(roleType: RoleTypes): { roleType: RoleTypes }; + handleUsernameSelectChange(username: string): { username: string }; handleSaveMapping(): void; + handleSaveUser(): void; initializeRoleMapping(roleMappingId?: string): { roleMappingId?: string }; + initializeSingleUserRoleMapping(roleMappingId?: string): { roleMappingId?: string }; initializeRoleMappings(): void; resetState(): void; setRoleMapping(roleMapping: ASRoleMapping): { roleMapping: ASRoleMapping }; + setSingleUserRoleMapping(data?: UserMapping): { singleUserRoleMapping: UserMapping }; setRoleMappings({ roleMappings, }: { roleMappings: ASRoleMapping[]; }): { roleMappings: ASRoleMapping[] }; setRoleMappingsData(data: RoleMappingsServerDetails): RoleMappingsServerDetails; + setElasticsearchUser( + elasticsearchUser?: ElasticsearchUser + ): { elasticsearchUser: ElasticsearchUser }; openRoleMappingFlyout(): void; - closeRoleMappingFlyout(): void; + openSingleUserRoleMappingFlyout(): void; + closeUsersAndRolesFlyout(): void; setRoleMappingErrors(errors: string[]): { errors: string[] }; enableRoleBasedAccess(): void; + setUserExistingRadioValue(userFormUserIsExisting: boolean): { userFormUserIsExisting: boolean }; + setElasticsearchUsernameValue(username: string): { username: string }; + setElasticsearchEmailValue(email: string): { email: string }; + setUserCreated(): void; + setUserFormIsNewUser(userFormIsNewUser: boolean): { userFormIsNewUser: boolean }; } interface RoleMappingsValues { @@ -79,27 +97,38 @@ interface RoleMappingsValues { availableEngines: Engine[]; dataLoading: boolean; elasticsearchRoles: string[]; + elasticsearchUsers: ElasticsearchUser[]; + elasticsearchUser: ElasticsearchUser; hasAdvancedRoles: boolean; multipleAuthProvidersConfig: boolean; roleMapping: ASRoleMapping | null; roleMappings: ASRoleMapping[]; + singleUserRoleMapping: UserMapping | null; + singleUserRoleMappings: UserMapping[]; roleType: RoleTypes; selectedAuthProviders: string[]; selectedEngines: Set; roleMappingFlyoutOpen: boolean; + singleUserRoleMappingFlyoutOpen: boolean; selectedOptions: EuiComboBoxOptionOption[]; roleMappingErrors: string[]; + userFormUserIsExisting: boolean; + userCreated: boolean; + userFormIsNewUser: boolean; } export const RoleMappingsLogic = kea>({ - path: ['enterprise_search', 'app_search', 'role_mappings'], + path: ['enterprise_search', 'app_search', 'users_and_roles'], actions: { setRoleMappingsData: (data: RoleMappingsServerDetails) => data, setRoleMapping: (roleMapping: ASRoleMapping) => ({ roleMapping }), + setElasticsearchUser: (elasticsearchUser: ElasticsearchUser) => ({ elasticsearchUser }), + setSingleUserRoleMapping: (singleUserRoleMapping: UserMapping) => ({ singleUserRoleMapping }), setRoleMappings: ({ roleMappings }: { roleMappings: ASRoleMapping[] }) => ({ roleMappings }), setRoleMappingErrors: (errors: string[]) => ({ errors }), handleAuthProviderChange: (value: string) => ({ value }), handleRoleChange: (roleType: RoleTypes) => ({ roleType }), + handleUsernameSelectChange: (username: string) => ({ username }), handleEngineSelectionChange: (engineNames: string[]) => ({ engineNames }), handleAttributeSelectorChange: (value: string, firstElasticsearchRole: string) => ({ value, @@ -108,13 +137,21 @@ export const RoleMappingsLogic = kea ({ value }), handleAccessAllEnginesChange: (selected: boolean) => ({ selected }), enableRoleBasedAccess: true, + openSingleUserRoleMappingFlyout: true, + setUserExistingRadioValue: (userFormUserIsExisting: boolean) => ({ userFormUserIsExisting }), resetState: true, initializeRoleMappings: true, + initializeSingleUserRoleMapping: (roleMappingId?: string) => ({ roleMappingId }), initializeRoleMapping: (roleMappingId) => ({ roleMappingId }), handleDeleteMapping: (roleMappingId: string) => ({ roleMappingId }), handleSaveMapping: true, + handleSaveUser: true, openRoleMappingFlyout: true, - closeRoleMappingFlyout: false, + closeUsersAndRolesFlyout: false, + setElasticsearchUsernameValue: (username: string) => ({ username }), + setElasticsearchEmailValue: (email: string) => ({ email }), + setUserCreated: true, + setUserFormIsNewUser: (userFormIsNewUser: boolean) => ({ userFormIsNewUser }), }, reducers: { dataLoading: [ @@ -134,6 +171,13 @@ export const RoleMappingsLogic = kea [], }, ], + singleUserRoleMappings: [ + [], + { + setRoleMappingsData: (_, { singleUserRoleMappings }) => singleUserRoleMappings, + resetState: () => [], + }, + ], multipleAuthProvidersConfig: [ false, { @@ -165,6 +209,14 @@ export const RoleMappingsLogic = kea elasticsearchRoles, + closeUsersAndRolesFlyout: () => [ANY_AUTH_PROVIDER], + }, + ], + elasticsearchUsers: [ + [], + { + setRoleMappingsData: (_, { elasticsearchUsers }) => elasticsearchUsers, + resetState: () => [], }, ], roleMapping: [ @@ -172,7 +224,7 @@ export const RoleMappingsLogic = kea roleMapping, resetState: () => null, - closeRoleMappingFlyout: () => null, + closeUsersAndRolesFlyout: () => null, }, ], roleType: [ @@ -188,6 +240,7 @@ export const RoleMappingsLogic = kea roleMapping.accessAllEngines, handleRoleChange: (_, { roleType }) => !roleHasScopedEngines(roleType), handleAccessAllEnginesChange: (_, { selected }) => selected, + closeUsersAndRolesFlyout: () => true, }, ], attributeValue: [ @@ -198,7 +251,7 @@ export const RoleMappingsLogic = kea value, resetState: () => '', - closeRoleMappingFlyout: () => '', + closeUsersAndRolesFlyout: () => '', }, ], attributeName: [ @@ -207,7 +260,7 @@ export const RoleMappingsLogic = kea getFirstAttributeName(roleMapping), handleAttributeSelectorChange: (_, { value }) => value, resetState: () => 'username', - closeRoleMappingFlyout: () => 'username', + closeUsersAndRolesFlyout: () => 'username', }, ], selectedEngines: [ @@ -222,6 +275,7 @@ export const RoleMappingsLogic = kea new Set(), }, ], availableAuthProviders: [ @@ -251,17 +305,68 @@ export const RoleMappingsLogic = kea true, - closeRoleMappingFlyout: () => false, + closeUsersAndRolesFlyout: () => false, initializeRoleMappings: () => false, initializeRoleMapping: () => true, }, ], + singleUserRoleMappingFlyoutOpen: [ + false, + { + openSingleUserRoleMappingFlyout: () => true, + closeUsersAndRolesFlyout: () => false, + initializeSingleUserRoleMapping: () => true, + }, + ], + singleUserRoleMapping: [ + null, + { + setSingleUserRoleMapping: (_, { singleUserRoleMapping }) => singleUserRoleMapping || null, + closeUsersAndRolesFlyout: () => null, + }, + ], roleMappingErrors: [ [], { setRoleMappingErrors: (_, { errors }) => errors, handleSaveMapping: () => [], - closeRoleMappingFlyout: () => [], + closeUsersAndRolesFlyout: () => [], + }, + ], + userFormUserIsExisting: [ + true, + { + setUserExistingRadioValue: (_, { userFormUserIsExisting }) => userFormUserIsExisting, + closeUsersAndRolesFlyout: () => true, + }, + ], + elasticsearchUser: [ + emptyUser, + { + setRoleMappingsData: (_, { elasticsearchUsers }) => elasticsearchUsers[0] || emptyUser, + setElasticsearchUser: (_, { elasticsearchUser }) => elasticsearchUser || emptyUser, + setElasticsearchUsernameValue: (state, { username }) => ({ + ...state, + username, + }), + setElasticsearchEmailValue: (state, { email }) => ({ + ...state, + email, + }), + closeUsersAndRolesFlyout: () => emptyUser, + }, + ], + userCreated: [ + false, + { + setUserCreated: () => true, + closeUsersAndRolesFlyout: () => false, + }, + ], + userFormIsNewUser: [ + true, + { + setUserFormIsNewUser: (_, { userFormIsNewUser }) => userFormIsNewUser, }, ], }, @@ -303,6 +408,17 @@ export const RoleMappingsLogic = kea id === roleMappingId); if (roleMapping) actions.setRoleMapping(roleMapping); }, + initializeSingleUserRoleMapping: ({ roleMappingId }) => { + const singleUserRoleMapping = values.singleUserRoleMappings.find( + ({ roleMapping }) => roleMapping.id === roleMappingId + ); + if (singleUserRoleMapping) { + actions.setElasticsearchUser(singleUserRoleMapping.elasticsearchUser); + actions.setRoleMapping(singleUserRoleMapping.roleMapping); + } + actions.setSingleUserRoleMapping(singleUserRoleMapping); + actions.setUserFormIsNewUser(!singleUserRoleMapping); + }, handleDeleteMapping: async ({ roleMappingId }) => { const { http } = HttpLogic.values; const route = `/api/app_search/role_mappings/${roleMappingId}`; @@ -357,11 +473,56 @@ export const RoleMappingsLogic = kea { clearFlashMessages(); }, - closeRoleMappingFlyout: () => { + handleSaveUser: async () => { + const { http } = HttpLogic.values; + const { + roleType, + singleUserRoleMapping, + accessAllEngines, + selectedEngines, + elasticsearchUser: { email, username }, + } = values; + + const body = JSON.stringify({ + roleMapping: { + engines: accessAllEngines ? [] : Array.from(selectedEngines), + roleType, + accessAllEngines, + id: singleUserRoleMapping?.roleMapping?.id, + }, + elasticsearchUser: { + username, + email, + }, + }); + + try { + const response = await http.post('/api/app_search/single_user_role_mapping', { body }); + actions.setSingleUserRoleMapping(response); + actions.setUserCreated(); + actions.initializeRoleMappings(); + } catch (e) { + actions.setRoleMappingErrors(e?.body?.attributes?.errors); + } + }, + closeUsersAndRolesFlyout: () => { clearFlashMessages(); + const firstUser = values.elasticsearchUsers[0]; + actions.setElasticsearchUser(firstUser); }, openRoleMappingFlyout: () => { clearFlashMessages(); }, + openSingleUserRoleMappingFlyout: () => { + clearFlashMessages(); + }, + setUserExistingRadioValue: ({ userFormUserIsExisting }) => { + const firstUser = values.elasticsearchUsers[0]; + actions.setElasticsearchUser(userFormUserIsExisting ? firstUser : emptyUser); + }, + handleUsernameSelectChange: ({ username }) => { + const user = values.elasticsearchUsers.find((u) => u.username === username); + if (user) actions.setElasticsearchUser(user); + }, }), }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/user.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/user.test.tsx new file mode 100644 index 0000000000000..88103532bd149 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/user.test.tsx @@ -0,0 +1,124 @@ +/* + * 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 '../../../__mocks__/react_router'; +import '../../../__mocks__/shallow_useeffect.mock'; +import { setMockActions, setMockValues } from '../../../__mocks__/kea_logic'; +import { engines } from '../../__mocks__/engines.mock'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { UserFlyout, UserAddedInfo, UserInvitationCallout } from '../../../shared/role_mapping'; +import { elasticsearchUsers } from '../../../shared/role_mapping/__mocks__/elasticsearch_users'; +import { wsSingleUserRoleMapping } from '../../../shared/role_mapping/__mocks__/roles'; + +import { EngineAssignmentSelector } from './engine_assignment_selector'; +import { User } from './user'; + +describe('User', () => { + const handleSaveUser = jest.fn(); + const closeUsersAndRolesFlyout = jest.fn(); + const setUserExistingRadioValue = jest.fn(); + const setElasticsearchUsernameValue = jest.fn(); + const setElasticsearchEmailValue = jest.fn(); + const handleRoleChange = jest.fn(); + const handleUsernameSelectChange = jest.fn(); + + const mockValues = { + availableEngines: [], + singleUserRoleMapping: null, + userFormUserIsExisting: false, + elasticsearchUsers: [], + elasticsearchUser: {}, + roleType: 'admin', + roleMappingErrors: [], + userCreated: false, + userFormIsNewUser: false, + hasAdvancedRoles: false, + }; + + beforeEach(() => { + setMockActions({ + handleSaveUser, + closeUsersAndRolesFlyout, + setUserExistingRadioValue, + setElasticsearchUsernameValue, + setElasticsearchEmailValue, + handleRoleChange, + handleUsernameSelectChange, + }); + + setMockValues(mockValues); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(UserFlyout)).toHaveLength(1); + }); + + it('renders engine assignment selector when groups present', () => { + setMockValues({ ...mockValues, availableEngines: engines, hasAdvancedRoles: true }); + const wrapper = shallow(); + + expect(wrapper.find(EngineAssignmentSelector)).toHaveLength(1); + }); + + it('renders userInvitationCallout', () => { + setMockValues({ + ...mockValues, + singleUserRoleMapping: wsSingleUserRoleMapping, + }); + const wrapper = shallow(); + + expect(wrapper.find(UserInvitationCallout)).toHaveLength(1); + }); + + it('renders user added info when user created', () => { + setMockValues({ + ...mockValues, + singleUserRoleMapping: wsSingleUserRoleMapping, + userCreated: true, + }); + const wrapper = shallow(); + + expect(wrapper.find(UserAddedInfo)).toHaveLength(1); + }); + + it('disables form when username value not present', () => { + setMockValues({ + ...mockValues, + singleUserRoleMapping: wsSingleUserRoleMapping, + elasticsearchUsers, + elasticsearchUser: { + username: null, + email: 'email@user.com', + }, + }); + const wrapper = shallow(); + + expect(wrapper.find(UserFlyout).prop('disabled')).toEqual(true); + }); + + it('enables form when userFormUserIsExisting', () => { + setMockValues({ + ...mockValues, + userFormUserIsExisting: true.valueOf, + singleUserRoleMapping: wsSingleUserRoleMapping, + elasticsearchUsers, + elasticsearchUser: { + username: null, + email: 'email@user.com', + }, + }); + const wrapper = shallow(); + + expect(wrapper.find(UserFlyout).prop('disabled')).toEqual(false); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/user.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/user.tsx new file mode 100644 index 0000000000000..df231fac64df7 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/user.tsx @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useActions, useValues } from 'kea'; + +import { EuiForm } from '@elastic/eui'; + +import { getAppSearchUrl } from '../../../shared/enterprise_search_url'; +import { + UserFlyout, + UserSelector, + UserAddedInfo, + UserInvitationCallout, +} from '../../../shared/role_mapping'; +import { RoleTypes } from '../../types'; + +import { EngineAssignmentSelector } from './engine_assignment_selector'; +import { RoleMappingsLogic } from './role_mappings_logic'; + +const standardRoles = (['owner', 'admin'] as unknown) as RoleTypes[]; +const advancedRoles = (['dev', 'editor', 'analyst'] as unknown) as RoleTypes[]; + +export const User: React.FC = () => { + const { + handleSaveUser, + closeUsersAndRolesFlyout, + setUserExistingRadioValue, + setElasticsearchUsernameValue, + setElasticsearchEmailValue, + handleRoleChange, + handleUsernameSelectChange, + } = useActions(RoleMappingsLogic); + + const { + availableEngines, + singleUserRoleMapping, + hasAdvancedRoles, + userFormUserIsExisting, + elasticsearchUsers, + elasticsearchUser, + roleType, + roleMappingErrors, + userCreated, + userFormIsNewUser, + } = useValues(RoleMappingsLogic); + + const roleTypes = hasAdvancedRoles ? [...standardRoles, ...advancedRoles] : standardRoles; + const hasEngines = availableEngines.length > 0; + const showEngineAssignmentSelector = hasEngines && hasAdvancedRoles; + const flyoutDisabled = + !userFormUserIsExisting && (!elasticsearchUser.email || !elasticsearchUser.username); + + const userAddedInfo = singleUserRoleMapping && ( + + ); + + const userInvitationCallout = singleUserRoleMapping?.invitation && ( + + ); + + const createUserForm = ( + 0} error={roleMappingErrors}> + + {showEngineAssignmentSelector && } + + ); + + return ( + + {userCreated ? userAddedInfo : createUserForm} + {userInvitationCallout} + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx index 2402a6ecc6401..00acea945177a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx @@ -196,6 +196,6 @@ describe('AppSearchNav', () => { setMockValues({ myRole: { canViewRoleMappings: true } }); const wrapper = shallow(); - expect(wrapper.find(SideNavLink).last().prop('to')).toEqual('/role_mappings'); + expect(wrapper.find(SideNavLink).last().prop('to')).toEqual('/users_and_roles'); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx index 191758af26758..d7ddad5683f38 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx @@ -37,7 +37,7 @@ import { SETUP_GUIDE_PATH, SETTINGS_PATH, CREDENTIALS_PATH, - ROLE_MAPPINGS_PATH, + USERS_AND_ROLES_PATH, ENGINES_PATH, ENGINE_PATH, LIBRARY_PATH, @@ -128,7 +128,7 @@ export const AppSearchConfigured: React.FC> = (props) = )} {canViewRoleMappings && ( - + )} @@ -162,7 +162,7 @@ export const AppSearchNav: React.FC = () => { {CREDENTIALS_TITLE} )} {canViewRoleMappings && ( - + {ROLE_MAPPINGS_TITLE} )} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts index d9d1935c648f7..f086a32bbf590 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts @@ -15,7 +15,7 @@ export const LIBRARY_PATH = '/library'; export const SETTINGS_PATH = '/settings'; export const CREDENTIALS_PATH = '/credentials'; -export const ROLE_MAPPINGS_PATH = '/role_mappings'; +export const USERS_AND_ROLES_PATH = '/users_and_roles'; export const ENGINES_PATH = '/engines'; export const ENGINE_CREATION_PATH = `${ENGINES_PATH}/new`; // This is safe from conflicting with an :engineName path because new is a reserved name diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/constants.ts b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/constants.ts index 45cab32b67e08..215c76ffb7ef4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/constants.ts @@ -136,7 +136,7 @@ export const FILTER_ROLE_MAPPINGS_PLACEHOLDER = i18n.translate( export const ROLE_MAPPINGS_TITLE = i18n.translate( 'xpack.enterpriseSearch.roleMapping.roleMappingsTitle', { - defaultMessage: 'Users & roles', + defaultMessage: 'Users and roles', } ); @@ -406,3 +406,19 @@ export const FILTER_USERS_LABEL = i18n.translate( export const NO_USERS_LABEL = i18n.translate('xpack.enterpriseSearch.roleMapping.noUsersLabel', { defaultMessage: 'No matching users found', }); + +export const EXTERNAL_ATTRIBUTE_TOOLTIP = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.externalAttributeTooltip', + { + defaultMessage: + 'External attributes are defined by the identity provider, and varies from service to service.', + } +); + +export const AUTH_PROVIDER_TOOLTIP = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.authProviderTooltip', + { + defaultMessage: + 'Provider-specific role mapping is still applied, but configuration is now deprecated.', + } +); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mapping_flyout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mapping_flyout.test.tsx index c0973bb2c9504..ffcf5508233fc 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mapping_flyout.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mapping_flyout.test.tsx @@ -20,13 +20,13 @@ import { import { RoleMappingFlyout } from './role_mapping_flyout'; describe('RoleMappingFlyout', () => { - const closeRoleMappingFlyout = jest.fn(); + const closeUsersAndRolesFlyout = jest.fn(); const handleSaveMapping = jest.fn(); const props = { isNew: true, disabled: false, - closeRoleMappingFlyout, + closeUsersAndRolesFlyout, handleSaveMapping, }; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mapping_flyout.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mapping_flyout.tsx index bae991fef3655..4416a2de28011 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mapping_flyout.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mapping_flyout.tsx @@ -36,7 +36,7 @@ interface Props { children: React.ReactNode; isNew: boolean; disabled: boolean; - closeRoleMappingFlyout(): void; + closeUsersAndRolesFlyout(): void; handleSaveMapping(): void; } @@ -44,13 +44,13 @@ export const RoleMappingFlyout: React.FC = ({ children, isNew, disabled, - closeRoleMappingFlyout, + closeUsersAndRolesFlyout, handleSaveMapping, }) => ( @@ -71,7 +71,9 @@ export const RoleMappingFlyout: React.FC = ({ - {CANCEL_BUTTON_LABEL} + + {CANCEL_BUTTON_LABEL} + { }); it('renders auth provider display names', () => { - const wrapper = mount(); + const roleMappingWithAuths = { + ...wsRoleMapping, + authProvider: ['saml', 'native'], + }; + const wrapper = mount(); - expect(wrapper.find('[data-test-subj="AuthProviderDisplayValue"]').prop('children')).toEqual( - `${ANY_AUTH_PROVIDER_OPTION_LABEL}, other_auth` - ); + expect(wrapper.find('[data-test-subj="ProviderSpecificList"]')).toHaveLength(1); }); it('handles manage click', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.tsx index eb9621c7a242c..4136d114d3420 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.tsx @@ -7,14 +7,17 @@ import React from 'react'; -import { EuiIconTip, EuiInMemoryTable, EuiBasicTableColumn } from '@elastic/eui'; +import { EuiIconTip, EuiInMemoryTable, EuiBasicTableColumn, EuiLink } from '@elastic/eui'; import { ASRoleMapping } from '../../app_search/types'; import { WSRoleMapping } from '../../workplace_search/types'; +import { docLinks } from '../doc_links'; import { RoleRules } from '../types'; import './role_mappings_table.scss'; +const AUTH_PROVIDER_DOCUMENTATION_URL = `${docLinks.enterpriseSearchBase}/users-access.html`; + import { ANY_AUTH_PROVIDER, ANY_AUTH_PROVIDER_OPTION_LABEL, @@ -25,6 +28,8 @@ import { ATTRIBUTE_VALUE_LABEL, FILTER_ROLE_MAPPINGS_PLACEHOLDER, ROLE_MAPPINGS_NO_RESULTS_MESSAGE, + EXTERNAL_ATTRIBUTE_TOOLTIP, + AUTH_PROVIDER_TOOLTIP, } from './constants'; import { UsersAndRolesRowActions } from './users_and_roles_row_actions'; @@ -46,9 +51,6 @@ interface Props { handleDeleteMapping(roleMappingId: string): void; } -const getAuthProviderDisplayValue = (authProvider: string) => - authProvider === ANY_AUTH_PROVIDER ? ANY_AUTH_PROVIDER_OPTION_LABEL : authProvider; - export const RoleMappingsTable: React.FC = ({ accessItemKey, accessHeader, @@ -69,7 +71,19 @@ export const RoleMappingsTable: React.FC = ({ const attributeNameCol: EuiBasicTableColumn = { field: 'attribute', - name: EXTERNAL_ATTRIBUTE_LABEL, + name: ( + + {EXTERNAL_ATTRIBUTE_LABEL}{' '} + + + ), render: (_, { rules }: SharedRoleMapping) => getFirstAttributeName(rules), }; @@ -105,11 +119,19 @@ export const RoleMappingsTable: React.FC = ({ const authProviderCol: EuiBasicTableColumn = { field: 'authProvider', name: AUTH_PROVIDER_LABEL, - render: (_, { authProvider }: SharedRoleMapping) => ( - - {authProvider.map(getAuthProviderDisplayValue).join(', ')} - - ), + render: (_, { authProvider }: SharedRoleMapping) => { + if (authProvider[0] === ANY_AUTH_PROVIDER) { + return ANY_AUTH_PROVIDER_OPTION_LABEL; + } + return ( + + {authProvider.join(', ')}{' '} + + + + + ); + }, }; const actionsCol: EuiBasicTableColumn = { diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_added_info.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_added_info.test.tsx index 30bdaa0010b58..57200b389591d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_added_info.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_added_info.test.tsx @@ -9,8 +9,6 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { EuiText } from '@elastic/eui'; - import { UserAddedInfo } from './'; describe('UserAddedInfo', () => { @@ -20,9 +18,103 @@ describe('UserAddedInfo', () => { roleType: 'user', }; - it('renders', () => { + it('renders with email', () => { const wrapper = shallow(); - expect(wrapper.find(EuiText)).toHaveLength(6); + expect(wrapper).toMatchInlineSnapshot(` + + + + Username + + + + user1 + + + + + Email + + + + test@test.com + + + + + Role + + + + user + + + + `); + }); + + it('renders without email', () => { + const wrapper = shallow(); + + expect(wrapper).toMatchInlineSnapshot(` + + + + Username + + + + user1 + + + + + Email + + + + + — + + + + + + Role + + + + user + + + + `); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_added_info.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_added_info.tsx index a12eae66262a0..37804414a94a9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_added_info.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_added_info.tsx @@ -7,7 +7,7 @@ import React from 'react'; -import { EuiSpacer, EuiText } from '@elastic/eui'; +import { EuiSpacer, EuiText, EuiTextColor } from '@elastic/eui'; import { USERNAME_LABEL, EMAIL_LABEL } from '../constants'; @@ -19,6 +19,8 @@ interface Props { roleType: string; } +const noItemsPlaceholder = ; + export const UserAddedInfo: React.FC = ({ username, email, roleType }) => ( <> @@ -29,7 +31,7 @@ export const UserAddedInfo: React.FC = ({ username, email, roleType }) => {EMAIL_LABEL} - {email} + {email || noItemsPlaceholder} {ROLE_LABEL} diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_flyout.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_flyout.tsx index e13a56a716929..a3be5e295ddfe 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_flyout.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_flyout.tsx @@ -17,6 +17,7 @@ import { EuiFlyoutFooter, EuiFlyoutHeader, EuiIcon, + EuiPortal, EuiText, EuiTitle, EuiSpacer, @@ -92,22 +93,26 @@ export const UserFlyout: React.FC = ({ ); return ( - - - -

{isComplete ? IS_COMPLETE_HEADING : IS_EDITING_HEADING}

-
- {!isComplete && ( - -

{IS_EDITING_DESCRIPTION}

-
- )} -
- - {children} - - - {isComplete ? completedFooterAction : editingFooterActions} -
+ + + + +

{isComplete ? IS_COMPLETE_HEADING : IS_EDITING_HEADING}

+
+ {!isComplete && ( + +

{IS_EDITING_DESCRIPTION}

+
+ )} +
+ + {children} + + + + {isComplete ? completedFooterAction : editingFooterActions} + +
+
); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_invitation_callout.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_invitation_callout.tsx index 8310077ad6f2e..d6d0ce7b050ab 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_invitation_callout.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_invitation_callout.tsx @@ -23,7 +23,7 @@ interface Props { } export const UserInvitationCallout: React.FC = ({ isNew, invitationCode, urlPrefix }) => { - const link = urlPrefix + invitationCode; + const link = `${urlPrefix}/invitations/${invitationCode}`; const label = isNew ? NEW_INVITATION_LABEL : EXISTING_INVITATION_LABEL; return ( diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_selector.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_selector.test.tsx index 08ddc7ba5427f..60bac97d09835 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_selector.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_selector.test.tsx @@ -13,7 +13,7 @@ import { shallow } from 'enzyme'; import { EuiFormRow } from '@elastic/eui'; -import { Role as ASRole } from '../../app_search/types'; +import { RoleTypes as ASRole } from '../../app_search/types'; import { REQUIRED_LABEL, USERNAME_NO_USERS_TEXT } from './constants'; @@ -107,6 +107,5 @@ describe('UserSelector', () => { expect(wrapper.find(EuiFormRow).at(0).prop('helpText')).toEqual(USERNAME_NO_USERS_TEXT); expect(wrapper.find(EuiFormRow).at(1).prop('helpText')).toEqual(REQUIRED_LABEL); - expect(wrapper.find(EuiFormRow).at(2).prop('helpText')).toEqual(REQUIRED_LABEL); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_selector.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_selector.tsx index 70348bf29894a..d65f97265f6a3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_selector.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_selector.tsx @@ -16,7 +16,7 @@ import { EuiSpacer, } from '@elastic/eui'; -import { Role as ASRole } from '../../app_search/types'; +import { RoleTypes as ASRole } from '../../app_search/types'; import { ElasticsearchUser } from '../../shared/types'; import { Role as WSRole } from '../../workplace_search/types'; @@ -80,7 +80,7 @@ export const UserSelector: React.FC = ({ ); const emailInput = ( - + = ({ setElasticsearchUsernameValue(e.target.value)} diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_table.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_table.tsx index 86dc2c2626229..674796775b1d3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_table.tsx @@ -46,8 +46,8 @@ interface SharedRoleMapping extends ASRoleMapping, WSRoleMapping { interface Props { accessItemKey: 'groups' | 'engines'; singleUserRoleMappings: Array>; - initializeSingleUserRoleMapping(roleId: string): string; - handleDeleteMapping(roleId: string): string; + initializeSingleUserRoleMapping(roleMappingId: string): void; + handleDeleteMapping(roleMappingId: string): void; } const noItemsPlaceholder = ; @@ -110,6 +110,7 @@ export const UsersTable: React.FC = ({ { field: 'id', name: '', + align: 'right', render: (_, { id, username }: SharedUser) => ( { }, { id: 'usersRoles', - name: 'Users & roles', - href: '/role_mappings', + name: 'Users and roles', + href: '/users_and_roles', }, { id: 'security', diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx index ce2f8bf7ef7e4..c8d821dcdae2e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx @@ -15,7 +15,7 @@ import { NAV } from '../../constants'; import { SOURCES_PATH, SECURITY_PATH, - ROLE_MAPPINGS_PATH, + USERS_AND_ROLES_PATH, GROUPS_PATH, ORG_SETTINGS_PATH, } from '../../routes'; @@ -48,7 +48,7 @@ export const useWorkplaceSearchNav = () => { { id: 'usersRoles', name: NAV.ROLE_MAPPINGS, - ...generateNavLink({ to: ROLE_MAPPINGS_PATH }), + ...generateNavLink({ to: USERS_AND_ROLES_PATH }), }, { id: 'security', @@ -92,7 +92,7 @@ export const WorkplaceSearchNav: React.FC = ({ {NAV.GROUPS} - + {NAV.ROLE_MAPPINGS} {NAV.SECURITY} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts index aa5419f12c7f3..cf459171a808a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts @@ -40,7 +40,7 @@ export const NAV = { defaultMessage: 'Content', }), ROLE_MAPPINGS: i18n.translate('xpack.enterpriseSearch.workplaceSearch.nav.roleMappings', { - defaultMessage: 'Users & roles', + defaultMessage: 'Users and roles', }), SECURITY: i18n.translate('xpack.enterpriseSearch.workplaceSearch.nav.security', { defaultMessage: 'Security', diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx index 8a1e9c0275322..05018be2934b4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx @@ -26,7 +26,7 @@ import { SOURCE_ADDED_PATH, PERSONAL_SOURCES_PATH, ORG_SETTINGS_PATH, - ROLE_MAPPINGS_PATH, + USERS_AND_ROLES_PATH, SECURITY_PATH, PERSONAL_SETTINGS_PATH, PERSONAL_PATH, @@ -103,7 +103,7 @@ export const WorkplaceSearchConfigured: React.FC = (props) => { - + diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts index 3c564c1f912ec..b9309ffd94809 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts @@ -48,7 +48,7 @@ export const ENT_SEARCH_LICENSE_MANAGEMENT = `${docLinks.enterpriseSearchBase}/l export const PERSONAL_PATH = '/p'; -export const ROLE_MAPPINGS_PATH = '/role_mappings'; +export const USERS_AND_ROLES_PATH = '/users_and_roles'; export const USERS_PATH = '/users'; export const SECURITY_PATH = '/security'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mapping.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mapping.tsx index cc773895bff1c..20211d40d7010 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mapping.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mapping.tsx @@ -43,7 +43,7 @@ export const RoleMapping: React.FC = () => { handleAttributeSelectorChange, handleRoleChange, handleAuthProviderChange, - closeRoleMappingFlyout, + closeUsersAndRolesFlyout, } = useActions(RoleMappingsLogic); const { @@ -70,7 +70,7 @@ export const RoleMapping: React.FC = () => { 0} error={roleMappingErrors}> diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings.test.tsx index 308022ccb2e5a..2e13f24a13eee 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings.test.tsx @@ -12,26 +12,39 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { RoleMappingsTable, RoleMappingsHeading } from '../../../shared/role_mapping'; -import { wsRoleMapping } from '../../../shared/role_mapping/__mocks__/roles'; +import { + RoleMappingsTable, + RoleMappingsHeading, + UsersHeading, + UsersEmptyPrompt, +} from '../../../shared/role_mapping'; +import { + wsRoleMapping, + wsSingleUserRoleMapping, +} from '../../../shared/role_mapping/__mocks__/roles'; import { RoleMapping } from './role_mapping'; import { RoleMappings } from './role_mappings'; +import { User } from './user'; describe('RoleMappings', () => { const initializeRoleMappings = jest.fn(); const initializeRoleMapping = jest.fn(); + const initializeSingleUserRoleMapping = jest.fn(); const handleDeleteMapping = jest.fn(); const mockValues = { roleMappings: [wsRoleMapping], dataLoading: false, multipleAuthProvidersConfig: false, + singleUserRoleMappings: [wsSingleUserRoleMapping], + singleUserRoleMappingFlyoutOpen: false, }; beforeEach(() => { setMockActions({ initializeRoleMappings, initializeRoleMapping, + initializeSingleUserRoleMapping, handleDeleteMapping, }); setMockValues(mockValues); @@ -50,10 +63,31 @@ describe('RoleMappings', () => { expect(wrapper.find(RoleMapping)).toHaveLength(1); }); - it('handles onClick', () => { + it('renders User flyout', () => { + setMockValues({ ...mockValues, singleUserRoleMappingFlyoutOpen: true }); + const wrapper = shallow(); + + expect(wrapper.find(User)).toHaveLength(1); + }); + + it('handles RoleMappingsHeading onClick', () => { const wrapper = shallow(); wrapper.find(RoleMappingsHeading).prop('onClick')(); expect(initializeRoleMapping).toHaveBeenCalled(); }); + + it('handles UsersHeading onClick', () => { + const wrapper = shallow(); + wrapper.find(UsersHeading).prop('onClick')(); + + expect(initializeSingleUserRoleMapping).toHaveBeenCalled(); + }); + + it('handles empty users state', () => { + setMockValues({ ...mockValues, singleUserRoleMappings: [] }); + const wrapper = shallow(); + + expect(wrapper.find(UsersEmptyPrompt)).toHaveLength(1); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings.tsx index 01d32bec14ebd..df5d7e4267690 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings.tsx @@ -9,11 +9,16 @@ import React, { useEffect } from 'react'; import { useActions, useValues } from 'kea'; +import { EuiSpacer } from '@elastic/eui'; + import { WORKPLACE_SEARCH_PLUGIN } from '../../../../../common/constants'; import { RoleMappingsTable, RoleMappingsHeading, RolesEmptyPrompt, + UsersTable, + UsersHeading, + UsersEmptyPrompt, } from '../../../shared/role_mapping'; import { ROLE_MAPPINGS_TITLE } from '../../../shared/role_mapping/constants'; import { WorkplaceSearchPageTemplate } from '../../components/layout'; @@ -23,26 +28,32 @@ import { ROLE_MAPPINGS_TABLE_HEADER } from './constants'; import { RoleMapping } from './role_mapping'; import { RoleMappingsLogic } from './role_mappings_logic'; +import { User } from './user'; export const RoleMappings: React.FC = () => { const { enableRoleBasedAccess, initializeRoleMappings, initializeRoleMapping, + initializeSingleUserRoleMapping, handleDeleteMapping, } = useActions(RoleMappingsLogic); const { roleMappings, + singleUserRoleMappings, dataLoading, multipleAuthProvidersConfig, roleMappingFlyoutOpen, + singleUserRoleMappingFlyoutOpen, } = useValues(RoleMappingsLogic); useEffect(() => { initializeRoleMappings(); }, []); + const hasUsers = singleUserRoleMappings.length > 0; + const rolesEmptyState = ( { ); + const usersTable = ( + + ); + + const usersSection = ( + <> + initializeSingleUserRoleMapping()} /> + + {hasUsers ? usersTable : } + + ); + return ( { emptyState={rolesEmptyState} > {roleMappingFlyoutOpen && } + {singleUserRoleMappingFlyoutOpen && } {roleMappingsSection} + + {usersSection} ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.test.ts index a4bbddbd23b49..c85e86ebcca2c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.test.ts @@ -15,11 +15,18 @@ import { groups } from '../../__mocks__/groups.mock'; import { nextTick } from '@kbn/test/jest'; -import { wsRoleMapping } from '../../../shared/role_mapping/__mocks__/roles'; +import { elasticsearchUsers } from '../../../shared/role_mapping/__mocks__/elasticsearch_users'; + +import { + wsRoleMapping, + wsSingleUserRoleMapping, +} from '../../../shared/role_mapping/__mocks__/roles'; import { ANY_AUTH_PROVIDER } from '../../../shared/role_mapping/constants'; import { RoleMappingsLogic } from './role_mappings_logic'; +const emptyUser = { username: '', email: '' }; + describe('RoleMappingsLogic', () => { const { http } = mockHttpValues; const { clearFlashMessages, flashAPIErrors } = mockFlashMessageHelpers; @@ -28,6 +35,8 @@ describe('RoleMappingsLogic', () => { attributes: [], availableAuthProviders: [], elasticsearchRoles: [], + elasticsearchUser: emptyUser, + elasticsearchUsers: [], roleMapping: null, roleMappingFlyoutOpen: false, roleMappings: [], @@ -42,6 +51,12 @@ describe('RoleMappingsLogic', () => { selectedAuthProviders: [ANY_AUTH_PROVIDER], selectedOptions: [], roleMappingErrors: [], + singleUserRoleMapping: null, + singleUserRoleMappings: [], + singleUserRoleMappingFlyoutOpen: false, + userCreated: false, + userFormIsNewUser: true, + userFormUserIsExisting: true, }; const roleGroup = { id: '123', @@ -59,6 +74,8 @@ describe('RoleMappingsLogic', () => { authProviders: [], availableGroups: [roleGroup, defaultGroup], elasticsearchRoles: [], + singleUserRoleMappings: [wsSingleUserRoleMapping], + elasticsearchUsers, }; beforeEach(() => { @@ -71,23 +88,36 @@ describe('RoleMappingsLogic', () => { }); describe('actions', () => { - it('setRoleMappingsData', () => { - RoleMappingsLogic.actions.setRoleMappingsData(mappingsServerProps); + describe('setRoleMappingsData', () => { + it('sets data based on server response from the `mappings` (plural) endpoint', () => { + RoleMappingsLogic.actions.setRoleMappingsData(mappingsServerProps); - expect(RoleMappingsLogic.values.roleMappings).toEqual([wsRoleMapping]); - expect(RoleMappingsLogic.values.dataLoading).toEqual(false); - expect(RoleMappingsLogic.values.multipleAuthProvidersConfig).toEqual(true); - expect(RoleMappingsLogic.values.dataLoading).toEqual(false); - expect(RoleMappingsLogic.values.attributes).toEqual(mappingsServerProps.attributes); - expect(RoleMappingsLogic.values.availableGroups).toEqual(mappingsServerProps.availableGroups); - expect(RoleMappingsLogic.values.includeInAllGroups).toEqual(false); - expect(RoleMappingsLogic.values.elasticsearchRoles).toEqual( - mappingsServerProps.elasticsearchRoles - ); - expect(RoleMappingsLogic.values.selectedOptions).toEqual([ - { label: defaultGroup.name, value: defaultGroup.id }, - ]); - expect(RoleMappingsLogic.values.selectedGroups).toEqual(new Set([defaultGroup.id])); + expect(RoleMappingsLogic.values.roleMappings).toEqual([wsRoleMapping]); + expect(RoleMappingsLogic.values.dataLoading).toEqual(false); + expect(RoleMappingsLogic.values.multipleAuthProvidersConfig).toEqual(true); + expect(RoleMappingsLogic.values.dataLoading).toEqual(false); + expect(RoleMappingsLogic.values.attributes).toEqual(mappingsServerProps.attributes); + expect(RoleMappingsLogic.values.availableGroups).toEqual( + mappingsServerProps.availableGroups + ); + expect(RoleMappingsLogic.values.includeInAllGroups).toEqual(false); + expect(RoleMappingsLogic.values.elasticsearchRoles).toEqual( + mappingsServerProps.elasticsearchRoles + ); + expect(RoleMappingsLogic.values.selectedOptions).toEqual([ + { label: defaultGroup.name, value: defaultGroup.id }, + ]); + expect(RoleMappingsLogic.values.selectedGroups).toEqual(new Set([defaultGroup.id])); + }); + + it('handles fallback if no elasticsearch users present', () => { + RoleMappingsLogic.actions.setRoleMappingsData({ + ...mappingsServerProps, + elasticsearchUsers: [], + }); + + expect(RoleMappingsLogic.values.elasticsearchUser).toEqual(emptyUser); + }); }); it('setRoleMappings', () => { @@ -97,6 +127,26 @@ describe('RoleMappingsLogic', () => { expect(RoleMappingsLogic.values.dataLoading).toEqual(false); }); + describe('setElasticsearchUser', () => { + it('sets user', () => { + RoleMappingsLogic.actions.setElasticsearchUser(elasticsearchUsers[0]); + + expect(RoleMappingsLogic.values.elasticsearchUser).toEqual(elasticsearchUsers[0]); + }); + + it('handles fallback if no user present', () => { + RoleMappingsLogic.actions.setElasticsearchUser(undefined); + + expect(RoleMappingsLogic.values.elasticsearchUser).toEqual(emptyUser); + }); + }); + + it('setSingleUserRoleMapping', () => { + RoleMappingsLogic.actions.setSingleUserRoleMapping(wsSingleUserRoleMapping); + + expect(RoleMappingsLogic.values.singleUserRoleMapping).toEqual(wsSingleUserRoleMapping); + }); + it('handleRoleChange', () => { RoleMappingsLogic.actions.handleRoleChange('user'); @@ -133,6 +183,12 @@ describe('RoleMappingsLogic', () => { expect(RoleMappingsLogic.values.includeInAllGroups).toEqual(true); }); + it('setUserExistingRadioValue', () => { + RoleMappingsLogic.actions.setUserExistingRadioValue(false); + + expect(RoleMappingsLogic.values.userFormUserIsExisting).toEqual(false); + }); + describe('handleAttributeSelectorChange', () => { const elasticsearchRoles = ['foo', 'bar']; @@ -228,16 +284,50 @@ describe('RoleMappingsLogic', () => { expect(clearFlashMessages).toHaveBeenCalled(); }); - it('closeRoleMappingFlyout', () => { + it('openSingleUserRoleMappingFlyout', () => { + mount(mappingsServerProps); + RoleMappingsLogic.actions.openSingleUserRoleMappingFlyout(); + + expect(RoleMappingsLogic.values.singleUserRoleMappingFlyoutOpen).toEqual(true); + expect(clearFlashMessages).toHaveBeenCalled(); + }); + + it('closeUsersAndRolesFlyout', () => { mount({ ...mappingsServerProps, roleMappingFlyoutOpen: true, }); - RoleMappingsLogic.actions.closeRoleMappingFlyout(); + RoleMappingsLogic.actions.closeUsersAndRolesFlyout(); expect(RoleMappingsLogic.values.roleMappingFlyoutOpen).toEqual(false); expect(clearFlashMessages).toHaveBeenCalled(); }); + + it('setElasticsearchUsernameValue', () => { + const username = 'newName'; + RoleMappingsLogic.actions.setElasticsearchUsernameValue(username); + + expect(RoleMappingsLogic.values.elasticsearchUser).toEqual({ + ...RoleMappingsLogic.values.elasticsearchUser, + username, + }); + }); + + it('setElasticsearchEmailValue', () => { + const email = 'newEmail@foo.cats'; + RoleMappingsLogic.actions.setElasticsearchEmailValue(email); + + expect(RoleMappingsLogic.values.elasticsearchUser).toEqual({ + ...RoleMappingsLogic.values.elasticsearchUser, + email, + }); + }); + + it('setUserCreated', () => { + RoleMappingsLogic.actions.setUserCreated(); + + expect(RoleMappingsLogic.values.userCreated).toEqual(true); + }); }); describe('listeners', () => { @@ -303,6 +393,39 @@ describe('RoleMappingsLogic', () => { }); }); + describe('initializeSingleUserRoleMapping', () => { + let setElasticsearchUserSpy: jest.MockedFunction; + let setRoleMappingSpy: jest.MockedFunction; + let setSingleUserRoleMappingSpy: jest.MockedFunction; + beforeEach(() => { + setElasticsearchUserSpy = jest.spyOn(RoleMappingsLogic.actions, 'setElasticsearchUser'); + setRoleMappingSpy = jest.spyOn(RoleMappingsLogic.actions, 'setRoleMapping'); + setSingleUserRoleMappingSpy = jest.spyOn( + RoleMappingsLogic.actions, + 'setSingleUserRoleMapping' + ); + }); + + it('should handle the new user state and only set an empty mapping', () => { + RoleMappingsLogic.actions.initializeSingleUserRoleMapping(); + + expect(setElasticsearchUserSpy).not.toHaveBeenCalled(); + expect(setRoleMappingSpy).not.toHaveBeenCalled(); + expect(setSingleUserRoleMappingSpy).toHaveBeenCalledWith(undefined); + }); + + it('should handle an existing user state and set mapping', () => { + RoleMappingsLogic.actions.setRoleMappingsData(mappingsServerProps); + RoleMappingsLogic.actions.initializeSingleUserRoleMapping( + wsSingleUserRoleMapping.roleMapping.id + ); + + expect(setElasticsearchUserSpy).toHaveBeenCalled(); + expect(setRoleMappingSpy).toHaveBeenCalled(); + expect(setSingleUserRoleMappingSpy).toHaveBeenCalledWith(wsSingleUserRoleMapping); + }); + }); + describe('handleSaveMapping', () => { it('calls API and refreshes list when new mapping', async () => { const initializeRoleMappingsSpy = jest.spyOn( @@ -381,6 +504,100 @@ describe('RoleMappingsLogic', () => { }); }); + describe('handleSaveUser', () => { + it('calls API and refreshes list when new mapping', async () => { + const initializeRoleMappingsSpy = jest.spyOn( + RoleMappingsLogic.actions, + 'initializeRoleMappings' + ); + const setUserCreatedSpy = jest.spyOn(RoleMappingsLogic.actions, 'setUserCreated'); + const setSingleUserRoleMappingSpy = jest.spyOn( + RoleMappingsLogic.actions, + 'setSingleUserRoleMapping' + ); + RoleMappingsLogic.actions.setRoleMappingsData(mappingsServerProps); + + http.post.mockReturnValue(Promise.resolve(mappingsServerProps)); + RoleMappingsLogic.actions.handleSaveUser(); + + expect(http.post).toHaveBeenCalledWith( + '/api/workplace_search/org/single_user_role_mapping', + { + body: JSON.stringify({ + roleMapping: { + groups: [defaultGroup.id], + roleType: 'admin', + allGroups: false, + }, + elasticsearchUser: { + username: elasticsearchUsers[0].username, + email: elasticsearchUsers[0].email, + }, + }), + } + ); + await nextTick(); + + expect(initializeRoleMappingsSpy).toHaveBeenCalled(); + expect(setUserCreatedSpy).toHaveBeenCalled(); + expect(setSingleUserRoleMappingSpy).toHaveBeenCalled(); + }); + + it('calls API and refreshes list when existing mapping', async () => { + const initializeRoleMappingsSpy = jest.spyOn( + RoleMappingsLogic.actions, + 'initializeRoleMappings' + ); + RoleMappingsLogic.actions.setSingleUserRoleMapping(wsSingleUserRoleMapping); + RoleMappingsLogic.actions.handleAllGroupsSelectionChange(true); + + http.put.mockReturnValue(Promise.resolve(mappingsServerProps)); + RoleMappingsLogic.actions.handleSaveUser(); + + expect(http.post).toHaveBeenCalledWith( + '/api/workplace_search/org/single_user_role_mapping', + { + body: JSON.stringify({ + roleMapping: { + groups: [], + roleType: 'admin', + allGroups: true, + id: wsSingleUserRoleMapping.roleMapping.id, + }, + elasticsearchUser: { + username: '', + email: '', + }, + }), + } + ); + await nextTick(); + + expect(initializeRoleMappingsSpy).toHaveBeenCalled(); + }); + + it('handles error', async () => { + const setRoleMappingErrorsSpy = jest.spyOn( + RoleMappingsLogic.actions, + 'setRoleMappingErrors' + ); + + http.post.mockReturnValue( + Promise.reject({ + body: { + attributes: { + errors: ['this is an error'], + }, + }, + }) + ); + RoleMappingsLogic.actions.handleSaveUser(); + await nextTick(); + + expect(setRoleMappingErrorsSpy).toHaveBeenCalledWith(['this is an error']); + }); + }); + describe('handleDeleteMapping', () => { const roleMappingId = 'r1'; @@ -410,5 +627,52 @@ describe('RoleMappingsLogic', () => { expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); }); + + describe('handleUsernameSelectChange', () => { + it('sets elasticsearchUser when match found', () => { + RoleMappingsLogic.actions.setRoleMappingsData(mappingsServerProps); + const setElasticsearchUserSpy = jest.spyOn( + RoleMappingsLogic.actions, + 'setElasticsearchUser' + ); + RoleMappingsLogic.actions.handleUsernameSelectChange(elasticsearchUsers[0].username); + + expect(setElasticsearchUserSpy).toHaveBeenCalledWith(elasticsearchUsers[0]); + }); + + it('does not set elasticsearchUser when no match found', () => { + RoleMappingsLogic.actions.setRoleMappingsData(mappingsServerProps); + const setElasticsearchUserSpy = jest.spyOn( + RoleMappingsLogic.actions, + 'setElasticsearchUser' + ); + RoleMappingsLogic.actions.handleUsernameSelectChange('bogus'); + + expect(setElasticsearchUserSpy).not.toHaveBeenCalled(); + }); + }); + + describe('setUserExistingRadioValue', () => { + it('handles existing user', () => { + RoleMappingsLogic.actions.setRoleMappingsData(mappingsServerProps); + const setElasticsearchUserSpy = jest.spyOn( + RoleMappingsLogic.actions, + 'setElasticsearchUser' + ); + RoleMappingsLogic.actions.setUserExistingRadioValue(true); + + expect(setElasticsearchUserSpy).toHaveBeenCalledWith(elasticsearchUsers[0]); + }); + + it('handles new user', () => { + const setElasticsearchUserSpy = jest.spyOn( + RoleMappingsLogic.actions, + 'setElasticsearchUser' + ); + RoleMappingsLogic.actions.setUserExistingRadioValue(false); + + expect(setElasticsearchUserSpy).toHaveBeenCalledWith(emptyUser); + }); + }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.ts index 76b41b2f383eb..7f26c8738786c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.ts @@ -16,7 +16,7 @@ import { } from '../../../shared/flash_messages'; import { HttpLogic } from '../../../shared/http'; import { ANY_AUTH_PROVIDER } from '../../../shared/role_mapping/constants'; -import { AttributeName } from '../../../shared/types'; +import { AttributeName, SingleUserRoleMapping, ElasticsearchUser } from '../../../shared/types'; import { RoleGroup, WSRoleMapping, Role } from '../../types'; import { @@ -26,19 +26,24 @@ import { DEFAULT_GROUP_NAME, } from './constants'; +type UserMapping = SingleUserRoleMapping; + interface RoleMappingsServerDetails { roleMappings: WSRoleMapping[]; attributes: string[]; authProviders: string[]; availableGroups: RoleGroup[]; + elasticsearchUsers: ElasticsearchUser[]; elasticsearchRoles: string[]; multipleAuthProvidersConfig: boolean; + singleUserRoleMappings: UserMapping[]; } const getFirstAttributeName = (roleMapping: WSRoleMapping): AttributeName => Object.entries(roleMapping.rules)[0][0] as AttributeName; const getFirstAttributeValue = (roleMapping: WSRoleMapping): string => Object.entries(roleMapping.rules)[0][1] as string; +const emptyUser = { username: '', email: '' } as ElasticsearchUser; interface RoleMappingsActions { handleAllGroupsSelectionChange(selected: boolean): { selected: boolean }; @@ -51,21 +56,35 @@ interface RoleMappingsActions { handleDeleteMapping(roleMappingId: string): { roleMappingId: string }; handleGroupSelectionChange(groupIds: string[]): { groupIds: string[] }; handleRoleChange(roleType: Role): { roleType: Role }; + handleUsernameSelectChange(username: string): { username: string }; handleSaveMapping(): void; + handleSaveUser(): void; initializeRoleMapping(roleMappingId?: string): { roleMappingId?: string }; + initializeSingleUserRoleMapping(roleMappingId?: string): { roleMappingId?: string }; initializeRoleMappings(): void; resetState(): void; setRoleMapping(roleMapping: WSRoleMapping): { roleMapping: WSRoleMapping }; + setSingleUserRoleMapping(data?: UserMapping): { singleUserRoleMapping: UserMapping }; setRoleMappings({ roleMappings, }: { roleMappings: WSRoleMapping[]; }): { roleMappings: WSRoleMapping[] }; setRoleMappingsData(data: RoleMappingsServerDetails): RoleMappingsServerDetails; + setElasticsearchUser( + elasticsearchUser?: ElasticsearchUser + ): { elasticsearchUser: ElasticsearchUser }; + setDefaultGroup(availableGroups: RoleGroup[]): { availableGroups: RoleGroup[] }; openRoleMappingFlyout(): void; - closeRoleMappingFlyout(): void; + openSingleUserRoleMappingFlyout(): void; + closeUsersAndRolesFlyout(): void; setRoleMappingErrors(errors: string[]): { errors: string[] }; enableRoleBasedAccess(): void; + setUserExistingRadioValue(userFormUserIsExisting: boolean): { userFormUserIsExisting: boolean }; + setElasticsearchUsernameValue(username: string): { username: string }; + setElasticsearchEmailValue(email: string): { email: string }; + setUserCreated(): void; + setUserFormIsNewUser(userFormIsNewUser: boolean): { userFormIsNewUser: boolean }; } interface RoleMappingsValues { @@ -77,26 +96,37 @@ interface RoleMappingsValues { availableGroups: RoleGroup[]; dataLoading: boolean; elasticsearchRoles: string[]; + elasticsearchUsers: ElasticsearchUser[]; + elasticsearchUser: ElasticsearchUser; multipleAuthProvidersConfig: boolean; roleMapping: WSRoleMapping | null; roleMappings: WSRoleMapping[]; + singleUserRoleMapping: UserMapping | null; + singleUserRoleMappings: UserMapping[]; roleType: Role; selectedAuthProviders: string[]; selectedGroups: Set; roleMappingFlyoutOpen: boolean; + singleUserRoleMappingFlyoutOpen: boolean; selectedOptions: EuiComboBoxOptionOption[]; roleMappingErrors: string[]; + userFormUserIsExisting: boolean; + userCreated: boolean; + userFormIsNewUser: boolean; } export const RoleMappingsLogic = kea>({ - path: ['enterprise_search', 'workplace_search', 'role_mappings'], + path: ['enterprise_search', 'workplace_search', 'users_and_roles'], actions: { setRoleMappingsData: (data: RoleMappingsServerDetails) => data, setRoleMapping: (roleMapping: WSRoleMapping) => ({ roleMapping }), + setElasticsearchUser: (elasticsearchUser: ElasticsearchUser) => ({ elasticsearchUser }), + setSingleUserRoleMapping: (singleUserRoleMapping: UserMapping) => ({ singleUserRoleMapping }), setRoleMappings: ({ roleMappings }: { roleMappings: WSRoleMapping[] }) => ({ roleMappings }), setRoleMappingErrors: (errors: string[]) => ({ errors }), handleAuthProviderChange: (value: string[]) => ({ value }), handleRoleChange: (roleType: Role) => ({ roleType }), + handleUsernameSelectChange: (username: string) => ({ username }), handleGroupSelectionChange: (groupIds: string[]) => ({ groupIds }), handleAttributeSelectorChange: (value: string, firstElasticsearchRole: string) => ({ value, @@ -105,13 +135,22 @@ export const RoleMappingsLogic = kea ({ value }), handleAllGroupsSelectionChange: (selected: boolean) => ({ selected }), enableRoleBasedAccess: true, + openSingleUserRoleMappingFlyout: true, + setUserExistingRadioValue: (userFormUserIsExisting: boolean) => ({ userFormUserIsExisting }), resetState: true, initializeRoleMappings: true, + initializeSingleUserRoleMapping: (roleMappingId?: string) => ({ roleMappingId }), initializeRoleMapping: (roleMappingId?: string) => ({ roleMappingId }), handleDeleteMapping: (roleMappingId: string) => ({ roleMappingId }), handleSaveMapping: true, + handleSaveUser: true, + setDefaultGroup: (availableGroups: RoleGroup[]) => ({ availableGroups }), openRoleMappingFlyout: true, - closeRoleMappingFlyout: false, + closeUsersAndRolesFlyout: false, + setElasticsearchUsernameValue: (username: string) => ({ username }), + setElasticsearchEmailValue: (email: string) => ({ email }), + setUserCreated: true, + setUserFormIsNewUser: (userFormIsNewUser: boolean) => ({ userFormIsNewUser }), }, reducers: { dataLoading: [ @@ -131,6 +170,13 @@ export const RoleMappingsLogic = kea [], }, ], + singleUserRoleMappings: [ + [], + { + setRoleMappingsData: (_, { singleUserRoleMappings }) => singleUserRoleMappings, + resetState: () => [], + }, + ], multipleAuthProvidersConfig: [ false, { @@ -154,6 +200,13 @@ export const RoleMappingsLogic = kea elasticsearchRoles, + closeUsersAndRolesFlyout: () => [ANY_AUTH_PROVIDER], + }, + ], + elasticsearchUsers: [ + [], + { + setRoleMappingsData: (_, { elasticsearchUsers }) => elasticsearchUsers, }, ], roleMapping: [ @@ -161,7 +214,14 @@ export const RoleMappingsLogic = kea roleMapping, resetState: () => null, - closeRoleMappingFlyout: () => null, + closeUsersAndRolesFlyout: () => null, + }, + ], + singleUserRoleMapping: [ + null, + { + setSingleUserRoleMapping: (_, { singleUserRoleMapping }) => singleUserRoleMapping || null, + closeUsersAndRolesFlyout: () => null, }, ], roleType: [ @@ -176,6 +236,7 @@ export const RoleMappingsLogic = kea roleMapping.allGroups, handleAllGroupsSelectionChange: (_, { selected }) => selected, + closeUsersAndRolesFlyout: () => false, }, ], attributeValue: [ @@ -186,7 +247,7 @@ export const RoleMappingsLogic = kea value, resetState: () => '', - closeRoleMappingFlyout: () => '', + closeUsersAndRolesFlyout: () => '', }, ], attributeName: [ @@ -195,7 +256,7 @@ export const RoleMappingsLogic = kea getFirstAttributeName(roleMapping), handleAttributeSelectorChange: (_, { value }) => value, resetState: () => 'username', - closeRoleMappingFlyout: () => 'username', + closeUsersAndRolesFlyout: () => 'username', }, ], selectedGroups: [ @@ -207,6 +268,12 @@ export const RoleMappingsLogic = kea group.name === DEFAULT_GROUP_NAME) .map((group) => group.id) ), + setDefaultGroup: (_, { availableGroups }) => + new Set( + availableGroups + .filter((group) => group.name === DEFAULT_GROUP_NAME) + .map((group) => group.id) + ), setRoleMapping: (_, { roleMapping }) => new Set(roleMapping.groups.map((group: RoleGroup) => group.id)), handleGroupSelectionChange: (_, { groupIds }) => { @@ -215,6 +282,7 @@ export const RoleMappingsLogic = kea new Set(), }, ], availableAuthProviders: [ @@ -244,17 +312,61 @@ export const RoleMappingsLogic = kea true, - closeRoleMappingFlyout: () => false, + closeUsersAndRolesFlyout: () => false, initializeRoleMappings: () => false, initializeRoleMapping: () => true, }, ], + singleUserRoleMappingFlyoutOpen: [ + false, + { + openSingleUserRoleMappingFlyout: () => true, + closeUsersAndRolesFlyout: () => false, + initializeSingleUserRoleMapping: () => true, + }, + ], roleMappingErrors: [ [], { setRoleMappingErrors: (_, { errors }) => errors, handleSaveMapping: () => [], - closeRoleMappingFlyout: () => [], + closeUsersAndRolesFlyout: () => [], + }, + ], + userFormUserIsExisting: [ + true, + { + setUserExistingRadioValue: (_, { userFormUserIsExisting }) => userFormUserIsExisting, + closeUsersAndRolesFlyout: () => true, + }, + ], + elasticsearchUser: [ + emptyUser, + { + setRoleMappingsData: (_, { elasticsearchUsers }) => elasticsearchUsers[0] || emptyUser, + setElasticsearchUser: (_, { elasticsearchUser }) => elasticsearchUser || emptyUser, + setElasticsearchUsernameValue: (state, { username }) => ({ + ...state, + username, + }), + setElasticsearchEmailValue: (state, { email }) => ({ + ...state, + email, + }), + closeUsersAndRolesFlyout: () => emptyUser, + }, + ], + userCreated: [ + false, + { + setUserCreated: () => true, + closeUsersAndRolesFlyout: () => false, + }, + ], + userFormIsNewUser: [ + true, + { + setUserFormIsNewUser: (_, { userFormIsNewUser }) => userFormIsNewUser, }, ], }, @@ -296,6 +408,18 @@ export const RoleMappingsLogic = kea id === roleMappingId); if (roleMapping) actions.setRoleMapping(roleMapping); }, + initializeSingleUserRoleMapping: ({ roleMappingId }) => { + const singleUserRoleMapping = values.singleUserRoleMappings.find( + ({ roleMapping }) => roleMapping.id === roleMappingId + ); + + if (singleUserRoleMapping) { + actions.setElasticsearchUser(singleUserRoleMapping.elasticsearchUser); + actions.setRoleMapping(singleUserRoleMapping.roleMapping); + } + actions.setSingleUserRoleMapping(singleUserRoleMapping); + actions.setUserFormIsNewUser(!singleUserRoleMapping); + }, handleDeleteMapping: async ({ roleMappingId }) => { const { http } = HttpLogic.values; const route = `/api/workplace_search/org/role_mappings/${roleMappingId}`; @@ -349,11 +473,59 @@ export const RoleMappingsLogic = kea { clearFlashMessages(); }, - closeRoleMappingFlyout: () => { + handleSaveUser: async () => { + const { http } = HttpLogic.values; + const { + roleType, + singleUserRoleMapping, + includeInAllGroups, + selectedGroups, + elasticsearchUser: { email, username }, + } = values; + + const body = JSON.stringify({ + roleMapping: { + groups: includeInAllGroups ? [] : Array.from(selectedGroups), + roleType, + allGroups: includeInAllGroups, + id: singleUserRoleMapping?.roleMapping?.id, + }, + elasticsearchUser: { + username, + email, + }, + }); + + try { + const response = await http.post('/api/workplace_search/org/single_user_role_mapping', { + body, + }); + actions.setSingleUserRoleMapping(response); + actions.setUserCreated(); + actions.initializeRoleMappings(); + } catch (e) { + actions.setRoleMappingErrors(e?.body?.attributes?.errors); + } + }, + closeUsersAndRolesFlyout: () => { clearFlashMessages(); + const firstUser = values.elasticsearchUsers[0]; + actions.setElasticsearchUser(firstUser); + actions.setDefaultGroup(values.availableGroups); }, openRoleMappingFlyout: () => { clearFlashMessages(); }, + openSingleUserRoleMappingFlyout: () => { + clearFlashMessages(); + }, + setUserExistingRadioValue: ({ userFormUserIsExisting }) => { + const firstUser = values.elasticsearchUsers[0]; + actions.setElasticsearchUser(userFormUserIsExisting ? firstUser : emptyUser); + }, + handleUsernameSelectChange: ({ username }) => { + const user = values.elasticsearchUsers.find((u) => u.username === username); + if (user) actions.setElasticsearchUser(user); + }, }), }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/user.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/user.test.tsx new file mode 100644 index 0000000000000..32ee1a7f22875 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/user.test.tsx @@ -0,0 +1,123 @@ +/* + * 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 '../../../__mocks__/react_router'; +import '../../../__mocks__/shallow_useeffect.mock'; +import { setMockActions, setMockValues } from '../../../__mocks__/kea_logic'; +import { groups } from '../../__mocks__/groups.mock'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { UserFlyout, UserAddedInfo, UserInvitationCallout } from '../../../shared/role_mapping'; +import { elasticsearchUsers } from '../../../shared/role_mapping/__mocks__/elasticsearch_users'; +import { wsSingleUserRoleMapping } from '../../../shared/role_mapping/__mocks__/roles'; + +import { GroupAssignmentSelector } from './group_assignment_selector'; +import { User } from './user'; + +describe('User', () => { + const handleSaveUser = jest.fn(); + const closeUsersAndRolesFlyout = jest.fn(); + const setUserExistingRadioValue = jest.fn(); + const setElasticsearchUsernameValue = jest.fn(); + const setElasticsearchEmailValue = jest.fn(); + const handleRoleChange = jest.fn(); + const handleUsernameSelectChange = jest.fn(); + + const mockValues = { + availableGroups: [], + singleUserRoleMapping: null, + userFormUserIsExisting: false, + elasticsearchUsers: [], + elasticsearchUser: {}, + roleType: 'admin', + roleMappingErrors: [], + userCreated: false, + userFormIsNewUser: false, + }; + + beforeEach(() => { + setMockActions({ + handleSaveUser, + closeUsersAndRolesFlyout, + setUserExistingRadioValue, + setElasticsearchUsernameValue, + setElasticsearchEmailValue, + handleRoleChange, + handleUsernameSelectChange, + }); + + setMockValues(mockValues); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(UserFlyout)).toHaveLength(1); + }); + + it('renders group assignment selector when groups present', () => { + setMockValues({ ...mockValues, availableGroups: groups }); + const wrapper = shallow(); + + expect(wrapper.find(GroupAssignmentSelector)).toHaveLength(1); + }); + + it('renders userInvitationCallout', () => { + setMockValues({ + ...mockValues, + singleUserRoleMapping: wsSingleUserRoleMapping, + }); + const wrapper = shallow(); + + expect(wrapper.find(UserInvitationCallout)).toHaveLength(1); + }); + + it('renders user added info when user created', () => { + setMockValues({ + ...mockValues, + singleUserRoleMapping: wsSingleUserRoleMapping, + userCreated: true, + }); + const wrapper = shallow(); + + expect(wrapper.find(UserAddedInfo)).toHaveLength(1); + }); + + it('disables form when username value not present', () => { + setMockValues({ + ...mockValues, + singleUserRoleMapping: wsSingleUserRoleMapping, + elasticsearchUsers, + elasticsearchUser: { + username: null, + email: 'email@user.com', + }, + }); + const wrapper = shallow(); + + expect(wrapper.find(UserFlyout).prop('disabled')).toEqual(true); + }); + + it('enables form when userFormUserIsExisting', () => { + setMockValues({ + ...mockValues, + userFormUserIsExisting: true.valueOf, + singleUserRoleMapping: wsSingleUserRoleMapping, + elasticsearchUsers, + elasticsearchUser: { + username: null, + email: 'email@user.com', + }, + }); + const wrapper = shallow(); + + expect(wrapper.find(UserFlyout).prop('disabled')).toEqual(false); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/user.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/user.tsx new file mode 100644 index 0000000000000..bfb32ee31c121 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/user.tsx @@ -0,0 +1,103 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useActions, useValues } from 'kea'; + +import { EuiForm } from '@elastic/eui'; + +import { getWorkplaceSearchUrl } from '../../../shared/enterprise_search_url'; +import { + UserFlyout, + UserSelector, + UserAddedInfo, + UserInvitationCallout, +} from '../../../shared/role_mapping'; +import { Role } from '../../types'; + +import { GroupAssignmentSelector } from './group_assignment_selector'; +import { RoleMappingsLogic } from './role_mappings_logic'; + +const roleTypes = (['admin', 'user'] as unknown) as Role[]; + +export const User: React.FC = () => { + const { + handleSaveUser, + closeUsersAndRolesFlyout, + setUserExistingRadioValue, + setElasticsearchUsernameValue, + setElasticsearchEmailValue, + handleRoleChange, + handleUsernameSelectChange, + } = useActions(RoleMappingsLogic); + + const { + availableGroups, + singleUserRoleMapping, + userFormUserIsExisting, + elasticsearchUsers, + elasticsearchUser, + roleType, + roleMappingErrors, + userCreated, + userFormIsNewUser, + } = useValues(RoleMappingsLogic); + + const showGroupAssignmentSelector = availableGroups.length > 0; + const hasAvailableUsers = elasticsearchUsers.length > 0; + const flyoutDisabled = + (!userFormUserIsExisting || !hasAvailableUsers) && !elasticsearchUser.username; + + const userAddedInfo = singleUserRoleMapping && ( + + ); + + const userInvitationCallout = singleUserRoleMapping?.invitation && ( + + ); + + const createUserForm = ( + 0} error={roleMappingErrors}> + + {showGroupAssignmentSelector && } + + ); + + return ( + + {userCreated ? userAddedInfo : createUserForm} + {userInvitationCallout} + + ); +}; diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.test.ts index 7d9f08627516b..dfb9765f834b6 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.test.ts @@ -11,6 +11,7 @@ import { registerEnableRoleMappingsRoute, registerRoleMappingsRoute, registerRoleMappingRoute, + registerUserRoute, } from './role_mappings'; const roleMappingBaseSchema = { @@ -160,4 +161,52 @@ describe('role mappings routes', () => { }); }); }); + + describe('POST /api/app_search/single_user_role_mapping', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'post', + path: '/api/app_search/single_user_role_mapping', + }); + + registerUserRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + describe('validates', () => { + it('correctly', () => { + const request = { + body: { + roleMapping: { + engines: ['foo', 'bar'], + roleType: 'admin', + accessAllEngines: true, + id: '123asf', + }, + elasticsearchUser: { + username: 'user2@elastic.co', + email: 'user2', + }, + }, + }; + mockRouter.shouldValidate(request); + }); + + it('missing required fields', () => { + const request = { body: {} }; + mockRouter.shouldThrow(request); + }); + }); + + it('creates a request handler', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/as/role_mappings/upsert_single_user_role_mapping', + }); + }); + }); }); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.ts index da620be2ea950..d90a005cb2532 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.ts @@ -93,8 +93,34 @@ export function registerRoleMappingRoute({ ); } +export function registerUserRoute({ router, enterpriseSearchRequestHandler }: RouteDependencies) { + router.post( + { + path: '/api/app_search/single_user_role_mapping', + validate: { + body: schema.object({ + roleMapping: schema.object({ + engines: schema.arrayOf(schema.string()), + roleType: schema.string(), + accessAllEngines: schema.boolean(), + id: schema.maybe(schema.string()), + }), + elasticsearchUser: schema.object({ + username: schema.string(), + email: schema.string(), + }), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/as/role_mappings/upsert_single_user_role_mapping', + }) + ); +} + export const registerRoleMappingsRoutes = (dependencies: RouteDependencies) => { registerEnableRoleMappingsRoute(dependencies); registerRoleMappingsRoute(dependencies); registerRoleMappingRoute(dependencies); + registerUserRoute(dependencies); }; diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.test.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.test.ts index aa0e9983166c0..ef8f1bd63f5d3 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.test.ts @@ -11,6 +11,7 @@ import { registerOrgEnableRoleMappingsRoute, registerOrgRoleMappingsRoute, registerOrgRoleMappingRoute, + registerOrgUserRoute, } from './role_mappings'; describe('role mappings routes', () => { @@ -128,4 +129,52 @@ describe('role mappings routes', () => { }); }); }); + + describe('POST /api/workplace_search/org/single_user_role_mapping', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'post', + path: '/api/workplace_search/org/single_user_role_mapping', + }); + + registerOrgUserRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + describe('validates', () => { + it('correctly', () => { + const request = { + body: { + roleMapping: { + groups: ['foo', 'bar'], + roleType: 'admin', + allGroups: true, + id: '123asf', + }, + elasticsearchUser: { + username: 'user2@elastic.co', + email: 'user2', + }, + }, + }; + mockRouter.shouldValidate(request); + }); + + it('missing required fields', () => { + const request = { body: {} }; + mockRouter.shouldThrow(request); + }); + }); + + it('creates a request handler', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/ws/org/role_mappings/upsert_single_user_role_mapping', + }); + }); + }); }); diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.ts index cea7bcb311ce8..e6f4919ed2a2f 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.ts @@ -93,8 +93,37 @@ export function registerOrgRoleMappingRoute({ ); } +export function registerOrgUserRoute({ + router, + enterpriseSearchRequestHandler, +}: RouteDependencies) { + router.post( + { + path: '/api/workplace_search/org/single_user_role_mapping', + validate: { + body: schema.object({ + roleMapping: schema.object({ + groups: schema.arrayOf(schema.string()), + roleType: schema.string(), + allGroups: schema.boolean(), + id: schema.maybe(schema.string()), + }), + elasticsearchUser: schema.object({ + username: schema.string(), + email: schema.string(), + }), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/ws/org/role_mappings/upsert_single_user_role_mapping', + }) + ); +} + export const registerRoleMappingsRoutes = (dependencies: RouteDependencies) => { registerOrgEnableRoleMappingsRoute(dependencies); registerOrgRoleMappingsRoute(dependencies); registerOrgRoleMappingRoute(dependencies); + registerOrgUserRoute(dependencies); }; From 9b56549c6c26a1f86c44e709b12c4e295aaebda3 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Thu, 24 Jun 2021 09:05:26 -0400 Subject: [PATCH 17/69] [Cases] Including owner when patching a comment Closes #102732 (#103020) * Including owner when patching a comment * Fixing tests --- .../cases/public/containers/api.test.tsx | 31 +++++---- x-pack/plugins/cases/public/containers/api.ts | 26 ++++--- .../containers/use_update_comment.test.tsx | 67 +++++++++---------- .../public/containers/use_update_comment.tsx | 13 ++-- 4 files changed, 77 insertions(+), 60 deletions(-) diff --git a/x-pack/plugins/cases/public/containers/api.test.tsx b/x-pack/plugins/cases/public/containers/api.test.tsx index abdee387a2c42..30a76e28e7485 100644 --- a/x-pack/plugins/cases/public/containers/api.test.tsx +++ b/x-pack/plugins/cases/public/containers/api.test.tsx @@ -363,13 +363,14 @@ describe('Case Configuration API', () => { }); test('check url, method, signal', async () => { - await patchComment( - basicCase.id, - basicCase.comments[0].id, - 'updated comment', - basicCase.comments[0].version, - abortCtrl.signal - ); + await patchComment({ + caseId: basicCase.id, + commentId: basicCase.comments[0].id, + commentUpdate: 'updated comment', + version: basicCase.comments[0].version, + signal: abortCtrl.signal, + owner: SECURITY_SOLUTION_OWNER, + }); expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/${basicCase.id}/comments`, { method: 'PATCH', body: JSON.stringify({ @@ -377,19 +378,21 @@ describe('Case Configuration API', () => { type: CommentType.user, id: basicCase.comments[0].id, version: basicCase.comments[0].version, + owner: SECURITY_SOLUTION_OWNER, }), signal: abortCtrl.signal, }); }); test('happy path', async () => { - const resp = await patchComment( - basicCase.id, - basicCase.comments[0].id, - 'updated comment', - basicCase.comments[0].version, - abortCtrl.signal - ); + const resp = await patchComment({ + caseId: basicCase.id, + commentId: basicCase.comments[0].id, + commentUpdate: 'updated comment', + version: basicCase.comments[0].version, + signal: abortCtrl.signal, + owner: SECURITY_SOLUTION_OWNER, + }); expect(resp).toEqual(basicCase); }); }); diff --git a/x-pack/plugins/cases/public/containers/api.ts b/x-pack/plugins/cases/public/containers/api.ts index 1a2a92850a4ad..b144a874cfc53 100644 --- a/x-pack/plugins/cases/public/containers/api.ts +++ b/x-pack/plugins/cases/public/containers/api.ts @@ -283,14 +283,23 @@ export const postComment = async ( return convertToCamelCase(decodeCaseResponse(response)); }; -export const patchComment = async ( - caseId: string, - commentId: string, - commentUpdate: string, - version: string, - signal: AbortSignal, - subCaseId?: string -): Promise => { +export const patchComment = async ({ + caseId, + commentId, + commentUpdate, + version, + signal, + owner, + subCaseId, +}: { + caseId: string; + commentId: string; + commentUpdate: string; + version: string; + signal: AbortSignal; + owner: string; + subCaseId?: string; +}): Promise => { const response = await KibanaServices.get().http.fetch(getCaseCommentsUrl(caseId), { method: 'PATCH', body: JSON.stringify({ @@ -298,6 +307,7 @@ export const patchComment = async ( type: CommentType.user, id: commentId, version, + owner, }), ...(subCaseId ? { query: { subCaseId } } : {}), signal, diff --git a/x-pack/plugins/cases/public/containers/use_update_comment.test.tsx b/x-pack/plugins/cases/public/containers/use_update_comment.test.tsx index b936eb126f0d4..14cc4dfab3599 100644 --- a/x-pack/plugins/cases/public/containers/use_update_comment.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_update_comment.test.tsx @@ -5,10 +5,13 @@ * 2.0. */ +import React from 'react'; import { renderHook, act } from '@testing-library/react-hooks'; import { useUpdateComment, UseUpdateComment } from './use_update_comment'; import { basicCase, basicCaseCommentPatch, basicSubCaseId } from './mock'; import * as api from './api'; +import { TestProviders } from '../common/mock'; +import { SECURITY_SOLUTION_OWNER } from '../../common'; jest.mock('./api'); jest.mock('../common/lib/kibana'); @@ -25,6 +28,12 @@ describe('useUpdateComment', () => { updateCase, version: basicCase.comments[0].version, }; + + const renderHookUseUpdateComment = () => + renderHook(() => useUpdateComment(), { + wrapper: ({ children }) => {children}, + }); + beforeEach(() => { jest.clearAllMocks(); jest.restoreAllMocks(); @@ -32,9 +41,7 @@ describe('useUpdateComment', () => { it('init', async () => { await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - useUpdateComment() - ); + const { result, waitForNextUpdate } = renderHookUseUpdateComment(); await waitForNextUpdate(); expect(result.current).toEqual({ isLoadingIds: [], @@ -48,21 +55,20 @@ describe('useUpdateComment', () => { const spyOnPatchComment = jest.spyOn(api, 'patchComment'); await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - useUpdateComment() - ); + const { result, waitForNextUpdate } = renderHookUseUpdateComment(); await waitForNextUpdate(); result.current.patchComment(sampleUpdate); await waitForNextUpdate(); - expect(spyOnPatchComment).toBeCalledWith( - basicCase.id, - basicCase.comments[0].id, - 'updated comment', - basicCase.comments[0].version, - abortCtrl.signal, - undefined - ); + expect(spyOnPatchComment).toBeCalledWith({ + caseId: basicCase.id, + commentId: basicCase.comments[0].id, + commentUpdate: 'updated comment', + version: basicCase.comments[0].version, + signal: abortCtrl.signal, + owner: SECURITY_SOLUTION_OWNER, + subCaseId: undefined, + }); }); }); @@ -70,29 +76,26 @@ describe('useUpdateComment', () => { const spyOnPatchComment = jest.spyOn(api, 'patchComment'); await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - useUpdateComment() - ); + const { result, waitForNextUpdate } = renderHookUseUpdateComment(); await waitForNextUpdate(); result.current.patchComment({ ...sampleUpdate, subCaseId: basicSubCaseId }); await waitForNextUpdate(); - expect(spyOnPatchComment).toBeCalledWith( - basicCase.id, - basicCase.comments[0].id, - 'updated comment', - basicCase.comments[0].version, - abortCtrl.signal, - basicSubCaseId - ); + expect(spyOnPatchComment).toBeCalledWith({ + caseId: basicCase.id, + commentId: basicCase.comments[0].id, + commentUpdate: 'updated comment', + version: basicCase.comments[0].version, + signal: abortCtrl.signal, + owner: SECURITY_SOLUTION_OWNER, + subCaseId: basicSubCaseId, + }); }); }); it('patch comment', async () => { await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - useUpdateComment() - ); + const { result, waitForNextUpdate } = renderHookUseUpdateComment(); await waitForNextUpdate(); result.current.patchComment(sampleUpdate); await waitForNextUpdate(); @@ -108,9 +111,7 @@ describe('useUpdateComment', () => { it('set isLoading to true when posting case', async () => { await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - useUpdateComment() - ); + const { result, waitForNextUpdate } = renderHookUseUpdateComment(); await waitForNextUpdate(); result.current.patchComment(sampleUpdate); @@ -125,9 +126,7 @@ describe('useUpdateComment', () => { }); await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - useUpdateComment() - ); + const { result, waitForNextUpdate } = renderHookUseUpdateComment(); await waitForNextUpdate(); result.current.patchComment(sampleUpdate); diff --git a/x-pack/plugins/cases/public/containers/use_update_comment.tsx b/x-pack/plugins/cases/public/containers/use_update_comment.tsx index 478d7ebf1fc32..3c307d86ac7bc 100644 --- a/x-pack/plugins/cases/public/containers/use_update_comment.tsx +++ b/x-pack/plugins/cases/public/containers/use_update_comment.tsx @@ -7,6 +7,7 @@ import { useReducer, useCallback, useRef, useEffect } from 'react'; import { useToasts } from '../common/lib/kibana'; +import { useOwnerContext } from '../components/owner_context/use_owner_context'; import { patchComment } from './api'; import * as i18n from './translations'; import { Case } from './types'; @@ -72,6 +73,9 @@ export const useUpdateComment = (): UseUpdateComment => { const toasts = useToasts(); const isCancelledRef = useRef(false); const abortCtrlRef = useRef(new AbortController()); + // this hook guarantees that there will be at least one value in the owner array, we'll + // just use the first entry just in case there are more than one entry + const owner = useOwnerContext()[0]; const dispatchUpdateComment = useCallback( async ({ @@ -89,14 +93,15 @@ export const useUpdateComment = (): UseUpdateComment => { abortCtrlRef.current = new AbortController(); dispatch({ type: 'FETCH_INIT', payload: commentId }); - const response = await patchComment( + const response = await patchComment({ caseId, commentId, commentUpdate, version, - abortCtrlRef.current.signal, - subCaseId - ); + signal: abortCtrlRef.current.signal, + subCaseId, + owner, + }); if (!isCancelledRef.current) { updateCase(response); From 1ef5a6aa05e9b4bd0fb96809b979877e7081f654 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20G=C3=B3mez?= Date: Thu, 24 Jun 2021 15:06:07 +0200 Subject: [PATCH 18/69] [Fleet][Logs UI] Prevent double loading of entries in `` component. (#102980) * Use better loading indicator for `useLogSource` * Use clearer name for the loading entries flag * Reuse query object if its value persists --- .../public/components/log_stream/log_stream.tsx | 6 +++--- .../public/containers/logs/log_stream/index.ts | 13 ++++++++++--- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/infra/public/components/log_stream/log_stream.tsx b/x-pack/plugins/infra/public/components/log_stream/log_stream.tsx index 0087d559a42e6..ff9b749911c84 100644 --- a/x-pack/plugins/infra/public/components/log_stream/log_stream.tsx +++ b/x-pack/plugins/infra/public/components/log_stream/log_stream.tsx @@ -112,7 +112,7 @@ Read more at https://github.com/elastic/kibana/blob/master/src/plugins/kibana_re const { derivedIndexPattern, - isLoadingSourceConfiguration, + isLoading: isLoadingSource, loadSource, sourceConfiguration, } = useLogSource({ @@ -138,7 +138,7 @@ Read more at https://github.com/elastic/kibana/blob/master/src/plugins/kibana_re hasMoreAfter, hasMoreBefore, isLoadingMore, - isReloading, + isReloading: isLoadingEntries, } = useLogStream({ sourceId, startTimestamp, @@ -198,7 +198,7 @@ Read more at https://github.com/elastic/kibana/blob/master/src/plugins/kibana_re items={streamItems} scale="medium" wrap={true} - isReloading={isLoadingSourceConfiguration || isReloading} + isReloading={isLoadingSource || isLoadingEntries} isLoadingMore={isLoadingMore} hasMoreBeforeStart={hasMoreBefore} hasMoreAfterEnd={hasMoreAfter} diff --git a/x-pack/plugins/infra/public/containers/logs/log_stream/index.ts b/x-pack/plugins/infra/public/containers/logs/log_stream/index.ts index 021aa8f79fe59..4cdeb678c432b 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_stream/index.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_stream/index.ts @@ -5,8 +5,9 @@ * 2.0. */ +import { isEqual } from 'lodash'; import createContainer from 'constate'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState, useRef } from 'react'; import usePrevious from 'react-use/lib/usePrevious'; import useSetState from 'react-use/lib/useSetState'; import { esQuery } from '../../../../../../../src/plugins/data/public'; @@ -65,6 +66,12 @@ export function useLogStream({ const prevStartTimestamp = usePrevious(startTimestamp); const prevEndTimestamp = usePrevious(endTimestamp); + const cachedQuery = useRef(query); + + if (!isEqual(query, cachedQuery)) { + cachedQuery.current = query; + } + useEffect(() => { if (prevStartTimestamp && prevStartTimestamp > startTimestamp) { setState({ hasMoreBefore: true }); @@ -82,10 +89,10 @@ export function useLogStream({ sourceId, startTimestamp, endTimestamp, - query, + query: cachedQuery.current, columnOverrides: columns, }), - [columns, endTimestamp, query, sourceId, startTimestamp] + [columns, endTimestamp, cachedQuery, sourceId, startTimestamp] ); const { From 0a2042eed55100f46766faefd03aeab2dc3607c3 Mon Sep 17 00:00:00 2001 From: Tim Roes Date: Thu, 24 Jun 2021 15:14:48 +0200 Subject: [PATCH 19/69] Prevent showing filter on unfilterable fields (#103241) --- .../discover_grid/discover_grid_cell_actions.test.tsx | 9 ++++++++- .../discover_grid/discover_grid_cell_actions.tsx | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_cell_actions.test.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_cell_actions.test.tsx index 965d3cb6a30c4..de3c55ad7a869 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid_cell_actions.test.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_cell_actions.test.tsx @@ -9,14 +9,21 @@ import React from 'react'; import { mountWithIntl } from '@kbn/test/jest'; import { findTestSubject } from '@elastic/eui/lib/test'; -import { FilterInBtn, FilterOutBtn } from './discover_grid_cell_actions'; +import { FilterInBtn, FilterOutBtn, buildCellActions } from './discover_grid_cell_actions'; import { DiscoverGridContext } from './discover_grid_context'; import { indexPatternMock } from '../../../__mocks__/index_pattern'; import { esHits } from '../../../__mocks__/es_hits'; import { EuiButton } from '@elastic/eui'; +import { IndexPatternField } from 'src/plugins/data/common'; describe('Discover cell actions ', function () { + it('should not show cell actions for unfilterable fields', async () => { + expect( + buildCellActions({ name: 'foo', filterable: false } as IndexPatternField) + ).toBeUndefined(); + }); + it('triggers filter function when FilterInBtn is clicked', async () => { const contextMock = { expanded: undefined, diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_cell_actions.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_cell_actions.tsx index 4e9218f0881cd..ab80cd3e7b461 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid_cell_actions.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_cell_actions.tsx @@ -79,7 +79,7 @@ export const FilterOutBtn = ({ }; export function buildCellActions(field: IndexPatternField) { - if (!field.aggregatable && !field.searchable) { + if (!field.filterable) { return undefined; } From 4e38dfee1430889667b5333ac25a00a8ef2ce89e Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Thu, 24 Jun 2021 14:15:44 +0100 Subject: [PATCH 20/69] skip flaky suite (#98240) --- test/api_integration/apis/ui_counters/ui_counters.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/api_integration/apis/ui_counters/ui_counters.ts b/test/api_integration/apis/ui_counters/ui_counters.ts index 2be6ea4341fb0..019dcfd621655 100644 --- a/test/api_integration/apis/ui_counters/ui_counters.ts +++ b/test/api_integration/apis/ui_counters/ui_counters.ts @@ -56,7 +56,8 @@ export default function ({ getService }: FtrProviderContext) { return savedObject; }; - describe('UI Counters API', () => { + // FLAKY: https://github.com/elastic/kibana/issues/98240 + describe.skip('UI Counters API', () => { const dayDate = moment().format('DDMMYYYY'); before(async () => await esArchiver.emptyKibanaIndex()); From 4266957a0df426cb88e9f8313e18d1f8c26b8652 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Thu, 24 Jun 2021 15:20:28 +0200 Subject: [PATCH 21/69] fix filter input debouncing (#103087) --- .../lens/public/indexpattern_datasource/query_input.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/query_input.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/query_input.tsx index a67199a9d3432..1b418ee3b408f 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/query_input.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/query_input.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; +import { isEqual } from 'lodash'; import { QueryStringInput, Query } from '../../../../../src/plugins/data/public'; import { useDebouncedValue } from '../shared_components'; @@ -36,7 +37,11 @@ export const QueryInput = ({ bubbleSubmitEvent={false} indexPatterns={[indexPatternTitle]} query={inputValue} - onChange={handleInputChange} + onChange={(newQuery) => { + if (!isEqual(newQuery, inputValue)) { + handleInputChange(newQuery); + } + }} onSubmit={() => { if (inputValue.query) { onSubmit(); From b1b182bdec007528722d2de5a12c2dde7a09c1d3 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Thu, 24 Jun 2021 15:23:39 +0200 Subject: [PATCH 22/69] [Lens] Add new error case for mixed x axes (#102861) --- .../xy_visualization/visualization.test.ts | 53 +++++++++++++++++++ .../public/xy_visualization/visualization.tsx | 36 +++++++++++-- 2 files changed, 84 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts b/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts index f2840b6d3844b..dee0e5763dee4 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts @@ -872,6 +872,59 @@ describe('xy_visualization', () => { }, ]); }); + + it('should return an error if string and date histogram xAccessors (multiple layers) are used together', () => { + // current incompatibility is only for date and numeric histograms as xAccessors + const datasourceLayers = { + first: mockDatasource.publicAPIMock, + second: createMockDatasource('testDatasource').publicAPIMock, + }; + datasourceLayers.first.getOperationForColumnId = jest.fn((id: string) => + id === 'a' + ? (({ + dataType: 'date', + scale: 'interval', + } as unknown) as Operation) + : null + ); + datasourceLayers.second.getOperationForColumnId = jest.fn((id: string) => + id === 'e' + ? (({ + dataType: 'string', + scale: 'ordinal', + } as unknown) as Operation) + : null + ); + expect( + xyVisualization.getErrorMessages( + { + ...exampleState(), + layers: [ + { + layerId: 'first', + seriesType: 'area', + splitAccessor: 'd', + xAccessor: 'a', + accessors: ['b'], + }, + { + layerId: 'second', + seriesType: 'area', + splitAccessor: 'd', + xAccessor: 'e', + accessors: ['b'], + }, + ], + }, + datasourceLayers + ) + ).toEqual([ + { + shortMessage: 'Wrong data type for Horizontal axis.', + longMessage: 'Data type mismatch for the Horizontal axis, use a different function.', + }, + ]); + }); }); describe('#getWarningMessages', () => { diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx index ad2c9fd713985..bd20ed300bf61 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx @@ -542,8 +542,15 @@ function checkXAccessorCompatibility( datasourceLayers: Record ) { const errors = []; - const hasDateHistogramSet = state.layers.some(checkIntervalOperation('date', datasourceLayers)); - const hasNumberHistogram = state.layers.some(checkIntervalOperation('number', datasourceLayers)); + const hasDateHistogramSet = state.layers.some( + checkScaleOperation('interval', 'date', datasourceLayers) + ); + const hasNumberHistogram = state.layers.some( + checkScaleOperation('interval', 'number', datasourceLayers) + ); + const hasOrdinalAxis = state.layers.some( + checkScaleOperation('ordinal', undefined, datasourceLayers) + ); if (state.layers.length > 1 && hasDateHistogramSet && hasNumberHistogram) { errors.push({ shortMessage: i18n.translate('xpack.lens.xyVisualization.dataTypeFailureXShort', { @@ -560,11 +567,28 @@ function checkXAccessorCompatibility( }), }); } + if (state.layers.length > 1 && (hasDateHistogramSet || hasNumberHistogram) && hasOrdinalAxis) { + errors.push({ + shortMessage: i18n.translate('xpack.lens.xyVisualization.dataTypeFailureXShort', { + defaultMessage: `Wrong data type for {axis}.`, + values: { + axis: getAxisName('x', { isHorizontal: isHorizontalChart(state.layers) }), + }, + }), + longMessage: i18n.translate('xpack.lens.xyVisualization.dataTypeFailureXOrdinalLong', { + defaultMessage: `Data type mismatch for the {axis}, use a different function.`, + values: { + axis: getAxisName('x', { isHorizontal: isHorizontalChart(state.layers) }), + }, + }), + }); + } return errors; } -function checkIntervalOperation( - dataType: 'date' | 'number', +function checkScaleOperation( + scaleType: 'ordinal' | 'interval' | 'ratio', + dataType: 'date' | 'number' | 'string' | undefined, datasourceLayers: Record ) { return (layer: XYLayerConfig) => { @@ -573,6 +597,8 @@ function checkIntervalOperation( return false; } const operation = datasourceAPI?.getOperationForColumnId(layer.xAccessor); - return Boolean(operation?.dataType === dataType && operation.scale === 'interval'); + return Boolean( + operation && (!dataType || operation.dataType === dataType) && operation.scale === scaleType + ); }; } From b70b34f88417bf0efea5f8979a660bce2dc8dbcc Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 24 Jun 2021 16:31:18 +0300 Subject: [PATCH 23/69] [Cases] Fix push to external service error when connector's mapping does not exists (#102894) Co-authored-by: Jonathan Buttner Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../plugins/cases/server/client/cases/push.ts | 14 +++-- .../case_api_integration/common/lib/utils.ts | 27 +++++++++ .../tests/trial/cases/push_case.ts | 59 ++++++++++++++++++- 3 files changed, 94 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/cases/server/client/cases/push.ts b/x-pack/plugins/cases/server/client/cases/push.ts index 6b4f038871626..9e2066984a9da 100644 --- a/x-pack/plugins/cases/server/client/cases/push.ts +++ b/x-pack/plugins/cases/server/client/cases/push.ts @@ -110,20 +110,24 @@ export const push = async ( alertsInfo, }); - const connectorMappings = await casesClientInternal.configuration.getMappings({ + const getMappingsResponse = await casesClientInternal.configuration.getMappings({ connector: theCase.connector, }); - if (connectorMappings.length === 0) { - throw new Error('Connector mapping has not been created'); - } + const mappings = + getMappingsResponse.length === 0 + ? await casesClientInternal.configuration.createMappings({ + connector: theCase.connector, + owner: theCase.owner, + }) + : getMappingsResponse[0].attributes.mappings; const externalServiceIncident = await createIncident({ actionsClient, theCase, userActions, connector: connector as ActionConnector, - mappings: connectorMappings[0].attributes.mappings, + mappings, alerts, casesConnectors, }); diff --git a/x-pack/test/case_api_integration/common/lib/utils.ts b/x-pack/test/case_api_integration/common/lib/utils.ts index 921589b2341dd..6b59d9780a513 100644 --- a/x-pack/test/case_api_integration/common/lib/utils.ts +++ b/x-pack/test/case_api_integration/common/lib/utils.ts @@ -46,6 +46,7 @@ import { CasesConfigurationsResponse, CaseUserActionsResponse, AlertResponse, + ConnectorMappings, CasesByAlertId, } from '../../../../plugins/cases/common/api'; import { getPostCaseRequest, postCollectionReq, postCommentGenAlertReq } from './mock'; @@ -578,6 +579,32 @@ export const ensureSavedObjectIsAuthorized = ( entities.forEach((entity) => expect(owners.includes(entity.owner)).to.be(true)); }; +interface ConnectorMappingsSavedObject { + 'cases-connector-mappings': ConnectorMappings; +} + +/** + * Returns connector mappings saved objects from Elasticsearch directly. + */ +export const getConnectorMappingsFromES = async ({ es }: { es: KibanaClient }) => { + const mappings: ApiResponse< + estypes.SearchResponse + > = await es.search({ + index: '.kibana', + body: { + query: { + term: { + type: { + value: 'cases-connector-mappings', + }, + }, + }, + }, + }); + + return mappings; +}; + export const createCaseWithConnector = async ({ supertest, configureReq = {}, diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts index 8a58c59718feb..374053dd3b8b7 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts @@ -28,12 +28,19 @@ import { deleteAllCaseItems, superUserSpace1Auth, createCaseWithConnector, + createConnector, + getServiceNowConnector, + getConnectorMappingsFromES, } from '../../../../common/lib/utils'; import { ExternalServiceSimulator, getExternalServiceSimulatorPath, } from '../../../../../alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin'; -import { CaseStatuses, CaseUserActionResponse } from '../../../../../../plugins/cases/common/api'; +import { + CaseConnector, + CaseStatuses, + CaseUserActionResponse, +} from '../../../../../../plugins/cases/common/api'; import { globalRead, noKibanaPrivileges, @@ -95,6 +102,56 @@ export default ({ getService }: FtrProviderContext): void => { ).to.equal(true); }); + it('should create the mappings when pushing a case', async () => { + // create a connector but not a configuration so that the mapping will not be present + const connector = await createConnector({ + supertest, + req: { + ...getServiceNowConnector(), + config: { apiUrl: servicenowSimulatorURL }, + }, + }); + + actionsRemover.add('default', connector.id, 'action', 'actions'); + + const postedCase = await createCase( + supertest, + { + ...getPostCaseRequest(), + connector: { + id: connector.id, + name: connector.name, + type: connector.connector_type_id, + fields: { + urgency: '2', + impact: '2', + severity: '2', + category: 'software', + subcategory: 'os', + }, + } as CaseConnector, + }, + 200 + ); + + // there should be no mappings initially + let mappings = await getConnectorMappingsFromES({ es }); + expect(mappings.body.hits.hits.length).to.eql(0); + + await pushCase({ + supertest, + caseId: postedCase.id, + connectorId: connector.id, + }); + + // the mappings should now be created after the push + mappings = await getConnectorMappingsFromES({ es }); + expect(mappings.body.hits.hits.length).to.be(1); + expect( + mappings.body.hits.hits[0]._source?.['cases-connector-mappings'].mappings.length + ).to.be.above(0); + }); + it('pushes a comment appropriately', async () => { const { postedCase, connector } = await createCaseWithConnector({ supertest, From b8234729854b8d394ea1f6784a3535e6feb82c35 Mon Sep 17 00:00:00 2001 From: Marco Liberati Date: Thu, 24 Jun 2021 15:32:12 +0200 Subject: [PATCH 24/69] [Lens] Add continuity icons to palette configuration (#103240) --- .../coloring/palette_configuration.tsx | 57 ++++++++++++------- 1 file changed, 36 insertions(+), 21 deletions(-) diff --git a/x-pack/plugins/lens/public/shared_components/coloring/palette_configuration.tsx b/x-pack/plugins/lens/public/shared_components/coloring/palette_configuration.tsx index f71bda986a8bb..ae7204d9f93e7 100644 --- a/x-pack/plugins/lens/public/shared_components/coloring/palette_configuration.tsx +++ b/x-pack/plugins/lens/public/shared_components/coloring/palette_configuration.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React from 'react'; +import React, { FC } from 'react'; import { PaletteOutput, PaletteRegistry } from 'src/plugins/charts/public'; import { EuiFormRow, @@ -39,6 +39,17 @@ import { } from './utils'; const idPrefix = htmlIdGenerator()(); +const ContinuityOption: FC<{ iconType: string }> = ({ children, iconType }) => { + return ( + + + + + {children} + + ); +}; + /** * Some name conventions here: * * `displayStops` => It's an additional transformation of `stops` into a [0, N] domain for the EUIPaletteDisplay component. @@ -141,41 +152,45 @@ export function CustomizablePalette({ options={[ { value: 'above', - inputDisplay: i18n.translate( - 'xpack.lens.table.dynamicColoring.continuity.aboveLabel', - { - defaultMessage: 'Above range', - } + inputDisplay: ( + + {i18n.translate('xpack.lens.table.dynamicColoring.continuity.aboveLabel', { + defaultMessage: 'Above range', + })} + ), 'data-test-subj': 'continuity-above', }, { value: 'below', - inputDisplay: i18n.translate( - 'xpack.lens.table.dynamicColoring.continuity.belowLabel', - { - defaultMessage: 'Below range', - } + inputDisplay: ( + + {i18n.translate('xpack.lens.table.dynamicColoring.continuity.belowLabel', { + defaultMessage: 'Below range', + })} + ), 'data-test-subj': 'continuity-below', }, { value: 'all', - inputDisplay: i18n.translate( - 'xpack.lens.table.dynamicColoring.continuity.allLabel', - { - defaultMessage: 'Above and below range', - } + inputDisplay: ( + + {i18n.translate('xpack.lens.table.dynamicColoring.continuity.allLabel', { + defaultMessage: 'Above and below range', + })} + ), 'data-test-subj': 'continuity-all', }, { value: 'none', - inputDisplay: i18n.translate( - 'xpack.lens.table.dynamicColoring.continuity.noneLabel', - { - defaultMessage: 'Within range', - } + inputDisplay: ( + + {i18n.translate('xpack.lens.table.dynamicColoring.continuity.noneLabel', { + defaultMessage: 'Within range', + })} + ), 'data-test-subj': 'continuity-none', }, From fdd878410ececb575bebf0575355908ed614059f Mon Sep 17 00:00:00 2001 From: Tim Roes Date: Thu, 24 Jun 2021 15:34:57 +0200 Subject: [PATCH 25/69] Add missing i18n (#103245) --- .../components/sidebar/discover_field_search.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_search.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_search.tsx index e11c1716efe6b..4abfa6ecea55a 100644 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_search.tsx +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_search.tsx @@ -204,15 +204,21 @@ export function DiscoverFieldSearch({ onChange, value, types, useNewFieldsApi }: return [ { id: `${id}-any`, - label: 'any', + label: i18n.translate('discover.fieldChooser.filter.toggleButton.any', { + defaultMessage: 'any', + }), }, { id: `${id}-true`, - label: 'yes', + label: i18n.translate('discover.fieldChooser.filter.toggleButton.yes', { + defaultMessage: 'yes', + }), }, { id: `${id}-false`, - label: 'no', + label: i18n.translate('discover.fieldChooser.filter.toggleButton.no', { + defaultMessage: 'no', + }), }, ]; }; From cc6a64514d9bad6c0ff5dbb9e4c0231939eedde4 Mon Sep 17 00:00:00 2001 From: Patrick Mueller Date: Thu, 24 Jun 2021 10:06:01 -0400 Subject: [PATCH 26/69] [alerting][actions] add task scheduled date and delay to event log - 2 (#103172) resolves #98634 This adds a new object property to the event log kibana object named task, with two properties to track the time the task was scheduled to run, and the delay between when it was supposed to run and when it actually started. This task property is only added to the appropriate events. task: schema.maybe( schema.object({ scheduled: ecsDate(), schedule_delay: ecsNumber(), }) ), Note that these changes were previously merged to master in https://github.com/elastic/kibana/pull/102252 which had to be reverted - this PR contains the same commits, plus some additional ones to resolve the tests that were broken during the bad merge. --- .../server/lib/action_executor.test.ts | 90 ++ .../actions/server/lib/action_executor.ts | 19 + .../server/lib/task_runner_factory.test.ts | 16 +- .../actions/server/lib/task_runner_factory.ts | 5 + .../create_execution_handler.test.ts | 1 - .../task_runner/create_execution_handler.ts | 1 - .../server/task_runner/task_runner.test.ts | 187 ++-- .../server/task_runner/task_runner.ts | 17 +- x-pack/plugins/event_log/README.md | 4 + .../plugins/event_log/generated/mappings.json | 14 +- x-pack/plugins/event_log/generated/schemas.ts | 7 +- x-pack/plugins/event_log/scripts/mappings.js | 11 + .../plugins/event_log/server/event_logger.ts | 2 +- .../tests/alerting/alerts.ts | 1 - .../tests/alerting/event_log.ts | 2 +- .../spaces_only/tests/actions/execute.ts | 2 + .../spaces_only/tests/alerting/event_log.ts | 918 +++++++++--------- 17 files changed, 791 insertions(+), 506 deletions(-) diff --git a/x-pack/plugins/actions/server/lib/action_executor.test.ts b/x-pack/plugins/actions/server/lib/action_executor.test.ts index 37d461d6b2a50..440de161490aa 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.test.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.test.ts @@ -109,6 +109,96 @@ test('successfully executes', async () => { }); expect(loggerMock.debug).toBeCalledWith('executing action test:1: 1'); + expect(eventLogger.logEvent.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "event": Object { + "action": "execute-start", + }, + "kibana": Object { + "saved_objects": Array [ + Object { + "id": "1", + "namespace": "some-namespace", + "rel": "primary", + "type": "action", + "type_id": "test", + }, + ], + }, + "message": "action started: test:1: 1", + }, + ], + Array [ + Object { + "event": Object { + "action": "execute", + "outcome": "success", + }, + "kibana": Object { + "saved_objects": Array [ + Object { + "id": "1", + "namespace": "some-namespace", + "rel": "primary", + "type": "action", + "type_id": "test", + }, + ], + }, + "message": "action executed: test:1: 1", + }, + ], + ] + `); +}); + +test('successfully executes as a task', async () => { + const actionType: jest.Mocked = { + id: 'test', + name: 'Test', + minimumLicenseRequired: 'basic', + executor: jest.fn(), + }; + const actionSavedObject = { + id: '1', + type: 'action', + attributes: { + actionTypeId: 'test', + config: { + bar: true, + }, + secrets: { + baz: true, + }, + }, + references: [], + }; + const actionResult = { + id: actionSavedObject.id, + name: actionSavedObject.id, + ...pick(actionSavedObject.attributes, 'actionTypeId', 'config'), + isPreconfigured: false, + }; + actionsClient.get.mockResolvedValueOnce(actionResult); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(actionSavedObject); + actionTypeRegistry.get.mockReturnValueOnce(actionType); + + const scheduleDelay = 10000; // milliseconds + const scheduled = new Date(Date.now() - scheduleDelay); + await actionExecutor.execute({ + ...executeParams, + taskInfo: { + scheduled, + }, + }); + + const eventTask = eventLogger.logEvent.mock.calls[0][0]?.kibana?.task; + expect(eventTask).toBeDefined(); + expect(eventTask?.scheduled).toBe(scheduled.toISOString()); + expect(eventTask?.schedule_delay).toBeGreaterThanOrEqual(scheduleDelay * 1000 * 1000); + expect(eventTask?.schedule_delay).toBeLessThanOrEqual(2 * scheduleDelay * 1000 * 1000); }); test('provides empty config when config and / or secrets is empty', async () => { diff --git a/x-pack/plugins/actions/server/lib/action_executor.ts b/x-pack/plugins/actions/server/lib/action_executor.ts index e9e7b17288611..9e62b123951df 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.ts @@ -25,6 +25,9 @@ import { ActionsClient } from '../actions_client'; import { ActionExecutionSource } from './action_execution_source'; import { RelatedSavedObjects } from './related_saved_objects'; +// 1,000,000 nanoseconds in 1 millisecond +const Millis2Nanos = 1000 * 1000; + export interface ActionExecutorContext { logger: Logger; spaces?: SpacesServiceStart; @@ -39,11 +42,16 @@ export interface ActionExecutorContext { preconfiguredActions: PreConfiguredAction[]; } +export interface TaskInfo { + scheduled: Date; +} + export interface ExecuteOptions { actionId: string; request: KibanaRequest; params: Record; source?: ActionExecutionSource; + taskInfo?: TaskInfo; relatedSavedObjects?: RelatedSavedObjects; } @@ -71,6 +79,7 @@ export class ActionExecutor { params, request, source, + taskInfo, relatedSavedObjects, }: ExecuteOptions): Promise> { if (!this.isInitialized) { @@ -143,9 +152,19 @@ export class ActionExecutor { const actionLabel = `${actionTypeId}:${actionId}: ${name}`; logger.debug(`executing action ${actionLabel}`); + const task = taskInfo + ? { + task: { + scheduled: taskInfo.scheduled.toISOString(), + schedule_delay: Millis2Nanos * (Date.now() - taskInfo.scheduled.getTime()), + }, + } + : {}; + const event: IEvent = { event: { action: EVENT_LOG_ACTIONS.execute }, kibana: { + ...task, saved_objects: [ { rel: SAVED_OBJECT_REL_PRIMARY, diff --git a/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts b/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts index 2292994e3ccfd..495d638951b56 100644 --- a/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts +++ b/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts @@ -133,6 +133,9 @@ test('executes the task by calling the executor with proper parameters', async ( authorization: 'ApiKey MTIzOmFiYw==', }, }), + taskInfo: { + scheduled: new Date(), + }, }); const [executeParams] = mockedActionExecutor.execute.mock.calls[0]; @@ -255,6 +258,9 @@ test('uses API key when provided', async () => { authorization: 'ApiKey MTIzOmFiYw==', }, }), + taskInfo: { + scheduled: new Date(), + }, }); const [executeParams] = mockedActionExecutor.execute.mock.calls[0]; @@ -300,6 +306,9 @@ test('uses relatedSavedObjects when provided', async () => { authorization: 'ApiKey MTIzOmFiYw==', }, }), + taskInfo: { + scheduled: new Date(), + }, }); }); @@ -323,7 +332,6 @@ test('sanitizes invalid relatedSavedObjects when provided', async () => { }); await taskRunner.run(); - expect(mockedActionExecutor.execute).toHaveBeenCalledWith({ actionId: '2', params: { baz: true }, @@ -334,6 +342,9 @@ test('sanitizes invalid relatedSavedObjects when provided', async () => { authorization: 'ApiKey MTIzOmFiYw==', }, }), + taskInfo: { + scheduled: new Date(), + }, }); }); @@ -363,6 +374,9 @@ test(`doesn't use API key when not provided`, async () => { request: expect.objectContaining({ headers: {}, }), + taskInfo: { + scheduled: new Date(), + }, }); const [executeParams] = mockedActionExecutor.execute.mock.calls[0]; diff --git a/x-pack/plugins/actions/server/lib/task_runner_factory.ts b/x-pack/plugins/actions/server/lib/task_runner_factory.ts index 0515963ab82f4..64169de728f75 100644 --- a/x-pack/plugins/actions/server/lib/task_runner_factory.ts +++ b/x-pack/plugins/actions/server/lib/task_runner_factory.ts @@ -72,6 +72,10 @@ export class TaskRunnerFactory { getUnsecuredSavedObjectsClient, } = this.taskRunnerContext!; + const taskInfo = { + scheduled: taskInstance.runAt, + }; + return { async run() { const { spaceId, actionTaskParamsId } = taskInstance.params as Record; @@ -118,6 +122,7 @@ export class TaskRunnerFactory { actionId, request: fakeRequest, ...getSourceFromReferences(references), + taskInfo, relatedSavedObjects: validatedRelatedSavedObjects(logger, relatedSavedObjects), }); } catch (e) { diff --git a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts index 033ffcceb6a0a..1dcd19119b6fd 100644 --- a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts @@ -195,7 +195,6 @@ test('enqueues execution per selected action', async () => { "id": "1", "license": "basic", "name": "name-of-alert", - "namespace": "test1", "ruleset": "alerts", }, }, diff --git a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts index 968fff540dc03..3004ed599128e 100644 --- a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts +++ b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts @@ -209,7 +209,6 @@ export function createExecutionHandler< license: alertType.minimumLicenseRequired, category: alertType.id, ruleset: alertType.producer, - ...namespace, name: alertName, }, }; diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts index 8ab267a5610d3..88d1b1b24a4ec 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts @@ -282,13 +282,16 @@ describe('Task Runner', () => { "type_id": "test", }, ], + "task": Object { + "schedule_delay": 0, + "scheduled": "1970-01-01T00:00:00.000Z", + }, }, "message": "alert execution start: \\"1\\"", "rule": Object { "category": "test", "id": "1", "license": "basic", - "namespace": undefined, "ruleset": "alerts", }, } @@ -394,6 +397,10 @@ describe('Task Runner', () => { kind: 'alert', }, kibana: { + task: { + schedule_delay: 0, + scheduled: '1970-01-01T00:00:00.000Z', + }, saved_objects: [ { id: '1', @@ -409,7 +416,6 @@ describe('Task Runner', () => { category: 'test', id: '1', license: 'basic', - namespace: undefined, ruleset: 'alerts', }, }); @@ -518,6 +524,10 @@ describe('Task Runner', () => { alerting: { status: 'active', }, + task: { + schedule_delay: 0, + scheduled: '1970-01-01T00:00:00.000Z', + }, saved_objects: [ { id: '1', @@ -534,7 +544,6 @@ describe('Task Runner', () => { id: '1', license: 'basic', name: 'alert-name', - namespace: undefined, ruleset: 'alerts', }, }); @@ -603,6 +612,10 @@ describe('Task Runner', () => { kind: 'alert', }, kibana: { + task: { + schedule_delay: 0, + scheduled: '1970-01-01T00:00:00.000Z', + }, saved_objects: [ { id: '1', @@ -618,7 +631,6 @@ describe('Task Runner', () => { category: 'test', id: '1', license: 'basic', - namespace: undefined, ruleset: 'alerts', }, }); @@ -700,6 +712,10 @@ describe('Task Runner', () => { alerting: { status: 'active', }, + task: { + schedule_delay: 0, + scheduled: '1970-01-01T00:00:00.000Z', + }, saved_objects: [ { id: '1', @@ -716,7 +732,6 @@ describe('Task Runner', () => { id: '1', license: 'basic', name: 'alert-name', - namespace: undefined, ruleset: 'alerts', }, }); @@ -854,13 +869,16 @@ describe('Task Runner', () => { "type_id": "test", }, ], + "task": Object { + "schedule_delay": 0, + "scheduled": "1970-01-01T00:00:00.000Z", + }, }, "message": "alert execution start: \\"1\\"", "rule": Object { "category": "test", "id": "1", "license": "basic", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -897,7 +915,6 @@ describe('Task Runner', () => { "id": "1", "license": "basic", "name": "alert-name", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -926,6 +943,10 @@ describe('Task Runner', () => { "type_id": "test", }, ], + "task": Object { + "schedule_delay": 0, + "scheduled": "1970-01-01T00:00:00.000Z", + }, }, "message": "alert executed: test:1: 'alert-name'", "rule": Object { @@ -933,7 +954,6 @@ describe('Task Runner', () => { "id": "1", "license": "basic", "name": "alert-name", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -1151,13 +1171,16 @@ describe('Task Runner', () => { "type_id": "test", }, ], + "task": Object { + "schedule_delay": 0, + "scheduled": "1970-01-01T00:00:00.000Z", + }, }, "message": "alert execution start: \\"1\\"", "rule": Object { "category": "test", "id": "1", "license": "basic", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -1194,7 +1217,6 @@ describe('Task Runner', () => { "id": "1", "license": "basic", "name": "alert-name", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -1231,7 +1253,6 @@ describe('Task Runner', () => { "id": "1", "license": "basic", "name": "alert-name", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -1273,7 +1294,6 @@ describe('Task Runner', () => { "id": "1", "license": "basic", "name": "alert-name", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -1302,6 +1322,10 @@ describe('Task Runner', () => { "type_id": "test", }, ], + "task": Object { + "schedule_delay": 0, + "scheduled": "1970-01-01T00:00:00.000Z", + }, }, "message": "alert executed: test:1: 'alert-name'", "rule": Object { @@ -1309,7 +1333,6 @@ describe('Task Runner', () => { "id": "1", "license": "basic", "name": "alert-name", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -1433,13 +1456,16 @@ describe('Task Runner', () => { "type_id": "test", }, ], + "task": Object { + "schedule_delay": 0, + "scheduled": "1970-01-01T00:00:00.000Z", + }, }, "message": "alert execution start: \\"1\\"", "rule": Object { "category": "test", "id": "1", "license": "basic", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -1476,7 +1502,6 @@ describe('Task Runner', () => { "id": "1", "license": "basic", "name": "alert-name", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -1513,7 +1538,6 @@ describe('Task Runner', () => { "id": "1", "license": "basic", "name": "alert-name", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -1555,7 +1579,6 @@ describe('Task Runner', () => { "id": "1", "license": "basic", "name": "alert-name", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -1597,7 +1620,6 @@ describe('Task Runner', () => { "id": "1", "license": "basic", "name": "alert-name", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -1626,6 +1648,10 @@ describe('Task Runner', () => { "type_id": "test", }, ], + "task": Object { + "schedule_delay": 0, + "scheduled": "1970-01-01T00:00:00.000Z", + }, }, "message": "alert executed: test:1: 'alert-name'", "rule": Object { @@ -1633,7 +1659,6 @@ describe('Task Runner', () => { "id": "1", "license": "basic", "name": "alert-name", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -1968,13 +1993,16 @@ describe('Task Runner', () => { "type_id": "test", }, ], + "task": Object { + "schedule_delay": 0, + "scheduled": "1970-01-01T00:00:00.000Z", + }, }, "message": "alert execution start: \\"1\\"", "rule": Object { "category": "test", "id": "1", "license": "basic", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -2012,7 +2040,6 @@ describe('Task Runner', () => { "id": "1", "license": "basic", "name": "alert-name", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -2049,7 +2076,6 @@ describe('Task Runner', () => { "id": "1", "license": "basic", "name": "alert-name", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -2078,6 +2104,10 @@ describe('Task Runner', () => { "type_id": "test", }, ], + "task": Object { + "schedule_delay": 0, + "scheduled": "1970-01-01T00:00:00.000Z", + }, }, "message": "alert executed: test:1: 'alert-name'", "rule": Object { @@ -2085,7 +2115,6 @@ describe('Task Runner', () => { "id": "1", "license": "basic", "name": "alert-name", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -2294,13 +2323,16 @@ describe('Task Runner', () => { "type_id": "test", }, ], + "task": Object { + "schedule_delay": 0, + "scheduled": "1970-01-01T00:00:00.000Z", + }, }, "message": "alert execution start: \\"1\\"", "rule": Object { "category": "test", "id": "1", "license": "basic", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -2333,13 +2365,16 @@ describe('Task Runner', () => { "type_id": "test", }, ], + "task": Object { + "schedule_delay": 0, + "scheduled": "1970-01-01T00:00:00.000Z", + }, }, "message": "alert execution failure: test:1: 'alert-name'", "rule": Object { "category": "test", "id": "1", "license": "basic", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -2397,13 +2432,16 @@ describe('Task Runner', () => { "type_id": "test", }, ], + "task": Object { + "schedule_delay": 0, + "scheduled": "1970-01-01T00:00:00.000Z", + }, }, "message": "alert execution start: \\"1\\"", "rule": Object { "category": "test", "id": "1", "license": "basic", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -2436,13 +2474,16 @@ describe('Task Runner', () => { "type_id": "test", }, ], + "task": Object { + "schedule_delay": 0, + "scheduled": "1970-01-01T00:00:00.000Z", + }, }, "message": "test:1: execution failed", "rule": Object { "category": "test", "id": "1", "license": "basic", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -2508,13 +2549,16 @@ describe('Task Runner', () => { "type_id": "test", }, ], + "task": Object { + "schedule_delay": 0, + "scheduled": "1970-01-01T00:00:00.000Z", + }, }, "message": "alert execution start: \\"1\\"", "rule": Object { "category": "test", "id": "1", "license": "basic", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -2547,13 +2591,16 @@ describe('Task Runner', () => { "type_id": "test", }, ], + "task": Object { + "schedule_delay": 0, + "scheduled": "1970-01-01T00:00:00.000Z", + }, }, "message": "test:1: execution failed", "rule": Object { "category": "test", "id": "1", "license": "basic", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -2619,13 +2666,16 @@ describe('Task Runner', () => { "type_id": "test", }, ], + "task": Object { + "schedule_delay": 0, + "scheduled": "1970-01-01T00:00:00.000Z", + }, }, "message": "alert execution start: \\"1\\"", "rule": Object { "category": "test", "id": "1", "license": "basic", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -2658,13 +2708,16 @@ describe('Task Runner', () => { "type_id": "test", }, ], + "task": Object { + "schedule_delay": 0, + "scheduled": "1970-01-01T00:00:00.000Z", + }, }, "message": "test:1: execution failed", "rule": Object { "category": "test", "id": "1", "license": "basic", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -2729,13 +2782,16 @@ describe('Task Runner', () => { "type_id": "test", }, ], + "task": Object { + "schedule_delay": 0, + "scheduled": "1970-01-01T00:00:00.000Z", + }, }, "message": "alert execution start: \\"1\\"", "rule": Object { "category": "test", "id": "1", "license": "basic", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -2768,13 +2824,16 @@ describe('Task Runner', () => { "type_id": "test", }, ], + "task": Object { + "schedule_delay": 0, + "scheduled": "1970-01-01T00:00:00.000Z", + }, }, "message": "test:1: execution failed", "rule": Object { "category": "test", "id": "1", "license": "basic", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -3007,13 +3066,16 @@ describe('Task Runner', () => { "type_id": "test", }, ], + "task": Object { + "schedule_delay": 0, + "scheduled": "1970-01-01T00:00:00.000Z", + }, }, "message": "alert execution start: \\"1\\"", "rule": Object { "category": "test", "id": "1", "license": "basic", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -3050,7 +3112,6 @@ describe('Task Runner', () => { "id": "1", "license": "basic", "name": "alert-name", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -3087,7 +3148,6 @@ describe('Task Runner', () => { "id": "1", "license": "basic", "name": "alert-name", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -3124,7 +3184,6 @@ describe('Task Runner', () => { "id": "1", "license": "basic", "name": "alert-name", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -3161,7 +3220,6 @@ describe('Task Runner', () => { "id": "1", "license": "basic", "name": "alert-name", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -3190,6 +3248,10 @@ describe('Task Runner', () => { "type_id": "test", }, ], + "task": Object { + "schedule_delay": 0, + "scheduled": "1970-01-01T00:00:00.000Z", + }, }, "message": "alert executed: test:1: 'alert-name'", "rule": Object { @@ -3197,7 +3259,6 @@ describe('Task Runner', () => { "id": "1", "license": "basic", "name": "alert-name", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -3291,13 +3352,16 @@ describe('Task Runner', () => { "type_id": "test", }, ], + "task": Object { + "schedule_delay": 0, + "scheduled": "1970-01-01T00:00:00.000Z", + }, }, "message": "alert execution start: \\"1\\"", "rule": Object { "category": "test", "id": "1", "license": "basic", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -3334,7 +3398,6 @@ describe('Task Runner', () => { "id": "1", "license": "basic", "name": "alert-name", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -3371,7 +3434,6 @@ describe('Task Runner', () => { "id": "1", "license": "basic", "name": "alert-name", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -3400,6 +3462,10 @@ describe('Task Runner', () => { "type_id": "test", }, ], + "task": Object { + "schedule_delay": 0, + "scheduled": "1970-01-01T00:00:00.000Z", + }, }, "message": "alert executed: test:1: 'alert-name'", "rule": Object { @@ -3407,7 +3473,6 @@ describe('Task Runner', () => { "id": "1", "license": "basic", "name": "alert-name", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -3493,13 +3558,16 @@ describe('Task Runner', () => { "type_id": "test", }, ], + "task": Object { + "schedule_delay": 0, + "scheduled": "1970-01-01T00:00:00.000Z", + }, }, "message": "alert execution start: \\"1\\"", "rule": Object { "category": "test", "id": "1", "license": "basic", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -3534,7 +3602,6 @@ describe('Task Runner', () => { "id": "1", "license": "basic", "name": "alert-name", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -3569,7 +3636,6 @@ describe('Task Runner', () => { "id": "1", "license": "basic", "name": "alert-name", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -3598,6 +3664,10 @@ describe('Task Runner', () => { "type_id": "test", }, ], + "task": Object { + "schedule_delay": 0, + "scheduled": "1970-01-01T00:00:00.000Z", + }, }, "message": "alert executed: test:1: 'alert-name'", "rule": Object { @@ -3605,7 +3675,6 @@ describe('Task Runner', () => { "id": "1", "license": "basic", "name": "alert-name", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -3686,13 +3755,16 @@ describe('Task Runner', () => { "type_id": "test", }, ], + "task": Object { + "schedule_delay": 0, + "scheduled": "1970-01-01T00:00:00.000Z", + }, }, "message": "alert execution start: \\"1\\"", "rule": Object { "category": "test", "id": "1", "license": "basic", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -3729,7 +3801,6 @@ describe('Task Runner', () => { "id": "1", "license": "basic", "name": "alert-name", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -3766,7 +3837,6 @@ describe('Task Runner', () => { "id": "1", "license": "basic", "name": "alert-name", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -3795,6 +3865,10 @@ describe('Task Runner', () => { "type_id": "test", }, ], + "task": Object { + "schedule_delay": 0, + "scheduled": "1970-01-01T00:00:00.000Z", + }, }, "message": "alert executed: test:1: 'alert-name'", "rule": Object { @@ -3802,7 +3876,6 @@ describe('Task Runner', () => { "id": "1", "license": "basic", "name": "alert-name", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -3885,13 +3958,16 @@ describe('Task Runner', () => { "type_id": "test", }, ], + "task": Object { + "schedule_delay": 0, + "scheduled": "1970-01-01T00:00:00.000Z", + }, }, "message": "alert execution start: \\"1\\"", "rule": Object { "category": "test", "id": "1", "license": "basic", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -3925,7 +4001,6 @@ describe('Task Runner', () => { "id": "1", "license": "basic", "name": "alert-name", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -3959,7 +4034,6 @@ describe('Task Runner', () => { "id": "1", "license": "basic", "name": "alert-name", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -3988,6 +4062,10 @@ describe('Task Runner', () => { "type_id": "test", }, ], + "task": Object { + "schedule_delay": 0, + "scheduled": "1970-01-01T00:00:00.000Z", + }, }, "message": "alert executed: test:1: 'alert-name'", "rule": Object { @@ -3995,7 +4073,6 @@ describe('Task Runner', () => { "id": "1", "license": "basic", "name": "alert-name", - "namespace": undefined, "ruleset": "alerts", }, }, diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.ts index b712b6237c8a7..c66c054bc8ac3 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.ts @@ -54,6 +54,9 @@ import { getEsErrorMessage } from '../lib/errors'; const FALLBACK_RETRY_INTERVAL = '5m'; +// 1,000,000 nanoseconds in 1 millisecond +const Millis2Nanos = 1000 * 1000; + type Event = Exclude; interface AlertTaskRunResult { @@ -489,15 +492,17 @@ export class TaskRunner< schedule: taskSchedule, } = this.taskInstance; - const runDate = new Date().toISOString(); - this.logger.debug(`executing alert ${this.alertType.id}:${alertId} at ${runDate}`); + const runDate = new Date(); + const runDateString = runDate.toISOString(); + this.logger.debug(`executing alert ${this.alertType.id}:${alertId} at ${runDateString}`); const namespace = this.context.spaceIdToNamespace(spaceId); const eventLogger = this.context.eventLogger; + const scheduleDelay = runDate.getTime() - this.taskInstance.runAt.getTime(); const event: IEvent = { // explicitly set execute timestamp so it will be before other events // generated here (new-instance, schedule-action, etc) - '@timestamp': runDate, + '@timestamp': runDateString, event: { action: EVENT_LOG_ACTIONS.execute, kind: 'alert', @@ -513,13 +518,16 @@ export class TaskRunner< namespace, }, ], + task: { + scheduled: this.taskInstance.runAt.toISOString(), + schedule_delay: Millis2Nanos * scheduleDelay, + }, }, rule: { id: alertId, license: this.alertType.minimumLicenseRequired, category: this.alertType.id, ruleset: this.alertType.producer, - namespace, }, }; @@ -814,7 +822,6 @@ function generateNewAndRecoveredInstanceEvents< license: ruleType.minimumLicenseRequired, category: ruleType.id, ruleset: ruleType.producer, - namespace, name: rule.name, }, }; diff --git a/x-pack/plugins/event_log/README.md b/x-pack/plugins/event_log/README.md index ffbd20dd6f2be..682bf2660c78b 100644 --- a/x-pack/plugins/event_log/README.md +++ b/x-pack/plugins/event_log/README.md @@ -127,6 +127,10 @@ Below is a document in the expected structure, with descriptions of the fields: // Custom fields that are not part of ECS. kibana: { server_uuid: "UUID of kibana server, for diagnosing multi-Kibana scenarios", + task: { + scheduled: "ISO date of when the task for this event was supposed to start", + schedule_delay: "delay in nanoseconds between when this task was supposed to start and when it actually started", + }, alerting: { instance_id: "alert instance id, for relevant documents", action_group_id: "alert action group, for relevant documents", diff --git a/x-pack/plugins/event_log/generated/mappings.json b/x-pack/plugins/event_log/generated/mappings.json index 3eadcc21257b0..0f5f4af2052ee 100644 --- a/x-pack/plugins/event_log/generated/mappings.json +++ b/x-pack/plugins/event_log/generated/mappings.json @@ -214,10 +214,6 @@ "version": { "ignore_above": 1024, "type": "keyword" - }, - "namespace": { - "ignore_above": 1024, - "type": "keyword" } } }, @@ -241,6 +237,16 @@ "type": "keyword", "ignore_above": 1024 }, + "task": { + "properties": { + "scheduled": { + "type": "date" + }, + "schedule_delay": { + "type": "long" + } + } + }, "alerting": { "properties": { "instance_id": { diff --git a/x-pack/plugins/event_log/generated/schemas.ts b/x-pack/plugins/event_log/generated/schemas.ts index 2a066ca0bd15b..556ddec5a7001 100644 --- a/x-pack/plugins/event_log/generated/schemas.ts +++ b/x-pack/plugins/event_log/generated/schemas.ts @@ -91,7 +91,6 @@ export const EventSchema = schema.maybe( ruleset: ecsString(), uuid: ecsString(), version: ecsString(), - namespace: ecsString(), }) ), user: schema.maybe( @@ -102,6 +101,12 @@ export const EventSchema = schema.maybe( kibana: schema.maybe( schema.object({ server_uuid: ecsString(), + task: schema.maybe( + schema.object({ + scheduled: ecsDate(), + schedule_delay: ecsNumber(), + }) + ), alerting: schema.maybe( schema.object({ instance_id: ecsString(), diff --git a/x-pack/plugins/event_log/scripts/mappings.js b/x-pack/plugins/event_log/scripts/mappings.js index f2020e76b46ba..93fe053bd0cdf 100644 --- a/x-pack/plugins/event_log/scripts/mappings.js +++ b/x-pack/plugins/event_log/scripts/mappings.js @@ -17,6 +17,17 @@ exports.EcsCustomPropertyMappings = { type: 'keyword', ignore_above: 1024, }, + // task specific fields + task: { + properties: { + scheduled: { + type: 'date', + }, + schedule_delay: { + type: 'long', + }, + }, + }, // alerting specific fields alerting: { properties: { diff --git a/x-pack/plugins/event_log/server/event_logger.ts b/x-pack/plugins/event_log/server/event_logger.ts index 4af69de0f47a0..b985a173ccdbf 100644 --- a/x-pack/plugins/event_log/server/event_logger.ts +++ b/x-pack/plugins/event_log/server/event_logger.ts @@ -88,7 +88,7 @@ export class EventLogger implements IEventLogger { try { validatedEvent = validateEvent(this.eventLogService, event); } catch (err) { - this.systemLogger.warn(`invalid event logged: ${err.message}`); + this.systemLogger.warn(`invalid event logged: ${err.message}; ${JSON.stringify(event)})`); return; } 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 e9ed14fbcddcd..b3d83ae22f330 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 @@ -1304,7 +1304,6 @@ instanceStateValue: true license: 'basic', category: ruleObject.alertInfo.ruleTypeId, ruleset: ruleObject.alertInfo.producer, - namespace: spaceId, name: ruleObject.alertInfo.name, }); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/event_log.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/event_log.ts index 5d13d641367a4..940203a9b1f8c 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/event_log.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/event_log.ts @@ -81,12 +81,12 @@ export default function eventLogTests({ getService }: FtrProviderContext) { errorMessage: 'Unable to decrypt attribute "apiKey"', status: 'error', reason: 'decrypt', + shouldHaveTask: true, rule: { id: alertId, category: response.body.rule_type_id, license: 'basic', ruleset: 'alertsFixture', - namespace: spaceId, }, }); }); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/execute.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/execute.ts index d494c99c80e8f..38f3a17f317c2 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/execute.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/execute.ts @@ -406,6 +406,8 @@ export default function ({ getService }: FtrProviderContext) { expect(startExecuteEvent?.message).to.eql(startMessage); } + expect(executeEvent?.kibana?.task).to.eql(undefined); + if (errorMessage) { expect(executeEvent?.error?.message).to.eql(errorMessage); } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts index fae5958d7827a..9bf7baf95d8d2 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts @@ -24,476 +24,512 @@ export default function eventLogTests({ getService }: FtrProviderContext) { after(() => objectRemover.removeAll()); - it('should generate expected events for normal operation', async () => { - const { body: createdAction } = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`) - .set('kbn-xsrf', 'foo') - .send({ - name: 'MY action', - connector_type_id: 'test.noop', - config: {}, - secrets: {}, - }) - .expect(200); - - // pattern of when the alert should fire - const pattern = { - instance: [false, true, true], - }; - - const response = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) - .set('kbn-xsrf', 'foo') - .send( - getTestAlertData({ - rule_type_id: 'test.patternFiring', - schedule: { interval: '1s' }, - throttle: null, - params: { - pattern, - }, - actions: [ - { - id: createdAction.id, - group: 'default', - params: {}, - }, - ], - }) - ); - - expect(response.status).to.eql(200); - const alertId = response.body.id; - objectRemover.add(Spaces.space1.id, alertId, 'rule', 'alerting'); - - // get the events we're expecting - const events = await retry.try(async () => { - return await getEventLog({ - getService, - spaceId: Spaces.space1.id, - type: 'alert', - id: alertId, - provider: 'alerting', - actions: new Map([ - // make sure the counts of the # of events per type are as expected - ['execute-start', { gte: 4 }], - ['execute', { gte: 4 }], - ['execute-action', { equal: 2 }], - ['new-instance', { equal: 1 }], - ['active-instance', { gte: 1 }], - ['recovered-instance', { equal: 1 }], - ]), - }); - }); - - // get the filtered events only with action 'new-instance' - const filteredEvents = await retry.try(async () => { - return await getEventLog({ - getService, - spaceId: Spaces.space1.id, - type: 'alert', - id: alertId, - provider: 'alerting', - actions: new Map([['new-instance', { equal: 1 }]]), - filter: 'event.action:(new-instance)', - }); - }); + for (const space of [Spaces.default, Spaces.space1]) { + describe(`in space ${space.id}`, () => { + it('should generate expected events for normal operation', async () => { + const { body: createdAction } = await supertest + .post(`${getUrlPrefix(space.id)}/api/actions/connector`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'MY action', + connector_type_id: 'test.noop', + config: {}, + secrets: {}, + }) + .expect(200); + + // pattern of when the alert should fire + const pattern = { + instance: [false, true, true], + }; + + const response = await supertest + .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send( + getTestAlertData({ + rule_type_id: 'test.patternFiring', + schedule: { interval: '1s' }, + throttle: null, + params: { + pattern, + }, + actions: [ + { + id: createdAction.id, + group: 'default', + params: {}, + }, + ], + }) + ); - expect(getEventsByAction(filteredEvents, 'execute').length).equal(0); - expect(getEventsByAction(filteredEvents, 'execute-action').length).equal(0); - expect(getEventsByAction(events, 'new-instance').length).equal(1); - - const executeEvents = getEventsByAction(events, 'execute'); - const executeStartEvents = getEventsByAction(events, 'execute-start'); - const executeActionEvents = getEventsByAction(events, 'execute-action'); - const newInstanceEvents = getEventsByAction(events, 'new-instance'); - const recoveredInstanceEvents = getEventsByAction(events, 'recovered-instance'); - - // make sure the events are in the right temporal order - const executeTimes = getTimestamps(executeEvents); - const executeStartTimes = getTimestamps(executeStartEvents); - const executeActionTimes = getTimestamps(executeActionEvents); - const newInstanceTimes = getTimestamps(newInstanceEvents); - const recoveredInstanceTimes = getTimestamps(recoveredInstanceEvents); - - expect(executeTimes[0] < newInstanceTimes[0]).to.be(true); - expect(executeTimes[1] <= newInstanceTimes[0]).to.be(true); - expect(executeTimes[2] > newInstanceTimes[0]).to.be(true); - expect(executeTimes[1] <= executeActionTimes[0]).to.be(true); - expect(executeTimes[2] > executeActionTimes[0]).to.be(true); - expect(executeStartTimes.length === executeTimes.length).to.be(true); - executeStartTimes.forEach((est, index) => expect(est === executeTimes[index]).to.be(true)); - expect(recoveredInstanceTimes[0] > newInstanceTimes[0]).to.be(true); - - // validate each event - let executeCount = 0; - const executeStatuses = ['ok', 'active', 'active']; - for (const event of events) { - switch (event?.event?.action) { - case 'execute-start': - validateEvent(event, { - spaceId: Spaces.space1.id, - savedObjects: [ - { type: 'alert', id: alertId, rel: 'primary', type_id: 'test.patternFiring' }, - ], - message: `alert execution start: "${alertId}"`, - rule: { - id: alertId, - category: response.body.rule_type_id, - license: 'basic', - ruleset: 'alertsFixture', - namespace: Spaces.space1.id, - }, + expect(response.status).to.eql(200); + const alertId = response.body.id; + objectRemover.add(space.id, alertId, 'rule', 'alerting'); + + // get the events we're expecting + const events = await retry.try(async () => { + return await getEventLog({ + getService, + spaceId: space.id, + type: 'alert', + id: alertId, + provider: 'alerting', + actions: new Map([ + // make sure the counts of the # of events per type are as expected + ['execute-start', { gte: 4 }], + ['execute', { gte: 4 }], + ['execute-action', { equal: 2 }], + ['new-instance', { equal: 1 }], + ['active-instance', { gte: 1 }], + ['recovered-instance', { equal: 1 }], + ]), }); - break; - case 'execute': - validateEvent(event, { - spaceId: Spaces.space1.id, - savedObjects: [ - { type: 'alert', id: alertId, rel: 'primary', type_id: 'test.patternFiring' }, - ], - outcome: 'success', - message: `alert executed: test.patternFiring:${alertId}: 'abc'`, - status: executeStatuses[executeCount++], - rule: { - id: alertId, - category: response.body.rule_type_id, - license: 'basic', - ruleset: 'alertsFixture', - namespace: Spaces.space1.id, - name: response.body.name, - }, + }); + + // get the filtered events only with action 'new-instance' + const filteredEvents = await retry.try(async () => { + return await getEventLog({ + getService, + spaceId: space.id, + type: 'alert', + id: alertId, + provider: 'alerting', + actions: new Map([['new-instance', { equal: 1 }]]), + filter: 'event.action:(new-instance)', }); - break; - case 'execute-action': + }); + + expect(getEventsByAction(filteredEvents, 'execute').length).equal(0); + expect(getEventsByAction(filteredEvents, 'execute-action').length).equal(0); + expect(getEventsByAction(events, 'new-instance').length).equal(1); + + const executeEvents = getEventsByAction(events, 'execute'); + const executeStartEvents = getEventsByAction(events, 'execute-start'); + const executeActionEvents = getEventsByAction(events, 'execute-action'); + const newInstanceEvents = getEventsByAction(events, 'new-instance'); + const recoveredInstanceEvents = getEventsByAction(events, 'recovered-instance'); + + // make sure the events are in the right temporal order + const executeTimes = getTimestamps(executeEvents); + const executeStartTimes = getTimestamps(executeStartEvents); + const executeActionTimes = getTimestamps(executeActionEvents); + const newInstanceTimes = getTimestamps(newInstanceEvents); + const recoveredInstanceTimes = getTimestamps(recoveredInstanceEvents); + + expect(executeTimes[0] < newInstanceTimes[0]).to.be(true); + expect(executeTimes[1] <= newInstanceTimes[0]).to.be(true); + expect(executeTimes[2] > newInstanceTimes[0]).to.be(true); + expect(executeTimes[1] <= executeActionTimes[0]).to.be(true); + expect(executeTimes[2] > executeActionTimes[0]).to.be(true); + expect(executeStartTimes.length === executeTimes.length).to.be(true); + executeStartTimes.forEach((est, index) => + expect(est === executeTimes[index]).to.be(true) + ); + expect(recoveredInstanceTimes[0] > newInstanceTimes[0]).to.be(true); + + // validate each event + let executeCount = 0; + const executeStatuses = ['ok', 'active', 'active']; + for (const event of events) { + switch (event?.event?.action) { + case 'execute-start': + validateEvent(event, { + spaceId: space.id, + savedObjects: [ + { type: 'alert', id: alertId, rel: 'primary', type_id: 'test.patternFiring' }, + ], + message: `alert execution start: "${alertId}"`, + shouldHaveTask: true, + rule: { + id: alertId, + category: response.body.rule_type_id, + license: 'basic', + ruleset: 'alertsFixture', + }, + }); + break; + case 'execute': + validateEvent(event, { + spaceId: space.id, + savedObjects: [ + { type: 'alert', id: alertId, rel: 'primary', type_id: 'test.patternFiring' }, + ], + outcome: 'success', + message: `alert executed: test.patternFiring:${alertId}: 'abc'`, + status: executeStatuses[executeCount++], + shouldHaveTask: true, + rule: { + id: alertId, + category: response.body.rule_type_id, + license: 'basic', + ruleset: 'alertsFixture', + name: response.body.name, + }, + }); + break; + case 'execute-action': + validateEvent(event, { + spaceId: space.id, + savedObjects: [ + { type: 'alert', id: alertId, rel: 'primary', type_id: 'test.patternFiring' }, + { type: 'action', id: createdAction.id, type_id: 'test.noop' }, + ], + message: `alert: test.patternFiring:${alertId}: 'abc' instanceId: 'instance' scheduled actionGroup: 'default' action: test.noop:${createdAction.id}`, + instanceId: 'instance', + actionGroupId: 'default', + rule: { + id: alertId, + category: response.body.rule_type_id, + license: 'basic', + ruleset: 'alertsFixture', + name: response.body.name, + }, + }); + break; + case 'new-instance': + validateInstanceEvent(event, `created new instance: 'instance'`, false); + break; + case 'recovered-instance': + validateInstanceEvent(event, `instance 'instance' has recovered`, true); + break; + case 'active-instance': + validateInstanceEvent( + event, + `active instance: 'instance' in actionGroup: 'default'`, + false + ); + break; + // this will get triggered as we add new event actions + default: + throw new Error(`unexpected event action "${event?.event?.action}"`); + } + } + + const actionEvents = await retry.try(async () => { + return await getEventLog({ + getService, + spaceId: space.id, + type: 'action', + id: createdAction.id, + provider: 'actions', + actions: new Map([['execute', { gte: 1 }]]), + }); + }); + + for (const event of actionEvents) { + switch (event?.event?.action) { + case 'execute': + validateEvent(event, { + spaceId: space.id, + savedObjects: [ + { type: 'action', id: createdAction.id, rel: 'primary', type_id: 'test.noop' }, + ], + message: `action executed: test.noop:${createdAction.id}: MY action`, + outcome: 'success', + shouldHaveTask: true, + rule: undefined, + }); + break; + } + } + + function validateInstanceEvent( + event: IValidatedEvent, + subMessage: string, + shouldHaveEventEnd: boolean + ) { validateEvent(event, { - spaceId: Spaces.space1.id, + spaceId: space.id, savedObjects: [ { type: 'alert', id: alertId, rel: 'primary', type_id: 'test.patternFiring' }, - { type: 'action', id: createdAction.id, type_id: 'test.noop' }, ], - message: `alert: test.patternFiring:${alertId}: 'abc' instanceId: 'instance' scheduled actionGroup: 'default' action: test.noop:${createdAction.id}`, + message: `test.patternFiring:${alertId}: 'abc' ${subMessage}`, instanceId: 'instance', actionGroupId: 'default', + shouldHaveEventEnd, rule: { id: alertId, category: response.body.rule_type_id, license: 'basic', ruleset: 'alertsFixture', - namespace: Spaces.space1.id, name: response.body.name, }, }); - break; - case 'new-instance': - validateInstanceEvent(event, `created new instance: 'instance'`, false); - break; - case 'recovered-instance': - validateInstanceEvent(event, `instance 'instance' has recovered`, true); - break; - case 'active-instance': - validateInstanceEvent( - event, - `active instance: 'instance' in actionGroup: 'default'`, - false - ); - break; - // this will get triggered as we add new event actions - default: - throw new Error(`unexpected event action "${event?.event?.action}"`); - } - } - - function validateInstanceEvent( - event: IValidatedEvent, - subMessage: string, - shouldHaveEventEnd: boolean - ) { - validateEvent(event, { - spaceId: Spaces.space1.id, - savedObjects: [ - { type: 'alert', id: alertId, rel: 'primary', type_id: 'test.patternFiring' }, - ], - message: `test.patternFiring:${alertId}: 'abc' ${subMessage}`, - instanceId: 'instance', - actionGroupId: 'default', - shouldHaveEventEnd, - rule: { - id: alertId, - category: response.body.rule_type_id, - license: 'basic', - ruleset: 'alertsFixture', - namespace: Spaces.space1.id, - name: response.body.name, - }, + } }); - } - }); - - it('should generate expected events for normal operation with subgroups', async () => { - const { body: createdAction } = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`) - .set('kbn-xsrf', 'foo') - .send({ - name: 'MY action', - connector_type_id: 'test.noop', - config: {}, - secrets: {}, - }) - .expect(200); - - // pattern of when the alert should fire - const [firstSubgroup, secondSubgroup] = [uuid.v4(), uuid.v4()]; - const pattern = { - instance: [false, firstSubgroup, secondSubgroup], - }; - - const response = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) - .set('kbn-xsrf', 'foo') - .send( - getTestAlertData({ - rule_type_id: 'test.patternFiring', - schedule: { interval: '1s' }, - throttle: null, - params: { - pattern, - }, - actions: [ - { - id: createdAction.id, - group: 'default', - params: {}, - }, - ], - }) - ); - - expect(response.status).to.eql(200); - const alertId = response.body.id; - objectRemover.add(Spaces.space1.id, alertId, 'rule', 'alerting'); - - // get the events we're expecting - const events = await retry.try(async () => { - return await getEventLog({ - getService, - spaceId: Spaces.space1.id, - type: 'alert', - id: alertId, - provider: 'alerting', - actions: new Map([ - // make sure the counts of the # of events per type are as expected - ['execute-start', { gte: 4 }], - ['execute', { gte: 4 }], - ['execute-action', { equal: 2 }], - ['new-instance', { equal: 1 }], - ['active-instance', { gte: 2 }], - ['recovered-instance', { equal: 1 }], - ]), - }); - }); - const executeEvents = getEventsByAction(events, 'execute'); - const executeStartEvents = getEventsByAction(events, 'execute-start'); - const executeActionEvents = getEventsByAction(events, 'execute-action'); - const newInstanceEvents = getEventsByAction(events, 'new-instance'); - const recoveredInstanceEvents = getEventsByAction(events, 'recovered-instance'); - - // make sure the events are in the right temporal order - const executeTimes = getTimestamps(executeEvents); - const executeStartTimes = getTimestamps(executeStartEvents); - const executeActionTimes = getTimestamps(executeActionEvents); - const newInstanceTimes = getTimestamps(newInstanceEvents); - const recoveredInstanceTimes = getTimestamps(recoveredInstanceEvents); - - expect(executeTimes[0] < newInstanceTimes[0]).to.be(true); - expect(executeTimes[1] <= newInstanceTimes[0]).to.be(true); - expect(executeTimes[2] > newInstanceTimes[0]).to.be(true); - expect(executeTimes[1] <= executeActionTimes[0]).to.be(true); - expect(executeTimes[2] > executeActionTimes[0]).to.be(true); - expect(executeStartTimes.length === executeTimes.length).to.be(true); - executeStartTimes.forEach((est, index) => expect(est === executeTimes[index]).to.be(true)); - expect(recoveredInstanceTimes[0] > newInstanceTimes[0]).to.be(true); - - // validate each event - let executeCount = 0; - const executeStatuses = ['ok', 'active', 'active']; - for (const event of events) { - switch (event?.event?.action) { - case 'execute-start': - validateEvent(event, { - spaceId: Spaces.space1.id, - savedObjects: [ - { type: 'alert', id: alertId, rel: 'primary', type_id: 'test.patternFiring' }, - ], - message: `alert execution start: "${alertId}"`, - rule: { - id: alertId, - category: response.body.rule_type_id, - license: 'basic', - ruleset: 'alertsFixture', - namespace: Spaces.space1.id, - }, - }); - break; - case 'execute': - validateEvent(event, { - spaceId: Spaces.space1.id, - savedObjects: [ - { type: 'alert', id: alertId, rel: 'primary', type_id: 'test.patternFiring' }, - ], - outcome: 'success', - message: `alert executed: test.patternFiring:${alertId}: 'abc'`, - status: executeStatuses[executeCount++], - rule: { - id: alertId, - category: response.body.rule_type_id, - license: 'basic', - ruleset: 'alertsFixture', - namespace: Spaces.space1.id, - name: response.body.name, - }, + it('should generate expected events for normal operation with subgroups', async () => { + const { body: createdAction } = await supertest + .post(`${getUrlPrefix(space.id)}/api/actions/connector`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'MY action', + connector_type_id: 'test.noop', + config: {}, + secrets: {}, + }) + .expect(200); + + // pattern of when the alert should fire + const [firstSubgroup, secondSubgroup] = [uuid.v4(), uuid.v4()]; + const pattern = { + instance: [false, firstSubgroup, secondSubgroup], + }; + + const response = await supertest + .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send( + getTestAlertData({ + rule_type_id: 'test.patternFiring', + schedule: { interval: '1s' }, + throttle: null, + params: { + pattern, + }, + actions: [ + { + id: createdAction.id, + group: 'default', + params: {}, + }, + ], + }) + ); + + expect(response.status).to.eql(200); + const alertId = response.body.id; + objectRemover.add(space.id, alertId, 'rule', 'alerting'); + + // get the events we're expecting + const events = await retry.try(async () => { + return await getEventLog({ + getService, + spaceId: space.id, + type: 'alert', + id: alertId, + provider: 'alerting', + actions: new Map([ + // make sure the counts of the # of events per type are as expected + ['execute-start', { gte: 4 }], + ['execute', { gte: 4 }], + ['execute-action', { equal: 2 }], + ['new-instance', { equal: 1 }], + ['active-instance', { gte: 2 }], + ['recovered-instance', { equal: 1 }], + ]), }); - break; - case 'execute-action': - expect( - [firstSubgroup, secondSubgroup].includes(event?.kibana?.alerting?.action_subgroup!) - ).to.be(true); + }); + + const executeEvents = getEventsByAction(events, 'execute'); + const executeStartEvents = getEventsByAction(events, 'execute-start'); + const executeActionEvents = getEventsByAction(events, 'execute-action'); + const newInstanceEvents = getEventsByAction(events, 'new-instance'); + const recoveredInstanceEvents = getEventsByAction(events, 'recovered-instance'); + + // make sure the events are in the right temporal order + const executeTimes = getTimestamps(executeEvents); + const executeStartTimes = getTimestamps(executeStartEvents); + const executeActionTimes = getTimestamps(executeActionEvents); + const newInstanceTimes = getTimestamps(newInstanceEvents); + const recoveredInstanceTimes = getTimestamps(recoveredInstanceEvents); + + expect(executeTimes[0] < newInstanceTimes[0]).to.be(true); + expect(executeTimes[1] <= newInstanceTimes[0]).to.be(true); + expect(executeTimes[2] > newInstanceTimes[0]).to.be(true); + expect(executeTimes[1] <= executeActionTimes[0]).to.be(true); + expect(executeTimes[2] > executeActionTimes[0]).to.be(true); + expect(executeStartTimes.length === executeTimes.length).to.be(true); + executeStartTimes.forEach((est, index) => + expect(est === executeTimes[index]).to.be(true) + ); + expect(recoveredInstanceTimes[0] > newInstanceTimes[0]).to.be(true); + + // validate each event + let executeCount = 0; + const executeStatuses = ['ok', 'active', 'active']; + for (const event of events) { + switch (event?.event?.action) { + case 'execute-start': + validateEvent(event, { + spaceId: space.id, + savedObjects: [ + { type: 'alert', id: alertId, rel: 'primary', type_id: 'test.patternFiring' }, + ], + message: `alert execution start: "${alertId}"`, + shouldHaveTask: true, + rule: { + id: alertId, + category: response.body.rule_type_id, + license: 'basic', + ruleset: 'alertsFixture', + }, + }); + break; + case 'execute': + validateEvent(event, { + spaceId: space.id, + savedObjects: [ + { type: 'alert', id: alertId, rel: 'primary', type_id: 'test.patternFiring' }, + ], + outcome: 'success', + message: `alert executed: test.patternFiring:${alertId}: 'abc'`, + status: executeStatuses[executeCount++], + shouldHaveTask: true, + rule: { + id: alertId, + category: response.body.rule_type_id, + license: 'basic', + ruleset: 'alertsFixture', + name: response.body.name, + }, + }); + break; + case 'execute-action': + expect( + [firstSubgroup, secondSubgroup].includes( + event?.kibana?.alerting?.action_subgroup! + ) + ).to.be(true); + validateEvent(event, { + spaceId: space.id, + savedObjects: [ + { type: 'alert', id: alertId, rel: 'primary', type_id: 'test.patternFiring' }, + { type: 'action', id: createdAction.id, type_id: 'test.noop' }, + ], + message: `alert: test.patternFiring:${alertId}: 'abc' instanceId: 'instance' scheduled actionGroup(subgroup): 'default(${event?.kibana?.alerting?.action_subgroup})' action: test.noop:${createdAction.id}`, + instanceId: 'instance', + actionGroupId: 'default', + rule: { + id: alertId, + category: response.body.rule_type_id, + license: 'basic', + ruleset: 'alertsFixture', + name: response.body.name, + }, + }); + break; + case 'new-instance': + validateInstanceEvent(event, `created new instance: 'instance'`, false); + break; + case 'recovered-instance': + validateInstanceEvent(event, `instance 'instance' has recovered`, true); + break; + case 'active-instance': + expect( + [firstSubgroup, secondSubgroup].includes( + event?.kibana?.alerting?.action_subgroup! + ) + ).to.be(true); + validateInstanceEvent( + event, + `active instance: 'instance' in actionGroup(subgroup): 'default(${event?.kibana?.alerting?.action_subgroup})'`, + false + ); + break; + // this will get triggered as we add new event actions + default: + throw new Error(`unexpected event action "${event?.event?.action}"`); + } + } + + function validateInstanceEvent( + event: IValidatedEvent, + subMessage: string, + shouldHaveEventEnd: boolean + ) { validateEvent(event, { - spaceId: Spaces.space1.id, + spaceId: space.id, savedObjects: [ { type: 'alert', id: alertId, rel: 'primary', type_id: 'test.patternFiring' }, - { type: 'action', id: createdAction.id, type_id: 'test.noop' }, ], - message: `alert: test.patternFiring:${alertId}: 'abc' instanceId: 'instance' scheduled actionGroup(subgroup): 'default(${event?.kibana?.alerting?.action_subgroup})' action: test.noop:${createdAction.id}`, + message: `test.patternFiring:${alertId}: 'abc' ${subMessage}`, instanceId: 'instance', actionGroupId: 'default', + shouldHaveEventEnd, rule: { id: alertId, category: response.body.rule_type_id, license: 'basic', ruleset: 'alertsFixture', - namespace: Spaces.space1.id, name: response.body.name, }, }); - break; - case 'new-instance': - validateInstanceEvent(event, `created new instance: 'instance'`, false); - break; - case 'recovered-instance': - validateInstanceEvent(event, `instance 'instance' has recovered`, true); - break; - case 'active-instance': - expect( - [firstSubgroup, secondSubgroup].includes(event?.kibana?.alerting?.action_subgroup!) - ).to.be(true); - validateInstanceEvent( - event, - `active instance: 'instance' in actionGroup(subgroup): 'default(${event?.kibana?.alerting?.action_subgroup})'`, - false - ); - break; - // this will get triggered as we add new event actions - default: - throw new Error(`unexpected event action "${event?.event?.action}"`); - } - } - - function validateInstanceEvent( - event: IValidatedEvent, - subMessage: string, - shouldHaveEventEnd: boolean - ) { - validateEvent(event, { - spaceId: Spaces.space1.id, - savedObjects: [ - { type: 'alert', id: alertId, rel: 'primary', type_id: 'test.patternFiring' }, - ], - message: `test.patternFiring:${alertId}: 'abc' ${subMessage}`, - instanceId: 'instance', - actionGroupId: 'default', - shouldHaveEventEnd, - rule: { - id: alertId, - category: response.body.rule_type_id, - license: 'basic', - ruleset: 'alertsFixture', - namespace: Spaces.space1.id, - name: response.body.name, - }, - }); - } - }); - - it('should generate events for execution errors', async () => { - const response = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) - .set('kbn-xsrf', 'foo') - .send( - getTestAlertData({ - rule_type_id: 'test.throw', - schedule: { interval: '1s' }, - throttle: null, - }) - ); - - expect(response.status).to.eql(200); - const alertId = response.body.id; - objectRemover.add(Spaces.space1.id, alertId, 'rule', 'alerting'); - - const events = await retry.try(async () => { - return await getEventLog({ - getService, - spaceId: Spaces.space1.id, - type: 'alert', - id: alertId, - provider: 'alerting', - actions: new Map([ - ['execute-start', { gte: 1 }], - ['execute', { gte: 1 }], - ]), + } }); - }); - const startEvent = events[0]; - const executeEvent = events[1]; - - expect(startEvent).to.be.ok(); - expect(executeEvent).to.be.ok(); - - validateEvent(startEvent, { - spaceId: Spaces.space1.id, - savedObjects: [ - { type: 'alert', id: alertId, rel: 'primary', type_id: 'test.patternFiring' }, - ], - message: `alert execution start: "${alertId}"`, - rule: { - id: alertId, - category: response.body.rule_type_id, - license: 'basic', - ruleset: 'alertsFixture', - namespace: Spaces.space1.id, - }, - }); + it('should generate events for execution errors', async () => { + const response = await supertest + .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send( + getTestAlertData({ + rule_type_id: 'test.throw', + schedule: { interval: '1s' }, + throttle: null, + }) + ); + + expect(response.status).to.eql(200); + const alertId = response.body.id; + objectRemover.add(space.id, alertId, 'rule', 'alerting'); + + const events = await retry.try(async () => { + return await getEventLog({ + getService, + spaceId: space.id, + type: 'alert', + id: alertId, + provider: 'alerting', + actions: new Map([ + ['execute-start', { gte: 1 }], + ['execute', { gte: 1 }], + ]), + }); + }); - validateEvent(executeEvent, { - spaceId: Spaces.space1.id, - savedObjects: [{ type: 'alert', id: alertId, rel: 'primary', type_id: 'test.throw' }], - outcome: 'failure', - message: `alert execution failure: test.throw:${alertId}: 'abc'`, - errorMessage: 'this alert is intended to fail', - status: 'error', - reason: 'execute', - rule: { - id: alertId, - category: response.body.rule_type_id, - license: 'basic', - ruleset: 'alertsFixture', - namespace: Spaces.space1.id, - }, + const startEvent = events[0]; + const executeEvent = events[1]; + + expect(startEvent).to.be.ok(); + expect(executeEvent).to.be.ok(); + + validateEvent(startEvent, { + spaceId: space.id, + savedObjects: [ + { type: 'alert', id: alertId, rel: 'primary', type_id: 'test.patternFiring' }, + ], + message: `alert execution start: "${alertId}"`, + shouldHaveTask: true, + rule: { + id: alertId, + category: response.body.rule_type_id, + license: 'basic', + ruleset: 'alertsFixture', + }, + }); + + validateEvent(executeEvent, { + spaceId: space.id, + savedObjects: [{ type: 'alert', id: alertId, rel: 'primary', type_id: 'test.throw' }], + outcome: 'failure', + message: `alert execution failure: test.throw:${alertId}: 'abc'`, + errorMessage: 'this alert is intended to fail', + status: 'error', + reason: 'execute', + shouldHaveTask: true, + rule: { + id: alertId, + category: response.body.rule_type_id, + license: 'basic', + ruleset: 'alertsFixture', + }, + }); + }); }); - }); + } }); } @@ -510,12 +546,13 @@ interface ValidateEventLogParams { outcome?: string; message: string; shouldHaveEventEnd?: boolean; + shouldHaveTask?: boolean; errorMessage?: string; status?: string; actionGroupId?: string; instanceId?: string; reason?: string; - rule: { + rule?: { id: string; name?: string; version?: string; @@ -529,7 +566,7 @@ interface ValidateEventLogParams { } export function validateEvent(event: IValidatedEvent, params: ValidateEventLogParams): void { - const { spaceId, savedObjects, outcome, message, errorMessage, rule } = params; + const { spaceId, savedObjects, outcome, message, errorMessage, rule, shouldHaveTask } = params; const { status, actionGroupId, instanceId, reason, shouldHaveEventEnd } = params; if (status) { @@ -587,6 +624,16 @@ export function validateEvent(event: IValidatedEvent, params: ValidateEventLogPa expect(event?.rule).to.eql(rule); + if (shouldHaveTask) { + const task = event?.kibana?.task; + expect(task).to.be.ok(); + expect(typeof Date.parse(typeof task?.scheduled)).to.be('number'); + expect(typeof task?.schedule_delay).to.be('number'); + expect(task?.schedule_delay).to.be.greaterThan(-1); + } else { + expect(event?.kibana?.task).to.be(undefined); + } + if (errorMessage) { expect(event?.error?.message).to.eql(errorMessage); } @@ -602,12 +649,13 @@ function getTimestamps(events: IValidatedEvent[]) { function isSavedObjectInEvent( event: IValidatedEvent, - namespace: string, + spaceId: string, type: string, id: string, rel?: string ): boolean { const savedObjects = event?.kibana?.saved_objects ?? []; + const namespace = spaceId === 'default' ? undefined : spaceId; for (const savedObject of savedObjects) { if ( From be1c5bbd72feed9544ad11e17c49fec3b2189104 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Thu, 24 Jun 2021 16:52:55 +0200 Subject: [PATCH 27/69] Don't import react-intl directly to reduce bundle sizes (#102497) --- packages/elastic-eslint-config-kibana/.eslintrc.js | 5 +++++ packages/kbn-i18n/src/react/index.tsx | 1 + packages/kbn-i18n/src/react/provider.tsx | 2 ++ .../components/field_format_editor/editors/url/url.test.tsx | 2 +- .../select_anomaly_severity.test.tsx | 2 +- .../public/search/sessions_mgmt/__mocks__/index.tsx | 2 +- .../public/search/sessions_mgmt/lib/get_columns.tsx | 2 +- .../connected_search_session_indicator.test.tsx | 2 +- .../search_session_indicator.test.tsx | 2 +- .../relevance_tuning/relevance_tuning_callouts.tsx | 3 +-- .../ml/anomaly_detection/anomalies_table/anomalies_table.tsx | 2 +- .../components/node_details/tabs/properties/table.tsx | 2 +- .../components/annotations/annotation_flyout/index.test.tsx | 2 +- .../scatterplot_matrix/scatterplot_matrix.test.tsx | 2 +- .../components/source_selection/source_selection.test.tsx | 2 +- .../core_web_vitals/__stories__/core_vitals.stories.tsx | 2 +- .../__stories__/field_value_selection.stories.tsx | 2 +- .../observability/public/pages/alerts/alerts.stories.tsx | 2 +- x-pack/plugins/observability/public/utils/test_helper.tsx | 2 +- .../public/components/screen_capture_panel_content.test.tsx | 2 +- .../security_solution/public/resolver/view/submenu.tsx | 2 +- .../transform/public/app/hooks/use_index_data.test.tsx | 2 +- .../components/action_discover/discover_action_name.test.tsx | 2 +- .../swimlane/steps/swimlane_connection.tsx | 2 +- .../sections/action_connector_form/action_type_menu.tsx | 2 +- 25 files changed, 30 insertions(+), 23 deletions(-) diff --git a/packages/elastic-eslint-config-kibana/.eslintrc.js b/packages/elastic-eslint-config-kibana/.eslintrc.js index a8c2e9546510e..3220a01184004 100644 --- a/packages/elastic-eslint-config-kibana/.eslintrc.js +++ b/packages/elastic-eslint-config-kibana/.eslintrc.js @@ -75,6 +75,11 @@ module.exports = { to: '@kbn/test', disallowedMessage: `import from the root of @kbn/test instead` }, + { + from: 'react-intl', + to: '@kbn/i18n/react', + disallowedMessage: `import from @kbn/i18n/react instead` + } ], ], }, diff --git a/packages/kbn-i18n/src/react/index.tsx b/packages/kbn-i18n/src/react/index.tsx index 08fa7173978d9..bc0a164d412af 100644 --- a/packages/kbn-i18n/src/react/index.tsx +++ b/packages/kbn-i18n/src/react/index.tsx @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +// eslint-disable-next-line @kbn/eslint/module_migration import { InjectedIntl as _InjectedIntl, InjectedIntlProps as _InjectedIntlProps } from 'react-intl'; export type { InjectedIntl, InjectedIntlProps } from 'react-intl'; diff --git a/packages/kbn-i18n/src/react/provider.tsx b/packages/kbn-i18n/src/react/provider.tsx index 2d88125291aa0..fc0f6769c7160 100644 --- a/packages/kbn-i18n/src/react/provider.tsx +++ b/packages/kbn-i18n/src/react/provider.tsx @@ -8,6 +8,8 @@ import * as PropTypes from 'prop-types'; import * as React from 'react'; + +// eslint-disable-next-line @kbn/eslint/module_migration import { IntlProvider } from 'react-intl'; import * as i18n from '../core'; diff --git a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/url/url.test.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/url/url.test.tsx index 9f299a433aab1..1000d9d2b8650 100644 --- a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/url/url.test.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/url/url.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { FieldFormat } from 'src/plugins/data/public'; -import { IntlProvider } from 'react-intl'; +import { __IntlProvider as IntlProvider } from '@kbn/i18n/react'; import { UrlFormatEditor } from './url'; import { coreMock } from 'src/core/public/mocks'; import { createKibanaReactContext } from '../../../../../../kibana_react/public'; diff --git a/x-pack/plugins/apm/public/components/alerting/transaction_duration_anomaly_alert_trigger/select_anomaly_severity.test.tsx b/x-pack/plugins/apm/public/components/alerting/transaction_duration_anomaly_alert_trigger/select_anomaly_severity.test.tsx index 7b56eaa4721de..8c8f0aa8b9b24 100644 --- a/x-pack/plugins/apm/public/components/alerting/transaction_duration_anomaly_alert_trigger/select_anomaly_severity.test.tsx +++ b/x-pack/plugins/apm/public/components/alerting/transaction_duration_anomaly_alert_trigger/select_anomaly_severity.test.tsx @@ -7,7 +7,7 @@ import { render } from '@testing-library/react'; import React, { ReactNode } from 'react'; -import { IntlProvider } from 'react-intl'; +import { __IntlProvider as IntlProvider } from '@kbn/i18n/react'; import { ANOMALY_SEVERITY } from '../../../../common/ml_constants'; import { SelectAnomalySeverity } from './select_anomaly_severity'; diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/__mocks__/index.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/__mocks__/index.tsx index ee43a3c1a21e2..d52ca5b45613a 100644 --- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/__mocks__/index.tsx +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/__mocks__/index.tsx @@ -6,7 +6,7 @@ */ import React, { ReactNode } from 'react'; -import { IntlProvider } from 'react-intl'; +import { __IntlProvider as IntlProvider } from '@kbn/i18n/react'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { UrlGeneratorsStart } from '../../../../../../../src/plugins/share/public/url_generators'; diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/get_columns.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/get_columns.tsx index d8d2fa0aeac59..7dd4966124e96 100644 --- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/get_columns.tsx +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/get_columns.tsx @@ -16,10 +16,10 @@ import { EuiToolTip, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { CoreStart } from 'kibana/public'; import { capitalize } from 'lodash'; import React from 'react'; -import { FormattedMessage } from 'react-intl'; import { RedirectAppLinks } from '../../../../../../../src/plugins/kibana_react/public'; import { IManagementSectionsPluginsSetup, SessionsConfigSchema } from '../'; import { SearchSessionStatus } from '../../../../../../../src/plugins/data/common'; diff --git a/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.test.tsx b/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.test.tsx index a16557b50700e..893f352b5d828 100644 --- a/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.test.tsx +++ b/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.test.tsx @@ -22,7 +22,7 @@ import { import { coreMock } from '../../../../../../../src/core/public/mocks'; import { TOUR_RESTORE_STEP_KEY, TOUR_TAKING_TOO_LONG_STEP_KEY } from './search_session_tour'; import userEvent from '@testing-library/user-event'; -import { IntlProvider } from 'react-intl'; +import { __IntlProvider as IntlProvider } from '@kbn/i18n/react'; import { createSearchUsageCollectorMock } from '../../../../../../../src/plugins/data/public/search/collectors/mocks'; const coreStart = coreMock.createStart(); diff --git a/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.test.tsx b/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.test.tsx index ff9e27cad1869..310379f90c789 100644 --- a/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.test.tsx +++ b/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.test.tsx @@ -9,7 +9,7 @@ import React, { ReactNode } from 'react'; import { screen, render } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { SearchSessionIndicator } from './search_session_indicator'; -import { IntlProvider } from 'react-intl'; +import { __IntlProvider as IntlProvider } from '@kbn/i18n/react'; import { SearchSessionState } from '../../../../../../../src/plugins/data/public'; function Container({ children }: { children?: ReactNode }) { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_callouts.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_callouts.tsx index c981d35ff20cb..bdbc414a22eaa 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_callouts.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_callouts.tsx @@ -7,12 +7,11 @@ import React from 'react'; -import { FormattedMessage } from 'react-intl'; - import { useValues } from 'kea'; import { EuiCallOut, EuiLink } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { EuiLinkTo } from '../../../shared/react_router_helpers'; import { DOCS_PREFIX, ENGINE_SCHEMA_PATH } from '../../routes'; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/anomalies_table/anomalies_table.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/anomalies_table/anomalies_table.tsx index 8c8a5ae56c3ba..98f3c82818dd2 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/anomalies_table/anomalies_table.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/anomalies_table/anomalies_table.tsx @@ -26,7 +26,7 @@ import { EuiText, OnTimeChangeProps, } from '@elastic/eui'; -import { FormattedDate, FormattedMessage } from 'react-intl'; +import { FormattedMessage, FormattedDate } from '@kbn/i18n/react'; import { datemathToEpochMillis } from '../../../../../../../utils/datemath'; import { SnapshotMetricType } from '../../../../../../../../common/inventory_models/types'; import { withTheme } from '../../../../../../../../../../../src/plugins/kibana_react/common'; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/properties/table.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/properties/table.tsx index 1d465698dcb45..053e50ff87049 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/properties/table.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/properties/table.tsx @@ -18,7 +18,7 @@ import { import { i18n } from '@kbn/i18n'; import { first } from 'lodash'; import React, { useCallback, useMemo, useState } from 'react'; -import { FormattedMessage } from 'react-intl'; +import { FormattedMessage } from '@kbn/i18n/react'; interface Row { name: string; diff --git a/x-pack/plugins/ml/public/application/components/annotations/annotation_flyout/index.test.tsx b/x-pack/plugins/ml/public/application/components/annotations/annotation_flyout/index.test.tsx index 05d400c5bb0ad..bf4b33350b382 100644 --- a/x-pack/plugins/ml/public/application/components/annotations/annotation_flyout/index.test.tsx +++ b/x-pack/plugins/ml/public/application/components/annotations/annotation_flyout/index.test.tsx @@ -9,7 +9,7 @@ import useObservable from 'react-use/lib/useObservable'; import mockAnnotations from '../annotations_table/__mocks__/mock_annotations.json'; import React from 'react'; import { fireEvent, render, screen, waitFor } from '@testing-library/react'; -import { IntlProvider } from 'react-intl'; +import { __IntlProvider as IntlProvider } from '@kbn/i18n/react'; import { Annotation } from '../../../../../common/types/annotations'; import { AnnotationUpdatesService } from '../../../services/annotations_service'; diff --git a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.test.tsx b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.test.tsx index 10deaa1c2d489..d0e70c38c23b4 100644 --- a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.test.tsx +++ b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { render, waitFor, screen } from '@testing-library/react'; -import { IntlProvider } from 'react-intl'; +import { __IntlProvider as IntlProvider } from '@kbn/i18n/react'; import euiThemeLight from '@elastic/eui/dist/eui_theme_light.json'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.test.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.test.tsx index 8e7aecf429ad0..7e90a4e3ed44a 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.test.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { render, fireEvent, waitFor, screen } from '@testing-library/react'; -import { IntlProvider } from 'react-intl'; +import { __IntlProvider as IntlProvider } from '@kbn/i18n/react'; import { getIndexPatternAndSavedSearch, diff --git a/x-pack/plugins/observability/public/components/shared/core_web_vitals/__stories__/core_vitals.stories.tsx b/x-pack/plugins/observability/public/components/shared/core_web_vitals/__stories__/core_vitals.stories.tsx index d84e637da087e..5f5cf2cb4da21 100644 --- a/x-pack/plugins/observability/public/components/shared/core_web_vitals/__stories__/core_vitals.stories.tsx +++ b/x-pack/plugins/observability/public/components/shared/core_web_vitals/__stories__/core_vitals.stories.tsx @@ -6,7 +6,7 @@ */ import React, { ComponentType } from 'react'; -import { IntlProvider } from 'react-intl'; +import { __IntlProvider as IntlProvider } from '@kbn/i18n/react'; import { Observable } from 'rxjs'; import { CoreStart } from 'src/core/public'; import { EuiThemeProvider } from '../../../../../../../../src/plugins/kibana_react/common'; diff --git a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/__stories__/field_value_selection.stories.tsx b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/__stories__/field_value_selection.stories.tsx index 665010be3aff2..80a25b82eb8cb 100644 --- a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/__stories__/field_value_selection.stories.tsx +++ b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/__stories__/field_value_selection.stories.tsx @@ -6,7 +6,7 @@ */ import React, { ComponentType, useEffect, useState } from 'react'; -import { IntlProvider } from 'react-intl'; +import { __IntlProvider as IntlProvider } from '@kbn/i18n/react'; import { Observable } from 'rxjs'; import { CoreStart } from 'src/core/public'; import { text } from '@storybook/addon-knobs'; diff --git a/x-pack/plugins/observability/public/pages/alerts/alerts.stories.tsx b/x-pack/plugins/observability/public/pages/alerts/alerts.stories.tsx index ba714a679f1e5..ccc85d7f40187 100644 --- a/x-pack/plugins/observability/public/pages/alerts/alerts.stories.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/alerts.stories.tsx @@ -7,7 +7,7 @@ import { StoryContext } from '@storybook/react'; import React, { ComponentType } from 'react'; -import { IntlProvider } from 'react-intl'; +import { __IntlProvider as IntlProvider } from '@kbn/i18n/react'; import { MemoryRouter } from 'react-router-dom'; import { AlertsPage } from '.'; import { HttpSetup } from '../../../../../../src/core/public'; diff --git a/x-pack/plugins/observability/public/utils/test_helper.tsx b/x-pack/plugins/observability/public/utils/test_helper.tsx index feacb011e0701..ce71a5640515b 100644 --- a/x-pack/plugins/observability/public/utils/test_helper.tsx +++ b/x-pack/plugins/observability/public/utils/test_helper.tsx @@ -8,7 +8,7 @@ import { render as testLibRender } from '@testing-library/react'; import { AppMountParameters, CoreStart } from 'kibana/public'; import React from 'react'; -import { IntlProvider } from 'react-intl'; +import { __IntlProvider as IntlProvider } from '@kbn/i18n/react'; import { of } from 'rxjs'; import { KibanaContextProvider, diff --git a/x-pack/plugins/reporting/public/components/screen_capture_panel_content.test.tsx b/x-pack/plugins/reporting/public/components/screen_capture_panel_content.test.tsx index 84a6dcc3c0ba3..a023eae512d54 100644 --- a/x-pack/plugins/reporting/public/components/screen_capture_panel_content.test.tsx +++ b/x-pack/plugins/reporting/public/components/screen_capture_panel_content.test.tsx @@ -7,7 +7,7 @@ import { mount } from 'enzyme'; import React from 'react'; -import { IntlProvider } from 'react-intl'; +import { __IntlProvider as IntlProvider } from '@kbn/i18n/react'; import { coreMock } from '../../../../../src/core/public/mocks'; import { BaseParams } from '../../common/types'; import { ReportingAPIClient } from '../lib/reporting_api_client'; diff --git a/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx b/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx index 77f97b947d824..1315a7d6c45d9 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import React, { useMemo, useContext, useCallback } from 'react'; -import { FormattedMessage } from 'react-intl'; +import { FormattedMessage } from '@kbn/i18n/react'; import { useDispatch } from 'react-redux'; import { EuiI18nNumber } from '@elastic/eui'; import { EventStats } from '../../../common/endpoint/types'; diff --git a/x-pack/plugins/transform/public/app/hooks/use_index_data.test.tsx b/x-pack/plugins/transform/public/app/hooks/use_index_data.test.tsx index c3dc9ab4bb8a1..68f6fea3aa943 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_index_data.test.tsx +++ b/x-pack/plugins/transform/public/app/hooks/use_index_data.test.tsx @@ -6,7 +6,7 @@ */ import React, { FC } from 'react'; -import { IntlProvider } from 'react-intl'; +import { __IntlProvider as IntlProvider } from '@kbn/i18n/react'; import '@testing-library/jest-dom/extend-expect'; import { render, screen, waitFor } from '@testing-library/react'; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_discover/discover_action_name.test.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_discover/discover_action_name.test.tsx index 8dba93399792c..dc6fae40ee0d1 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_discover/discover_action_name.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_discover/discover_action_name.test.tsx @@ -7,7 +7,7 @@ import { cloneDeep } from 'lodash'; import React from 'react'; -import { IntlProvider } from 'react-intl'; +import { __IntlProvider as IntlProvider } from '@kbn/i18n/react'; import { render, waitFor, screen } from '@testing-library/react'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_connection.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_connection.tsx index cd29037e3535f..05b6d8d63f1cf 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_connection.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_connection.tsx @@ -14,7 +14,7 @@ import { EuiText, } from '@elastic/eui'; import React, { useCallback } from 'react'; -import { FormattedMessage } from 'react-intl'; +import { FormattedMessage } from '@kbn/i18n/react'; import * as i18n from '../translations'; import { useKibana } from '../../../../../common/lib/kibana'; import { useGetApplication } from '../use_get_application'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.tsx index 4428d635c6493..cc7e08bc73d15 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.tsx @@ -9,7 +9,7 @@ import React, { useEffect, useState } from 'react'; import { EuiFlexItem, EuiCard, EuiIcon, EuiFlexGrid, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { EuiToolTip } from '@elastic/eui'; -import { FormattedMessage } from 'react-intl'; +import { FormattedMessage } from '@kbn/i18n/react'; import { ActionType, ActionTypeIndex, ActionTypeRegistryContract } from '../../../types'; import { loadActionTypes } from '../../lib/action_connector_api'; import { actionTypeCompare } from '../../lib/action_type_compare'; From f2937720aa8e370d35890c3bdad1b2bd40839394 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Thu, 24 Jun 2021 10:53:15 -0400 Subject: [PATCH 28/69] [APM] Link to Fleet APM Server Configuration when managed by Elastic Agent w/Fleet (#100816) * Register tutorial on APM plugin * using files from apm * removing tutorial from apm_oss * removing export * fixing i18n * adding fleet section * adding fleet information on APM tutorial * adding fleet typing * fixing i18n * adding fleet information on APM tutorial * checks apm fleet integration when pushing button * adding fleet information on APM tutorial * refactoring * registering status check callback * addin custom component registration function * fixing TS issue * addressing PR comments * fixing tests * adding i18n * fixing issues * adding unit test * adding unit test * addressing PR comments * fixing TS issue * moving tutorial to a common directory Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../home/common/instruction_variant.ts | 6 + .../home/public/application/application.tsx | 10 +- .../components/tutorial/instruction.js | 34 ++++- .../components/tutorial/instruction_set.js | 2 + .../components/tutorial/tutorial.js | 41 ++++-- .../components/tutorial/tutorial.test.js | 113 ++++++++++++++++ .../tutorials/tutorial_service.mock.ts | 4 + .../tutorials/tutorial_service.test.tsx | 40 ++++++ .../services/tutorials/tutorial_service.ts | 21 +++ src/plugins/home/server/index.ts | 7 +- .../services/tutorials/lib/tutorial_schema.ts | 4 +- .../illustration_integrations_darkmode.svg | 1 + .../illustration_integrations_lightmode.svg | 1 + x-pack/plugins/apm/public/plugin.ts | 50 +++++-- .../apm_observability_overview_fetchers.ts | 2 - .../tutorial/tutorial_apm_fleet_check.ts | 20 +++ .../tutorial_fleet_instructions/index.tsx | 122 ++++++++++++++++++ x-pack/plugins/apm/server/routes/fleet.ts | 36 ++++++ .../get_global_apm_server_route_repository.ts | 4 +- .../apm/server/tutorial/envs/elastic_cloud.ts | 18 ++- .../apm/server/tutorial/envs/on_prem.ts | 39 +++--- x-pack/plugins/apm/server/tutorial/index.ts | 12 +- .../instructions/apm_agent_instructions.ts | 5 +- 23 files changed, 535 insertions(+), 57 deletions(-) create mode 100644 x-pack/plugins/apm/public/assets/illustration_integrations_darkmode.svg create mode 100644 x-pack/plugins/apm/public/assets/illustration_integrations_lightmode.svg create mode 100644 x-pack/plugins/apm/public/tutorial/tutorial_apm_fleet_check.ts create mode 100644 x-pack/plugins/apm/public/tutorial/tutorial_fleet_instructions/index.tsx create mode 100644 x-pack/plugins/apm/server/routes/fleet.ts diff --git a/src/plugins/home/common/instruction_variant.ts b/src/plugins/home/common/instruction_variant.ts index 310ee23460a08..f27b2c97bdc1e 100644 --- a/src/plugins/home/common/instruction_variant.ts +++ b/src/plugins/home/common/instruction_variant.ts @@ -6,6 +6,8 @@ * Side Public License, v 1. */ +import { i18n } from '@kbn/i18n'; + export const INSTRUCTION_VARIANT = { ESC: 'esc', OSX: 'osx', @@ -24,6 +26,7 @@ export const INSTRUCTION_VARIANT = { DOTNET: 'dotnet', LINUX: 'linux', PHP: 'php', + FLEET: 'fleet', }; const DISPLAY_MAP = { @@ -44,6 +47,9 @@ const DISPLAY_MAP = { [INSTRUCTION_VARIANT.DOTNET]: '.NET', [INSTRUCTION_VARIANT.LINUX]: 'Linux', [INSTRUCTION_VARIANT.PHP]: 'PHP', + [INSTRUCTION_VARIANT.FLEET]: i18n.translate('home.tutorial.instruction_variant.fleet', { + defaultMessage: 'Elastic APM (beta) in Fleet', + }), }; /** diff --git a/src/plugins/home/public/application/application.tsx b/src/plugins/home/public/application/application.tsx index 9ab720b47ab92..18f3089c14d11 100644 --- a/src/plugins/home/public/application/application.tsx +++ b/src/plugins/home/public/application/application.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { i18n } from '@kbn/i18n'; import { ScopedHistory, CoreStart } from 'kibana/public'; -import { KibanaContextProvider } from '../../../kibana_react/public'; +import { KibanaContextProvider, RedirectAppLinks } from '../../../kibana_react/public'; // @ts-ignore import { HomeApp } from './components/home_app'; import { getServices } from './kibana_services'; @@ -44,9 +44,11 @@ export const renderApp = async ( }); render( - - - , + + + + + , element ); diff --git a/src/plugins/home/public/application/components/tutorial/instruction.js b/src/plugins/home/public/application/components/tutorial/instruction.js index 42c22b057b1e2..373f8c318a504 100644 --- a/src/plugins/home/public/application/components/tutorial/instruction.js +++ b/src/plugins/home/public/application/components/tutorial/instruction.js @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import React from 'react'; +import React, { Suspense, useMemo } from 'react'; import PropTypes from 'prop-types'; import { Content } from './content'; @@ -17,11 +17,23 @@ import { EuiSpacer, EuiCopy, EuiButton, + EuiLoadingSpinner, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -export function Instruction({ commands, paramValues, textPost, textPre, replaceTemplateStrings }) { +import { getServices } from '../../kibana_services'; + +export function Instruction({ + commands, + paramValues, + textPost, + textPre, + replaceTemplateStrings, + customComponentName, +}) { + const { tutorialService, http, uiSettings, getBasePath } = getServices(); + let pre; if (textPre) { pre = ; @@ -36,6 +48,13 @@ export function Instruction({ commands, paramValues, textPost, textPre, replaceT ); } + const customComponent = tutorialService.getCustomComponent(customComponentName); + //Memoize the custom component so it wont rerender everytime + const LazyCustomComponent = useMemo(() => { + if (customComponent) { + return React.lazy(() => customComponent()); + } + }, [customComponent]); let copyButton; let commandBlock; @@ -79,6 +98,16 @@ export function Instruction({ commands, paramValues, textPost, textPre, replaceT {post} + {LazyCustomComponent && ( + }> + + + )} + ); @@ -90,4 +119,5 @@ Instruction.propTypes = { textPost: PropTypes.string, textPre: PropTypes.string, replaceTemplateStrings: PropTypes.func.isRequired, + customComponentName: PropTypes.string, }; diff --git a/src/plugins/home/public/application/components/tutorial/instruction_set.js b/src/plugins/home/public/application/components/tutorial/instruction_set.js index f16e276ed4c56..da368120d493c 100644 --- a/src/plugins/home/public/application/components/tutorial/instruction_set.js +++ b/src/plugins/home/public/application/components/tutorial/instruction_set.js @@ -186,6 +186,7 @@ class InstructionSetUi extends React.Component { textPre={instruction.textPre} textPost={instruction.textPost} replaceTemplateStrings={this.props.replaceTemplateStrings} + customComponentName={instruction.customComponentName} /> ); return { @@ -298,6 +299,7 @@ const statusCheckConfigShape = PropTypes.shape({ title: PropTypes.string, text: PropTypes.string, btnLabel: PropTypes.string, + customStatusCheck: PropTypes.string, }); InstructionSetUi.propTypes = { diff --git a/src/plugins/home/public/application/components/tutorial/tutorial.js b/src/plugins/home/public/application/components/tutorial/tutorial.js index 81a75d8881e2d..92bbb92fa0850 100644 --- a/src/plugins/home/public/application/components/tutorial/tutorial.js +++ b/src/plugins/home/public/application/components/tutorial/tutorial.js @@ -67,7 +67,6 @@ class TutorialUi extends React.Component { async componentDidMount() { const tutorial = await this.props.getTutorial(this.props.tutorialId); - if (!this._isMounted) { return; } @@ -172,15 +171,39 @@ class TutorialUi extends React.Component { const instructionSet = this.getInstructionSets()[instructionSetIndex]; const esHitsCheckConfig = _.get(instructionSet, `statusCheck.esHitsCheck`); - if (esHitsCheckConfig) { - const statusCheckState = await this.fetchEsHitsStatus(esHitsCheckConfig); + //Checks if a custom status check callback was registered in the CLIENT + //that matches the same name registered in the SERVER (customStatusCheckName) + const customStatusCheckCallback = getServices().tutorialService.getCustomStatusCheck( + this.state.tutorial.customStatusCheckName + ); - this.setState((prevState) => ({ - statusCheckStates: { - ...prevState.statusCheckStates, - [instructionSetIndex]: statusCheckState, - }, - })); + const [esHitsStatusCheck, customStatusCheck] = await Promise.all([ + ...(esHitsCheckConfig ? [this.fetchEsHitsStatus(esHitsCheckConfig)] : []), + ...(customStatusCheckCallback + ? [this.fetchCustomStatusCheck(customStatusCheckCallback)] + : []), + ]); + + const nextStatusCheckState = + esHitsStatusCheck === StatusCheckStates.HAS_DATA || + customStatusCheck === StatusCheckStates.HAS_DATA + ? StatusCheckStates.HAS_DATA + : StatusCheckStates.NO_DATA; + + this.setState((prevState) => ({ + statusCheckStates: { + ...prevState.statusCheckStates, + [instructionSetIndex]: nextStatusCheckState, + }, + })); + }; + + fetchCustomStatusCheck = async (customStatusCheckCallback) => { + try { + const response = await customStatusCheckCallback(); + return response ? StatusCheckStates.HAS_DATA : StatusCheckStates.NO_DATA; + } catch (e) { + return StatusCheckStates.ERROR; } }; diff --git a/src/plugins/home/public/application/components/tutorial/tutorial.test.js b/src/plugins/home/public/application/components/tutorial/tutorial.test.js index 490ecfd8edd78..e9c0b49451e23 100644 --- a/src/plugins/home/public/application/components/tutorial/tutorial.test.js +++ b/src/plugins/home/public/application/components/tutorial/tutorial.test.js @@ -13,12 +13,23 @@ import { Tutorial } from './tutorial'; jest.mock('../../kibana_services', () => ({ getServices: () => ({ + http: { + post: jest.fn().mockImplementation(async () => ({ count: 1 })), + }, getBasePath: jest.fn(() => 'path'), chrome: { setBreadcrumbs: () => {}, }, tutorialService: { getModuleNotices: () => [], + getCustomComponent: jest.fn(), + getCustomStatusCheck: (name) => { + const customStatusCheckMock = { + custom_status_check_has_data: async () => true, + custom_status_check_no_data: async () => false, + }; + return customStatusCheckMock[name]; + }, }, }), })); @@ -54,6 +65,7 @@ const tutorial = { elasticCloud: buildInstructionSet('elasticCloud'), onPrem: buildInstructionSet('onPrem'), onPremElasticCloud: buildInstructionSet('onPremElasticCloud'), + customStatusCheckName: 'custom_status_check_has_data', }; const loadTutorialPromise = Promise.resolve(tutorial); const getTutorial = () => { @@ -143,3 +155,104 @@ test('should render ELASTIC_CLOUD instructions when isCloudEnabled is true', asy component.update(); expect(component).toMatchSnapshot(); // eslint-disable-line }); + +describe('custom status check', () => { + test('should return has_data when custom status check callback is set and returns true', async () => { + const component = mountWithIntl( + {}} + /> + ); + await loadTutorialPromise; + component.update(); + await component.instance().checkInstructionSetStatus(0); + expect(component.state('statusCheckStates')[0]).toEqual('has_data'); + }); + test('should return no_data when custom status check callback is set and returns false', async () => { + const tutorialWithCustomStatusCheckNoData = { + ...tutorial, + customStatusCheckName: 'custom_status_check_no_data', + }; + const component = mountWithIntl( + tutorialWithCustomStatusCheckNoData} + replaceTemplateStrings={replaceTemplateStrings} + tutorialId={'my_testing_tutorial'} + bulkCreate={() => {}} + /> + ); + await loadTutorialPromise; + component.update(); + await component.instance().checkInstructionSetStatus(0); + expect(component.state('statusCheckStates')[0]).toEqual('NO_DATA'); + }); + + test('should return no_data when custom status check callback is not defined', async () => { + const tutorialWithoutCustomStatusCheck = { + ...tutorial, + customStatusCheckName: undefined, + }; + const component = mountWithIntl( + tutorialWithoutCustomStatusCheck} + replaceTemplateStrings={replaceTemplateStrings} + tutorialId={'my_testing_tutorial'} + bulkCreate={() => {}} + /> + ); + await loadTutorialPromise; + component.update(); + await component.instance().checkInstructionSetStatus(0); + expect(component.state('statusCheckStates')[0]).toEqual('NO_DATA'); + }); + + test('should return has_data if esHits or customStatusCheck returns true', async () => { + const { instructionSets } = tutorial.elasticCloud; + const tutorialWithStatusCheckAndCustomStatusCheck = { + ...tutorial, + customStatusCheckName: undefined, + elasticCloud: { + instructionSets: [ + { + ...instructionSets[0], + statusCheck: { + title: 'check status', + text: 'check status', + esHitsCheck: { + index: 'foo', + query: { + bool: { + filter: [{ term: { 'processor.event': 'onboarding' } }], + }, + }, + }, + }, + }, + ], + }, + }; + const component = mountWithIntl( + tutorialWithStatusCheckAndCustomStatusCheck} + replaceTemplateStrings={replaceTemplateStrings} + tutorialId={'my_testing_tutorial'} + bulkCreate={() => {}} + /> + ); + await loadTutorialPromise; + component.update(); + await component.instance().checkInstructionSetStatus(0); + expect(component.state('statusCheckStates')[0]).toEqual('has_data'); + }); +}); diff --git a/src/plugins/home/public/services/tutorials/tutorial_service.mock.ts b/src/plugins/home/public/services/tutorials/tutorial_service.mock.ts index ac48168a360d4..0c109d61912ca 100644 --- a/src/plugins/home/public/services/tutorials/tutorial_service.mock.ts +++ b/src/plugins/home/public/services/tutorials/tutorial_service.mock.ts @@ -15,6 +15,8 @@ const createSetupMock = (): jest.Mocked => { registerDirectoryNotice: jest.fn(), registerDirectoryHeaderLink: jest.fn(), registerModuleNotice: jest.fn(), + registerCustomStatusCheck: jest.fn(), + registerCustomComponent: jest.fn(), }; return setup; }; @@ -26,6 +28,8 @@ const createMock = (): jest.Mocked> => { getDirectoryNotices: jest.fn(() => []), getDirectoryHeaderLinks: jest.fn(() => []), getModuleNotices: jest.fn(() => []), + getCustomStatusCheck: jest.fn(), + getCustomComponent: jest.fn(), }; service.setup.mockImplementation(createSetupMock); return service; diff --git a/src/plugins/home/public/services/tutorials/tutorial_service.test.tsx b/src/plugins/home/public/services/tutorials/tutorial_service.test.tsx index 69d24b66ec6bf..a88cf526e3716 100644 --- a/src/plugins/home/public/services/tutorials/tutorial_service.test.tsx +++ b/src/plugins/home/public/services/tutorials/tutorial_service.test.tsx @@ -138,4 +138,44 @@ describe('TutorialService', () => { expect(service.getModuleNotices()).toEqual(notices); }); }); + + describe('custom status check', () => { + test('returns undefined when name is customStatusCheckName is empty', () => { + const service = new TutorialService(); + expect(service.getCustomStatusCheck('')).toBeUndefined(); + }); + test('returns undefined when custom status check was not registered', () => { + const service = new TutorialService(); + expect(service.getCustomStatusCheck('foo')).toBeUndefined(); + }); + test('returns custom status check', () => { + const service = new TutorialService(); + const callback = jest.fn(); + service.setup().registerCustomStatusCheck('foo', callback); + const customStatusCheckCallback = service.getCustomStatusCheck('foo'); + expect(customStatusCheckCallback).toBeDefined(); + customStatusCheckCallback(); + expect(callback).toHaveBeenCalled(); + }); + }); + + describe('custom component', () => { + test('returns undefined when name is customComponentName is empty', () => { + const service = new TutorialService(); + expect(service.getCustomComponent('')).toBeUndefined(); + }); + test('returns undefined when custom component was not registered', () => { + const service = new TutorialService(); + expect(service.getCustomComponent('foo')).toBeUndefined(); + }); + test('returns custom component', async () => { + const service = new TutorialService(); + const customComponent =
foo
; + service.setup().registerCustomComponent('foo', async () => customComponent); + const customStatusCheckCallback = service.getCustomComponent('foo'); + expect(customStatusCheckCallback).toBeDefined(); + const result = await customStatusCheckCallback(); + expect(result).toEqual(customComponent); + }); + }); }); diff --git a/src/plugins/home/public/services/tutorials/tutorial_service.ts b/src/plugins/home/public/services/tutorials/tutorial_service.ts index 8ba766d34da53..839b0702a499e 100644 --- a/src/plugins/home/public/services/tutorials/tutorial_service.ts +++ b/src/plugins/home/public/services/tutorials/tutorial_service.ts @@ -22,6 +22,9 @@ export type TutorialModuleNoticeComponent = React.FC<{ moduleName: string; }>; +type CustomStatusCheckCallback = () => Promise; +type CustomComponent = () => Promise; + export class TutorialService { private tutorialVariables: TutorialVariables = {}; private tutorialDirectoryNotices: { [key: string]: TutorialDirectoryNoticeComponent } = {}; @@ -29,6 +32,8 @@ export class TutorialService { [key: string]: TutorialDirectoryHeaderLinkComponent; } = {}; private tutorialModuleNotices: { [key: string]: TutorialModuleNoticeComponent } = {}; + private customStatusCheck: Record = {}; + private customComponent: Record = {}; public setup() { return { @@ -74,6 +79,14 @@ export class TutorialService { } this.tutorialModuleNotices[id] = component; }, + + registerCustomStatusCheck: (name: string, fnCallback: CustomStatusCheckCallback) => { + this.customStatusCheck[name] = fnCallback; + }, + + registerCustomComponent: (name: string, component: CustomComponent) => { + this.customComponent[name] = component; + }, }; } @@ -92,6 +105,14 @@ export class TutorialService { public getModuleNotices() { return Object.values(this.tutorialModuleNotices); } + + public getCustomStatusCheck(customStatusCheckName: string) { + return this.customStatusCheck[customStatusCheckName]; + } + + public getCustomComponent(customComponentName: string) { + return this.customComponent[customComponentName]; + } } export type TutorialServiceSetup = ReturnType; diff --git a/src/plugins/home/server/index.ts b/src/plugins/home/server/index.ts index 840a5944a1343..9523766596fed 100644 --- a/src/plugins/home/server/index.ts +++ b/src/plugins/home/server/index.ts @@ -27,4 +27,9 @@ export const plugin = (initContext: PluginInitializerContext) => new HomeServerP export { INSTRUCTION_VARIANT } from '../common/instruction_variant'; export { TutorialsCategory } from './services/tutorials'; -export type { ArtifactsSchema } from './services/tutorials'; +export type { + ArtifactsSchema, + TutorialSchema, + InstructionSetSchema, + InstructionsSchema, +} from './services/tutorials'; diff --git a/src/plugins/home/server/services/tutorials/lib/tutorial_schema.ts b/src/plugins/home/server/services/tutorials/lib/tutorial_schema.ts index 5efbe067f6ece..76b045173a876 100644 --- a/src/plugins/home/server/services/tutorials/lib/tutorial_schema.ts +++ b/src/plugins/home/server/services/tutorials/lib/tutorial_schema.ts @@ -56,6 +56,7 @@ const instructionSchema = schema.object({ textPre: schema.maybe(schema.string()), commands: schema.maybe(schema.arrayOf(schema.string())), textPost: schema.maybe(schema.string()), + customComponentName: schema.maybe(schema.string()), }); export type Instruction = TypeOf; @@ -100,7 +101,7 @@ const instructionsSchema = schema.object({ instructionSets: schema.arrayOf(instructionSetSchema), params: schema.maybe(schema.arrayOf(paramSchema)), }); -export type InstructionsSchema = TypeOf; +export type InstructionsSchema = TypeOf; const tutorialIdRegExp = /^[a-zA-Z0-9-]+$/; export const tutorialSchema = schema.object({ @@ -152,6 +153,7 @@ export const tutorialSchema = schema.object({ // saved objects used by data module. savedObjects: schema.maybe(schema.arrayOf(schema.any())), savedObjectsInstallMsg: schema.maybe(schema.string()), + customStatusCheckName: schema.maybe(schema.string()), }); export type TutorialSchema = TypeOf; diff --git a/x-pack/plugins/apm/public/assets/illustration_integrations_darkmode.svg b/x-pack/plugins/apm/public/assets/illustration_integrations_darkmode.svg new file mode 100644 index 0000000000000..b1f86be19a080 --- /dev/null +++ b/x-pack/plugins/apm/public/assets/illustration_integrations_darkmode.svg @@ -0,0 +1 @@ +Kibana-integrations-darkmode \ No newline at end of file diff --git a/x-pack/plugins/apm/public/assets/illustration_integrations_lightmode.svg b/x-pack/plugins/apm/public/assets/illustration_integrations_lightmode.svg new file mode 100644 index 0000000000000..0cddcb0af6909 --- /dev/null +++ b/x-pack/plugins/apm/public/assets/illustration_integrations_lightmode.svg @@ -0,0 +1 @@ +Kibana-integrations-lightmode \ No newline at end of file diff --git a/x-pack/plugins/apm/public/plugin.ts b/x-pack/plugins/apm/public/plugin.ts index 77e7f2834b080..012856ca9213c 100644 --- a/x-pack/plugins/apm/public/plugin.ts +++ b/x-pack/plugins/apm/public/plugin.ts @@ -4,7 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - import { i18n } from '@kbn/i18n'; import { from } from 'rxjs'; import { map } from 'rxjs/operators'; @@ -140,16 +139,42 @@ export class ApmPlugin implements Plugin { ); const getApmDataHelper = async () => { - const { - fetchObservabilityOverviewPageData, - getHasData, - createCallApmApi, - } = await import('./services/rest/apm_observability_overview_fetchers'); + const { fetchObservabilityOverviewPageData, getHasData } = await import( + './services/rest/apm_observability_overview_fetchers' + ); + const { hasFleetApmIntegrations } = await import( + './tutorial/tutorial_apm_fleet_check' + ); + + const { createCallApmApi } = await import( + './services/rest/createCallApmApi' + ); + // have to do this here as well in case app isn't mounted yet createCallApmApi(core); - return { fetchObservabilityOverviewPageData, getHasData }; + return { + fetchObservabilityOverviewPageData, + getHasData, + hasFleetApmIntegrations, + }; }; + + // Registers a status check callback for the tutorial to call and verify if the APM integration is installed on fleet. + pluginSetupDeps.home?.tutorials.registerCustomStatusCheck( + 'apm_fleet_server_status_check', + async () => { + const { hasFleetApmIntegrations } = await getApmDataHelper(); + return hasFleetApmIntegrations(); + } + ); + + // Registers custom component that is going to be render on fleet section + pluginSetupDeps.home?.tutorials.registerCustomComponent( + 'TutorialFleetInstructions', + () => import('./tutorial/tutorial_fleet_instructions') + ); + plugins.observability.dashboard.register({ appName: 'apm', hasData: async () => { @@ -163,11 +188,12 @@ export class ApmPlugin implements Plugin { }); const getUxDataHelper = async () => { - const { - fetchUxOverviewDate, - hasRumData, - createCallApmApi, - } = await import('./components/app/RumDashboard/ux_overview_fetchers'); + const { fetchUxOverviewDate, hasRumData } = await import( + './components/app/RumDashboard/ux_overview_fetchers' + ); + const { createCallApmApi } = await import( + './services/rest/createCallApmApi' + ); // have to do this here as well in case app isn't mounted yet createCallApmApi(core); diff --git a/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.ts b/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.ts index ef61e25af4fc2..1b95c88a5fdc5 100644 --- a/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.ts +++ b/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.ts @@ -11,8 +11,6 @@ import { } from '../../../../observability/public'; import { callApmApi } from './createCallApmApi'; -export { createCallApmApi } from './createCallApmApi'; - export const fetchObservabilityOverviewPageData = async ({ absoluteTime, relativeTime, diff --git a/x-pack/plugins/apm/public/tutorial/tutorial_apm_fleet_check.ts b/x-pack/plugins/apm/public/tutorial/tutorial_apm_fleet_check.ts new file mode 100644 index 0000000000000..8db8614d606a9 --- /dev/null +++ b/x-pack/plugins/apm/public/tutorial/tutorial_apm_fleet_check.ts @@ -0,0 +1,20 @@ +/* + * 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 { callApmApi } from '../services/rest/createCallApmApi'; + +export async function hasFleetApmIntegrations() { + try { + const { hasData = false } = await callApmApi({ + endpoint: 'GET /api/apm/fleet/has_data', + signal: null, + }); + return hasData; + } catch (e) { + console.error('Something went wrong while fetching apm fleet data', e); + return false; + } +} diff --git a/x-pack/plugins/apm/public/tutorial/tutorial_fleet_instructions/index.tsx b/x-pack/plugins/apm/public/tutorial/tutorial_fleet_instructions/index.tsx new file mode 100644 index 0000000000000..8a81b7a994e76 --- /dev/null +++ b/x-pack/plugins/apm/public/tutorial/tutorial_fleet_instructions/index.tsx @@ -0,0 +1,122 @@ +/* + * 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 { EuiButton } from '@elastic/eui'; +import { EuiFlexItem } from '@elastic/eui'; +import { EuiFlexGroup } from '@elastic/eui'; +import { EuiPanel } from '@elastic/eui'; +import { EuiCard } from '@elastic/eui'; +import { EuiImage } from '@elastic/eui'; +import { EuiLoadingSpinner } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { HttpStart } from 'kibana/public'; +import React, { useEffect, useState } from 'react'; +import styled from 'styled-components'; +import { APIReturnType } from '../../services/rest/createCallApmApi'; + +interface Props { + http: HttpStart; + basePath: string; + isDarkTheme: boolean; +} + +const CentralizedContainer = styled.div` + display: flex; + justify-content: center; + align-items: center; +`; + +type APIResponseType = APIReturnType<'GET /api/apm/fleet/has_data'>; + +function TutorialFleetInstructions({ http, basePath, isDarkTheme }: Props) { + const [data, setData] = useState(); + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + async function fetchData() { + setIsLoading(true); + try { + const response = await http.get('/api/apm/fleet/has_data'); + setData(response as APIResponseType); + } catch (e) { + console.error('Error while fetching fleet details.', e); + } + setIsLoading(false); + } + fetchData(); + }, [http]); + + if (isLoading) { + return ( + + + + ); + } + + // When APM integration is enable in Fleet + if (data?.hasData) { + return ( + + {i18n.translate( + 'xpack.apm.tutorial.apmServer.fleet.manageApmIntegration.button', + { + defaultMessage: 'Manage APM integration in Fleet', + } + )} + + ); + } + // When APM integration is not installed in Fleet or for some reason the API didn't work out + return ( + + + + + {i18n.translate( + 'xpack.apm.tutorial.apmServer.fleet.apmIntegration.button', + { + defaultMessage: 'APM integration', + } + )} +
+ } + /> +
+ + + +
+ + ); +} +// eslint-disable-next-line import/no-default-export +export default TutorialFleetInstructions; diff --git a/x-pack/plugins/apm/server/routes/fleet.ts b/x-pack/plugins/apm/server/routes/fleet.ts new file mode 100644 index 0000000000000..74ca8dc368dad --- /dev/null +++ b/x-pack/plugins/apm/server/routes/fleet.ts @@ -0,0 +1,36 @@ +/* + * 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 Boom from '@hapi/boom'; +import { i18n } from '@kbn/i18n'; +import { getApmPackgePolicies } from '../lib/fleet/get_apm_package_policies'; +import { createApmServerRoute } from './create_apm_server_route'; +import { createApmServerRouteRepository } from './create_apm_server_route_repository'; + +const hasFleetDataRoute = createApmServerRoute({ + endpoint: 'GET /api/apm/fleet/has_data', + options: { tags: [] }, + handler: async ({ core, plugins }) => { + const fleetPluginStart = await plugins.fleet?.start(); + if (!fleetPluginStart) { + throw Boom.internal( + i18n.translate('xpack.apm.fleet_has_data.fleetRequired', { + defaultMessage: `Fleet plugin is required`, + }) + ); + } + const packagePolicies = await getApmPackgePolicies({ + core, + fleetPluginStart, + }); + return { hasData: packagePolicies.total > 0 }; + }, +}); + +export const ApmFleetRouteRepository = createApmServerRouteRepository().add( + hasFleetDataRoute +); diff --git a/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts b/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts index f1c08444d2e1e..fa2f80f073958 100644 --- a/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts +++ b/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts @@ -30,6 +30,7 @@ import { sourceMapsRouteRepository } from './source_maps'; import { traceRouteRepository } from './traces'; import { transactionRouteRepository } from './transactions'; import { APMRouteHandlerResources } from './typings'; +import { ApmFleetRouteRepository } from './fleet'; const getTypedGlobalApmServerRouteRepository = () => { const repository = createApmServerRouteRepository() @@ -50,7 +51,8 @@ const getTypedGlobalApmServerRouteRepository = () => { .merge(anomalyDetectionRouteRepository) .merge(apmIndicesRouteRepository) .merge(customLinkRouteRepository) - .merge(sourceMapsRouteRepository); + .merge(sourceMapsRouteRepository) + .merge(ApmFleetRouteRepository); return repository; }; diff --git a/x-pack/plugins/apm/server/tutorial/envs/elastic_cloud.ts b/x-pack/plugins/apm/server/tutorial/envs/elastic_cloud.ts index c6afd6a592fff..55adc756f31af 100644 --- a/x-pack/plugins/apm/server/tutorial/envs/elastic_cloud.ts +++ b/x-pack/plugins/apm/server/tutorial/envs/elastic_cloud.ts @@ -6,7 +6,11 @@ */ import { i18n } from '@kbn/i18n'; -import { INSTRUCTION_VARIANT } from '../../../../../../src/plugins/home/server'; +import { + INSTRUCTION_VARIANT, + TutorialSchema, + InstructionSetSchema, +} from '../../../../../../src/plugins/home/server'; import { createNodeAgentInstructions, @@ -22,7 +26,9 @@ import { } from '../instructions/apm_agent_instructions'; import { CloudSetup } from '../../../../cloud/server'; -export function createElasticCloudInstructions(cloudSetup?: CloudSetup) { +export function createElasticCloudInstructions( + cloudSetup?: CloudSetup +): TutorialSchema['elasticCloud'] { const apmServerUrl = cloudSetup?.apm.url; const instructionSets = []; @@ -37,7 +43,9 @@ export function createElasticCloudInstructions(cloudSetup?: CloudSetup) { }; } -function getApmServerInstructionSet(cloudSetup?: CloudSetup) { +function getApmServerInstructionSet( + cloudSetup?: CloudSetup +): InstructionSetSchema { const cloudId = cloudSetup?.cloudId; return { title: i18n.translate('xpack.apm.tutorial.apmServer.title', { @@ -61,7 +69,9 @@ function getApmServerInstructionSet(cloudSetup?: CloudSetup) { }; } -function getApmAgentInstructionSet(cloudSetup?: CloudSetup) { +function getApmAgentInstructionSet( + cloudSetup?: CloudSetup +): InstructionSetSchema { const apmServerUrl = cloudSetup?.apm.url; const secretToken = cloudSetup?.apm.secretToken; diff --git a/x-pack/plugins/apm/server/tutorial/envs/on_prem.ts b/x-pack/plugins/apm/server/tutorial/envs/on_prem.ts index a0e96f563381c..882d45c4c21db 100644 --- a/x-pack/plugins/apm/server/tutorial/envs/on_prem.ts +++ b/x-pack/plugins/apm/server/tutorial/envs/on_prem.ts @@ -6,28 +6,31 @@ */ import { i18n } from '@kbn/i18n'; -import { INSTRUCTION_VARIANT } from '../../../../../../src/plugins/home/server'; import { - createWindowsServerInstructions, - createEditConfig, - createStartServerUnixSysv, - createStartServerUnix, - createDownloadServerRpm, - createDownloadServerDeb, - createDownloadServerOsx, -} from '../instructions/apm_server_instructions'; + INSTRUCTION_VARIANT, + InstructionsSchema, +} from '../../../../../../src/plugins/home/server'; import { - createNodeAgentInstructions, createDjangoAgentInstructions, + createDotNetAgentInstructions, createFlaskAgentInstructions, - createRailsAgentInstructions, - createRackAgentInstructions, - createJsAgentInstructions, createGoAgentInstructions, createJavaAgentInstructions, - createDotNetAgentInstructions, + createJsAgentInstructions, + createNodeAgentInstructions, createPhpAgentInstructions, + createRackAgentInstructions, + createRailsAgentInstructions, } from '../instructions/apm_agent_instructions'; +import { + createDownloadServerDeb, + createDownloadServerOsx, + createDownloadServerRpm, + createEditConfig, + createStartServerUnix, + createStartServerUnixSysv, + createWindowsServerInstructions, +} from '../instructions/apm_server_instructions'; export function onPremInstructions({ errorIndices, @@ -41,7 +44,7 @@ export function onPremInstructions({ metricsIndices: string; sourcemapIndices: string; onboardingIndices: string; -}) { +}): InstructionsSchema { const EDIT_CONFIG = createEditConfig(); const START_SERVER_UNIX = createStartServerUnix(); const START_SERVER_UNIX_SYSV = createStartServerUnixSysv(); @@ -66,6 +69,12 @@ export function onPremInstructions({ iconType: 'alert', }, instructionVariants: [ + { + id: INSTRUCTION_VARIANT.FLEET, + instructions: [ + { customComponentName: 'TutorialFleetInstructions' }, + ], + }, { id: INSTRUCTION_VARIANT.OSX, instructions: [ diff --git a/x-pack/plugins/apm/server/tutorial/index.ts b/x-pack/plugins/apm/server/tutorial/index.ts index d678677a4b751..9118c30b845d0 100644 --- a/x-pack/plugins/apm/server/tutorial/index.ts +++ b/x-pack/plugins/apm/server/tutorial/index.ts @@ -6,15 +6,16 @@ */ import { i18n } from '@kbn/i18n'; -import { onPremInstructions } from './envs/on_prem'; -import { createElasticCloudInstructions } from './envs/elastic_cloud'; -import apmIndexPattern from './index_pattern.json'; -import { CloudSetup } from '../../../cloud/server'; import { ArtifactsSchema, TutorialsCategory, + TutorialSchema, } from '../../../../../src/plugins/home/server'; +import { CloudSetup } from '../../../cloud/server'; import { APM_STATIC_INDEX_PATTERN_ID } from '../../common/index_pattern_constants'; +import { createElasticCloudInstructions } from './envs/elastic_cloud'; +import { onPremInstructions } from './envs/on_prem'; +import apmIndexPattern from './index_pattern.json'; const apmIntro = i18n.translate('xpack.apm.tutorial.introduction', { defaultMessage: @@ -102,6 +103,7 @@ It allows you to monitor the performance of thousands of applications in real ti ), euiIconType: 'apmApp', artifacts, + customStatusCheckName: 'apm_fleet_server_status_check', onPrem: onPremInstructions(indices), elasticCloud: createElasticCloudInstructions(cloud), previewImagePath: '/plugins/apm/assets/apm.png', @@ -113,5 +115,5 @@ It allows you to monitor the performance of thousands of applications in real ti 'An APM index pattern is required for some features in the APM UI.', } ), - }; + } as TutorialSchema; }; diff --git a/x-pack/plugins/apm/server/tutorial/instructions/apm_agent_instructions.ts b/x-pack/plugins/apm/server/tutorial/instructions/apm_agent_instructions.ts index a25021fac5d00..ba11a996f00df 100644 --- a/x-pack/plugins/apm/server/tutorial/instructions/apm_agent_instructions.ts +++ b/x-pack/plugins/apm/server/tutorial/instructions/apm_agent_instructions.ts @@ -913,7 +913,10 @@ export const createPhpAgentInstructions = ( 'APM is automatically started when your app boots. Configure the agent either via `php.ini` file:', } ), - commands: `elastic_apm.server_url=http://localhost:8200 + commands: `elastic_apm.server_url="${ + apmServerUrl || 'http://localhost:8200' + }" +elastic.apm.secret_token="${secretToken}" elastic_apm.service_name="My service" `.split('\n'), textPost: i18n.translate( From 8a422fdbc203b0cc50e36a16547abe6cc3623834 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Thu, 24 Jun 2021 17:01:09 +0200 Subject: [PATCH 29/69] [Fleet] Use "Integrations" breadcrumb in "Add integrations" (#103227) * update the UI breadcrumbs in create policy package to always look like it came from integrations * added comment about the removal of a path from fleet Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../create_package_policy_page/index.tsx | 26 +++++-------------- .../fleet/public/constants/page_paths.ts | 1 + 2 files changed, 8 insertions(+), 19 deletions(-) diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx index b3b0d6ed51cb4..3fbaea67d8973 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx @@ -495,17 +495,13 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => { onCancel={() => setFormState('VALID')} /> )} - {from === 'package' - ? packageInfo && ( - - ) - : agentPolicy && ( - - )} + {packageInfo && ( + + )} @@ -559,14 +555,6 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => { ); }; -const PolicyBreadcrumb: React.FunctionComponent<{ - policyName: string; - policyId: string; -}> = ({ policyName, policyId }) => { - useBreadcrumbs('add_integration_from_policy', { policyName, policyId }); - return null; -}; - const IntegrationBreadcrumb: React.FunctionComponent<{ pkgTitle: string; pkgkey: string; diff --git a/x-pack/plugins/fleet/public/constants/page_paths.ts b/x-pack/plugins/fleet/public/constants/page_paths.ts index 1688a396cd5a1..317241358a381 100644 --- a/x-pack/plugins/fleet/public/constants/page_paths.ts +++ b/x-pack/plugins/fleet/public/constants/page_paths.ts @@ -54,6 +54,7 @@ export const FLEET_ROUTING_PATHS = { policy_details: '/policies/:policyId/:tabId?', policy_details_settings: '/policies/:policyId/settings', edit_integration: '/policies/:policyId/edit-integration/:packagePolicyId', + // TODO: Review uses and remove if it is no longer used or linked to in any UX flows add_integration_from_policy: '/policies/:policyId/add-integration', enrollment_tokens: '/enrollment-tokens', data_streams: '/data-streams', From 5a76c84dc9385ca00b6af4362401846f5f3e1de8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Thu, 24 Jun 2021 11:04:48 -0400 Subject: [PATCH 30/69] [APM] refactoring sourcemap api to receive form-data (#103152) --- .../apm/server/lib/fleet/source_maps.test.ts | 45 +++++++++++++++++-- .../apm/server/lib/fleet/source_maps.ts | 3 -- .../plugins/apm/server/routes/source_maps.ts | 30 ++++++++----- x-pack/plugins/apm/server/routes/typings.ts | 1 + 4 files changed, 61 insertions(+), 18 deletions(-) diff --git a/x-pack/plugins/apm/server/lib/fleet/source_maps.test.ts b/x-pack/plugins/apm/server/lib/fleet/source_maps.test.ts index 61a4fa4436e69..d6a1770a91591 100644 --- a/x-pack/plugins/apm/server/lib/fleet/source_maps.test.ts +++ b/x-pack/plugins/apm/server/lib/fleet/source_maps.test.ts @@ -103,12 +103,51 @@ const artifacts = [ describe('Source maps', () => { describe('getPackagePolicyWithSourceMap', () => { - it('returns unchanged package policy when artifacts is empty', () => { + it('removes source map from package policy', () => { + const packagePolicyWithSourceMaps = { + ...packagePolicy, + inputs: [ + { + ...packagePolicy.inputs[0], + compiled_input: { + 'apm-server': { + ...packagePolicy.inputs[0].compiled_input['apm-server'], + value: { + rum: { + source_mapping: { + metadata: [ + { + 'service.name': 'service_name', + 'service.version': '1.0.0', + 'bundle.filepath': + 'http://localhost:3000/static/js/main.chunk.js', + 'sourcemap.url': + '/api/fleet/artifacts/service_name-1.0.0/my-id-1', + }, + { + 'service.name': 'service_name', + 'service.version': '2.0.0', + 'bundle.filepath': + 'http://localhost:3000/static/js/main.chunk.js', + 'sourcemap.url': + '/api/fleet/artifacts/service_name-2.0.0/my-id-2', + }, + ], + }, + }, + }, + }, + }, + }, + ], + }; const updatedPackagePolicy = getPackagePolicyWithSourceMap({ - packagePolicy, + packagePolicy: packagePolicyWithSourceMaps, artifacts: [], }); - expect(updatedPackagePolicy).toEqual(packagePolicy); + expect(updatedPackagePolicy.inputs[0].config).toEqual({ + 'apm-server': { value: { rum: { source_mapping: { metadata: [] } } } }, + }); }); it('adds source maps into the package policy', () => { const updatedPackagePolicy = getPackagePolicyWithSourceMap({ diff --git a/x-pack/plugins/apm/server/lib/fleet/source_maps.ts b/x-pack/plugins/apm/server/lib/fleet/source_maps.ts index b313fbad2806f..6d608f7751f3b 100644 --- a/x-pack/plugins/apm/server/lib/fleet/source_maps.ts +++ b/x-pack/plugins/apm/server/lib/fleet/source_maps.ts @@ -97,9 +97,6 @@ export function getPackagePolicyWithSourceMap({ packagePolicy: PackagePolicy; artifacts: ArtifactSourceMap[]; }) { - if (!artifacts.length) { - return packagePolicy; - } const [firstInput, ...restInputs] = packagePolicy.inputs; return { ...packagePolicy, diff --git a/x-pack/plugins/apm/server/routes/source_maps.ts b/x-pack/plugins/apm/server/routes/source_maps.ts index 24ea825774b0a..f6d160e68a76a 100644 --- a/x-pack/plugins/apm/server/routes/source_maps.ts +++ b/x-pack/plugins/apm/server/routes/source_maps.ts @@ -5,6 +5,7 @@ * 2.0. */ import Boom from '@hapi/boom'; +import { jsonRt } from '@kbn/io-ts-utils'; import * as t from 'io-ts'; import { SavedObjectsClientContract } from 'kibana/server'; import { @@ -34,7 +35,7 @@ export const sourceMapRt = t.intersection([ const listSourceMapRoute = createApmServerRoute({ endpoint: 'GET /api/apm/sourcemaps', options: { tags: ['access:apm'] }, - handler: async ({ plugins, logger }) => { + handler: async ({ plugins }) => { try { const fleetPluginStart = await plugins.fleet?.start(); if (fleetPluginStart) { @@ -51,21 +52,26 @@ const listSourceMapRoute = createApmServerRoute({ }); const uploadSourceMapRoute = createApmServerRoute({ - endpoint: 'POST /api/apm/sourcemaps/{serviceName}/{serviceVersion}', - options: { tags: ['access:apm', 'access:apm_write'] }, + endpoint: 'POST /api/apm/sourcemaps', + options: { + tags: ['access:apm', 'access:apm_write'], + body: { accepts: ['multipart/form-data'] }, + }, params: t.type({ - path: t.type({ - serviceName: t.string, - serviceVersion: t.string, - }), body: t.type({ - bundleFilepath: t.string, - sourceMap: sourceMapRt, + service_name: t.string, + service_version: t.string, + bundle_filepath: t.string, + sourcemap: jsonRt.pipe(sourceMapRt), }), }), handler: async ({ params, plugins, core }) => { - const { serviceName, serviceVersion } = params.path; - const { bundleFilepath, sourceMap } = params.body; + const { + service_name: serviceName, + service_version: serviceVersion, + bundle_filepath: bundleFilepath, + sourcemap: sourceMap, + } = params.body; const fleetPluginStart = await plugins.fleet?.start(); const coreStart = await core.start(); const esClient = coreStart.elasticsearch.client.asInternalUser; @@ -107,7 +113,7 @@ const deleteSourceMapRoute = createApmServerRoute({ id: t.string, }), }), - handler: async ({ context, params, plugins, core }) => { + handler: async ({ params, plugins, core }) => { const fleetPluginStart = await plugins.fleet?.start(); const { id } = params.path; const coreStart = await core.start(); diff --git a/x-pack/plugins/apm/server/routes/typings.ts b/x-pack/plugins/apm/server/routes/typings.ts index 13bd631085aac..474464dec1f99 100644 --- a/x-pack/plugins/apm/server/routes/typings.ts +++ b/x-pack/plugins/apm/server/routes/typings.ts @@ -39,6 +39,7 @@ export interface APMRouteCreateOptions { | 'access:ml:canGetJobs' | 'access:ml:canCreateJob' >; + body?: { accepts: Array<'application/json' | 'multipart/form-data'> }; }; } From dd072c392786b8f7c49c16ee48fc95a0aba2be6e Mon Sep 17 00:00:00 2001 From: Chris Roberson Date: Thu, 24 Jun 2021 11:09:47 -0400 Subject: [PATCH 31/69] [Task Manager] Add config switch around logging at different levels based on the state (#102804) * Gate behind a config with warning message that helps users enable * Update more files * Fix docs formatting * Preserve existing functionality * Add in task type to the message * Show multiple alert types that are over the threshold Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- docs/settings/task-manager-settings.asciidoc | 8 +- .../resources/base/bin/kibana-docker | 3 +- .../task_manager/server/config.test.ts | 15 +- x-pack/plugins/task_manager/server/config.ts | 9 +- .../managed_configuration.test.ts | 5 +- .../server/lib/log_health_metrics.test.ts | 191 +++++++++++++++++- .../server/lib/log_health_metrics.ts | 73 +++++-- .../configuration_statistics.test.ts | 5 +- .../monitoring_stats_stream.test.ts | 5 +- .../server/monitoring/task_run_statistics.ts | 3 + .../task_manager/server/plugin.test.ts | 10 +- .../server/polling_lifecycle.test.ts | 5 +- .../task_manager/server/routes/health.test.ts | 15 +- 13 files changed, 305 insertions(+), 42 deletions(-) diff --git a/docs/settings/task-manager-settings.asciidoc b/docs/settings/task-manager-settings.asciidoc index 87f5b700870eb..7f4dbb3a96e6b 100644 --- a/docs/settings/task-manager-settings.asciidoc +++ b/docs/settings/task-manager-settings.asciidoc @@ -29,7 +29,13 @@ Task Manager runs background tasks by polling for work on an interval. You can | The maximum number of tasks that this Kibana instance will run simultaneously. Defaults to 10. Starting in 8.0, it will not be possible to set the value greater than 100. - | `xpack.task_manager.monitored_stats_warn_delayed_task_start_in_seconds` + | `xpack.task_manager.` + `monitored_stats_health_verbose_log.enabled` + | This flag will enable automatic warn and error logging if task manager self detects a performance issue, such as the time between when a task is scheduled to execute and when it actually executes. Defaults to false. + + | `xpack.task_manager.` + `monitored_stats_health_verbose_log.` + `warn_delayed_task_start_in_seconds` | The amount of seconds we allow a task to delay before printing a warning server log. Defaults to 60. |=== diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker index d109a824ca81d..b520ab3070b15 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker @@ -379,7 +379,8 @@ kibana_vars=( xpack.task_manager.monitored_aggregated_stats_refresh_rate xpack.task_manager.monitored_stats_required_freshness xpack.task_manager.monitored_stats_running_average_window - xpack.task_manager.monitored_stats_warn_delayed_task_start_in_seconds + xpack.task_manager.monitored_stats_health_verbose_log.enabled + xpack.task_manager.monitored_stats_health_verbose_log.warn_delayed_task_start_in_seconds xpack.task_manager.monitored_task_execution_thresholds xpack.task_manager.poll_interval xpack.task_manager.request_capacity diff --git a/x-pack/plugins/task_manager/server/config.test.ts b/x-pack/plugins/task_manager/server/config.test.ts index 947b1fd84467e..5e44181f35b20 100644 --- a/x-pack/plugins/task_manager/server/config.test.ts +++ b/x-pack/plugins/task_manager/server/config.test.ts @@ -18,9 +18,12 @@ describe('config validation', () => { "max_poll_inactivity_cycles": 10, "max_workers": 10, "monitored_aggregated_stats_refresh_rate": 60000, + "monitored_stats_health_verbose_log": Object { + "enabled": false, + "warn_delayed_task_start_in_seconds": 60, + }, "monitored_stats_required_freshness": 4000, "monitored_stats_running_average_window": 50, - "monitored_stats_warn_delayed_task_start_in_seconds": 60, "monitored_task_execution_thresholds": Object { "custom": Object {}, "default": Object { @@ -67,9 +70,12 @@ describe('config validation', () => { "max_poll_inactivity_cycles": 10, "max_workers": 10, "monitored_aggregated_stats_refresh_rate": 60000, + "monitored_stats_health_verbose_log": Object { + "enabled": false, + "warn_delayed_task_start_in_seconds": 60, + }, "monitored_stats_required_freshness": 4000, "monitored_stats_running_average_window": 50, - "monitored_stats_warn_delayed_task_start_in_seconds": 60, "monitored_task_execution_thresholds": Object { "custom": Object {}, "default": Object { @@ -103,9 +109,12 @@ describe('config validation', () => { "max_poll_inactivity_cycles": 10, "max_workers": 10, "monitored_aggregated_stats_refresh_rate": 60000, + "monitored_stats_health_verbose_log": Object { + "enabled": false, + "warn_delayed_task_start_in_seconds": 60, + }, "monitored_stats_required_freshness": 4000, "monitored_stats_running_average_window": 50, - "monitored_stats_warn_delayed_task_start_in_seconds": 60, "monitored_task_execution_thresholds": Object { "custom": Object { "alerting:always-fires": Object { diff --git a/x-pack/plugins/task_manager/server/config.ts b/x-pack/plugins/task_manager/server/config.ts index 5dee66cf113b2..03bb98170a34a 100644 --- a/x-pack/plugins/task_manager/server/config.ts +++ b/x-pack/plugins/task_manager/server/config.ts @@ -110,9 +110,12 @@ export const configSchema = schema.object( defaultValue: {}, }), }), - /* The amount of seconds we allow a task to delay before printing a warning server log */ - monitored_stats_warn_delayed_task_start_in_seconds: schema.number({ - defaultValue: DEFAULT_MONITORING_STATS_WARN_DELAYED_TASK_START_IN_SECONDS, + monitored_stats_health_verbose_log: schema.object({ + enabled: schema.boolean({ defaultValue: false }), + /* The amount of seconds we allow a task to delay before printing a warning server log */ + warn_delayed_task_start_in_seconds: schema.number({ + defaultValue: DEFAULT_MONITORING_STATS_WARN_DELAYED_TASK_START_IN_SECONDS, + }), }), }, { diff --git a/x-pack/plugins/task_manager/server/integration_tests/managed_configuration.test.ts b/x-pack/plugins/task_manager/server/integration_tests/managed_configuration.test.ts index f6ee8d8a78ddc..f925c4d978ad7 100644 --- a/x-pack/plugins/task_manager/server/integration_tests/managed_configuration.test.ts +++ b/x-pack/plugins/task_manager/server/integration_tests/managed_configuration.test.ts @@ -37,7 +37,10 @@ describe('managed configuration', () => { version_conflict_threshold: 80, max_poll_inactivity_cycles: 10, monitored_aggregated_stats_refresh_rate: 60000, - monitored_stats_warn_delayed_task_start_in_seconds: 60, + monitored_stats_health_verbose_log: { + enabled: false, + warn_delayed_task_start_in_seconds: 60, + }, monitored_stats_required_freshness: 4000, monitored_stats_running_average_window: 50, request_capacity: 1000, diff --git a/x-pack/plugins/task_manager/server/lib/log_health_metrics.test.ts b/x-pack/plugins/task_manager/server/lib/log_health_metrics.test.ts index ccbbf81ebfa31..f5163f4ca5ed8 100644 --- a/x-pack/plugins/task_manager/server/lib/log_health_metrics.test.ts +++ b/x-pack/plugins/task_manager/server/lib/log_health_metrics.test.ts @@ -10,7 +10,7 @@ import { configSchema, TaskManagerConfig } from '../config'; import { HealthStatus } from '../monitoring'; import { TaskPersistence } from '../monitoring/task_run_statistics'; import { MonitoredHealth } from '../routes/health'; -import { logHealthMetrics } from './log_health_metrics'; +import { logHealthMetrics, resetLastLogLevel } from './log_health_metrics'; import { Logger } from '../../../../../src/core/server'; jest.mock('./calculate_health_status', () => ({ @@ -20,12 +20,110 @@ jest.mock('./calculate_health_status', () => ({ describe('logHealthMetrics', () => { afterEach(() => { const { calculateHealthStatus } = jest.requireMock('./calculate_health_status'); + // Reset the last state by running through this as OK + // (calculateHealthStatus as jest.Mock).mockImplementation(() => HealthStatus.OK); + resetLastLogLevel(); (calculateHealthStatus as jest.Mock).mockReset(); }); + + it('should log a warning message to enable verbose logging when the status goes from OK to Warning/Error', () => { + const logger = loggingSystemMock.create().get(); + const config = getTaskManagerConfig({ + monitored_stats_health_verbose_log: { + enabled: false, + warn_delayed_task_start_in_seconds: 60, + }, + }); + const health = getMockMonitoredHealth(); + const { calculateHealthStatus } = jest.requireMock('./calculate_health_status'); + + // We must change from OK to Warning + (calculateHealthStatus as jest.Mock).mockImplementation(() => HealthStatus.OK); + logHealthMetrics(health, logger, config); + (calculateHealthStatus as jest.Mock).mockImplementation( + () => HealthStatus.Warning + ); + logHealthMetrics(health, logger, config); + // We must change from OK to Error + (calculateHealthStatus as jest.Mock).mockImplementation(() => HealthStatus.OK); + logHealthMetrics(health, logger, config); + (calculateHealthStatus as jest.Mock).mockImplementation(() => HealthStatus.Error); + logHealthMetrics(health, logger, config); + + expect((logger as jest.Mocked).warn.mock.calls[0][0] as string).toBe( + `Detected potential performance issue with Task Manager. Set 'xpack.task_manager.monitored_stats_health_verbose_log.enabled: true' in your Kibana.yml to enable debug logging` + ); + expect((logger as jest.Mocked).warn.mock.calls[1][0] as string).toBe( + `Detected potential performance issue with Task Manager. Set 'xpack.task_manager.monitored_stats_health_verbose_log.enabled: true' in your Kibana.yml to enable debug logging` + ); + }); + + it('should not log a warning message to enable verbose logging when the status goes from Warning to OK', () => { + const logger = loggingSystemMock.create().get(); + const config = getTaskManagerConfig({ + monitored_stats_health_verbose_log: { + enabled: false, + warn_delayed_task_start_in_seconds: 60, + }, + }); + const health = getMockMonitoredHealth(); + const { calculateHealthStatus } = jest.requireMock('./calculate_health_status'); + + // We must change from Warning to OK + (calculateHealthStatus as jest.Mock).mockImplementation( + () => HealthStatus.Warning + ); + logHealthMetrics(health, logger, config); + (calculateHealthStatus as jest.Mock).mockImplementation(() => HealthStatus.OK); + logHealthMetrics(health, logger, config); + expect((logger as jest.Mocked).warn).not.toHaveBeenCalled(); + }); + + it('should not log a warning message to enable verbose logging when the status goes from Error to OK', () => { + // console.log('start', getLastLogLevel()); + const logger = loggingSystemMock.create().get(); + const config = getTaskManagerConfig({ + monitored_stats_health_verbose_log: { + enabled: false, + warn_delayed_task_start_in_seconds: 60, + }, + }); + const health = getMockMonitoredHealth(); + const { calculateHealthStatus } = jest.requireMock('./calculate_health_status'); + + // We must change from Error to OK + (calculateHealthStatus as jest.Mock).mockImplementation(() => HealthStatus.Error); + logHealthMetrics(health, logger, config); + (calculateHealthStatus as jest.Mock).mockImplementation(() => HealthStatus.OK); + logHealthMetrics(health, logger, config); + expect((logger as jest.Mocked).warn).not.toHaveBeenCalled(); + }); + it('should log as debug if status is OK', () => { const logger = loggingSystemMock.create().get(); const config = getTaskManagerConfig({ - monitored_stats_warn_delayed_task_start_in_seconds: 60, + monitored_stats_health_verbose_log: { + enabled: true, + warn_delayed_task_start_in_seconds: 60, + }, + }); + const health = getMockMonitoredHealth(); + + logHealthMetrics(health, logger, config); + + const firstDebug = JSON.parse( + (logger as jest.Mocked).debug.mock.calls[0][0].replace('Latest Monitored Stats: ', '') + ); + expect(firstDebug).toMatchObject(health); + }); + + it('should log as debug if status is OK even if not enabled', () => { + const logger = loggingSystemMock.create().get(); + const config = getTaskManagerConfig({ + monitored_stats_health_verbose_log: { + enabled: false, + warn_delayed_task_start_in_seconds: 60, + }, }); const health = getMockMonitoredHealth(); @@ -40,7 +138,10 @@ describe('logHealthMetrics', () => { it('should log as warn if status is Warn', () => { const logger = loggingSystemMock.create().get(); const config = getTaskManagerConfig({ - monitored_stats_warn_delayed_task_start_in_seconds: 60, + monitored_stats_health_verbose_log: { + enabled: true, + warn_delayed_task_start_in_seconds: 60, + }, }); const health = getMockMonitoredHealth(); const { calculateHealthStatus } = jest.requireMock('./calculate_health_status'); @@ -62,7 +163,10 @@ describe('logHealthMetrics', () => { it('should log as error if status is Error', () => { const logger = loggingSystemMock.create().get(); const config = getTaskManagerConfig({ - monitored_stats_warn_delayed_task_start_in_seconds: 60, + monitored_stats_health_verbose_log: { + enabled: true, + warn_delayed_task_start_in_seconds: 60, + }, }); const health = getMockMonitoredHealth(); const { calculateHealthStatus } = jest.requireMock('./calculate_health_status'); @@ -79,15 +183,26 @@ describe('logHealthMetrics', () => { expect(logMessage).toMatchObject(health); }); - it('should log as warn if drift exceeds the threshold', () => { + it('should log as warn if drift exceeds the threshold for a single alert type', () => { const logger = loggingSystemMock.create().get(); const config = getTaskManagerConfig({ - monitored_stats_warn_delayed_task_start_in_seconds: 60, + monitored_stats_health_verbose_log: { + enabled: true, + warn_delayed_task_start_in_seconds: 60, + }, }); const health = getMockMonitoredHealth({ stats: { runtime: { value: { + drift_by_type: { + 'taskType:test': { + p99: 60000, + }, + 'taskType:test2': { + p99: 60000 - 1, + }, + }, drift: { p99: 60000, }, @@ -99,7 +214,50 @@ describe('logHealthMetrics', () => { logHealthMetrics(health, logger, config); expect((logger as jest.Mocked).warn.mock.calls[0][0] as string).toBe( - `Detected delay task start of 60s (which exceeds configured value of 60s)` + `Detected delay task start of 60s for task(s) \"taskType:test\" (which exceeds configured value of 60s)` + ); + + const secondMessage = JSON.parse( + ((logger as jest.Mocked).warn.mock.calls[1][0] as string).replace( + `Latest Monitored Stats: `, + '' + ) + ); + expect(secondMessage).toMatchObject(health); + }); + + it('should log as warn if drift exceeds the threshold for multiple alert types', () => { + const logger = loggingSystemMock.create().get(); + const config = getTaskManagerConfig({ + monitored_stats_health_verbose_log: { + enabled: true, + warn_delayed_task_start_in_seconds: 60, + }, + }); + const health = getMockMonitoredHealth({ + stats: { + runtime: { + value: { + drift_by_type: { + 'taskType:test': { + p99: 60000, + }, + 'taskType:test2': { + p99: 60000, + }, + }, + drift: { + p99: 60000, + }, + }, + }, + }, + }); + + logHealthMetrics(health, logger, config); + + expect((logger as jest.Mocked).warn.mock.calls[0][0] as string).toBe( + `Detected delay task start of 60s for task(s) \"taskType:test, taskType:test2\" (which exceeds configured value of 60s)` ); const secondMessage = JSON.parse( @@ -114,7 +272,10 @@ describe('logHealthMetrics', () => { it('should log as debug if there are no stats', () => { const logger = loggingSystemMock.create().get(); const config = getTaskManagerConfig({ - monitored_stats_warn_delayed_task_start_in_seconds: 60, + monitored_stats_health_verbose_log: { + enabled: true, + warn_delayed_task_start_in_seconds: 60, + }, }); const health = { id: '1', @@ -135,7 +296,10 @@ describe('logHealthMetrics', () => { it('should ignore capacity estimation status', () => { const logger = loggingSystemMock.create().get(); const config = getTaskManagerConfig({ - monitored_stats_warn_delayed_task_start_in_seconds: 60, + monitored_stats_health_verbose_log: { + enabled: true, + warn_delayed_task_start_in_seconds: 60, + }, }); const health = getMockMonitoredHealth({ stats: { @@ -213,7 +377,14 @@ function getMockMonitoredHealth(overrides = {}): MonitoredHealth { p95: 2500, p99: 3000, }, - drift_by_type: {}, + drift_by_type: { + 'taskType:test': { + p50: 1000, + p90: 2000, + p95: 2500, + p99: 3000, + }, + }, load: { p50: 1000, p90: 2000, diff --git a/x-pack/plugins/task_manager/server/lib/log_health_metrics.ts b/x-pack/plugins/task_manager/server/lib/log_health_metrics.ts index 1c98b3272a82d..e8511b1e8c71d 100644 --- a/x-pack/plugins/task_manager/server/lib/log_health_metrics.ts +++ b/x-pack/plugins/task_manager/server/lib/log_health_metrics.ts @@ -12,11 +12,23 @@ import { TaskManagerConfig } from '../config'; import { MonitoredHealth } from '../routes/health'; import { calculateHealthStatus } from './calculate_health_status'; +enum LogLevel { + Warn = 'warn', + Error = 'error', + Debug = 'debug', +} + +let lastLogLevel: LogLevel | null = null; +export function resetLastLogLevel() { + lastLogLevel = null; +} export function logHealthMetrics( monitoredHealth: MonitoredHealth, logger: Logger, config: TaskManagerConfig ) { + let logLevel: LogLevel = LogLevel.Debug; + const enabled = config.monitored_stats_health_verbose_log.enabled; const healthWithoutCapacity: MonitoredHealth = { ...monitoredHealth, stats: { @@ -25,23 +37,54 @@ export function logHealthMetrics( }, }; const statusWithoutCapacity = calculateHealthStatus(healthWithoutCapacity, config); - let logAsWarn = statusWithoutCapacity === HealthStatus.Warning; - const logAsError = - statusWithoutCapacity === HealthStatus.Error && !isEmpty(monitoredHealth.stats); - const driftInSeconds = (monitoredHealth.stats.runtime?.value.drift.p99 ?? 0) / 1000; - - if (driftInSeconds >= config.monitored_stats_warn_delayed_task_start_in_seconds) { - logger.warn( - `Detected delay task start of ${driftInSeconds}s (which exceeds configured value of ${config.monitored_stats_warn_delayed_task_start_in_seconds}s)` - ); - logAsWarn = true; + if (statusWithoutCapacity === HealthStatus.Warning) { + logLevel = LogLevel.Warn; + } else if (statusWithoutCapacity === HealthStatus.Error && !isEmpty(monitoredHealth.stats)) { + logLevel = LogLevel.Error; } - if (logAsError) { - logger.error(`Latest Monitored Stats: ${JSON.stringify(monitoredHealth)}`); - } else if (logAsWarn) { - logger.warn(`Latest Monitored Stats: ${JSON.stringify(monitoredHealth)}`); + const message = `Latest Monitored Stats: ${JSON.stringify(monitoredHealth)}`; + if (enabled) { + const driftInSeconds = (monitoredHealth.stats.runtime?.value.drift.p99 ?? 0) / 1000; + if ( + driftInSeconds >= config.monitored_stats_health_verbose_log.warn_delayed_task_start_in_seconds + ) { + const taskTypes = Object.keys(monitoredHealth.stats.runtime?.value.drift_by_type ?? {}) + .reduce((accum: string[], typeName) => { + if ( + monitoredHealth.stats.runtime?.value.drift_by_type[typeName].p99 === + monitoredHealth.stats.runtime?.value.drift.p99 + ) { + accum.push(typeName); + } + return accum; + }, []) + .join(', '); + + logger.warn( + `Detected delay task start of ${driftInSeconds}s for task(s) "${taskTypes}" (which exceeds configured value of ${config.monitored_stats_health_verbose_log.warn_delayed_task_start_in_seconds}s)` + ); + logLevel = LogLevel.Warn; + } + switch (logLevel) { + case LogLevel.Warn: + logger.warn(message); + break; + case LogLevel.Error: + logger.error(message); + break; + default: + logger.debug(message); + } } else { - logger.debug(`Latest Monitored Stats: ${JSON.stringify(monitoredHealth)}`); + // This is legacy support - we used to always show this + logger.debug(message); + if (logLevel !== LogLevel.Debug && lastLogLevel === LogLevel.Debug) { + logger.warn( + `Detected potential performance issue with Task Manager. Set 'xpack.task_manager.monitored_stats_health_verbose_log.enabled: true' in your Kibana.yml to enable debug logging` + ); + } } + + lastLogLevel = logLevel; } diff --git a/x-pack/plugins/task_manager/server/monitoring/configuration_statistics.test.ts b/x-pack/plugins/task_manager/server/monitoring/configuration_statistics.test.ts index 39a7658fb09e4..6aa8bad5717ec 100644 --- a/x-pack/plugins/task_manager/server/monitoring/configuration_statistics.test.ts +++ b/x-pack/plugins/task_manager/server/monitoring/configuration_statistics.test.ts @@ -23,7 +23,10 @@ describe('Configuration Statistics Aggregator', () => { max_poll_inactivity_cycles: 10, request_capacity: 1000, monitored_aggregated_stats_refresh_rate: 5000, - monitored_stats_warn_delayed_task_start_in_seconds: 60, + monitored_stats_health_verbose_log: { + enabled: false, + warn_delayed_task_start_in_seconds: 60, + }, monitored_stats_running_average_window: 50, monitored_task_execution_thresholds: { default: { diff --git a/x-pack/plugins/task_manager/server/monitoring/monitoring_stats_stream.test.ts b/x-pack/plugins/task_manager/server/monitoring/monitoring_stats_stream.test.ts index 01bd86ec96db6..2e53850814e83 100644 --- a/x-pack/plugins/task_manager/server/monitoring/monitoring_stats_stream.test.ts +++ b/x-pack/plugins/task_manager/server/monitoring/monitoring_stats_stream.test.ts @@ -27,7 +27,10 @@ describe('createMonitoringStatsStream', () => { max_poll_inactivity_cycles: 10, request_capacity: 1000, monitored_aggregated_stats_refresh_rate: 5000, - monitored_stats_warn_delayed_task_start_in_seconds: 60, + monitored_stats_health_verbose_log: { + enabled: false, + warn_delayed_task_start_in_seconds: 60, + }, monitored_stats_running_average_window: 50, monitored_task_execution_thresholds: { default: { diff --git a/x-pack/plugins/task_manager/server/monitoring/task_run_statistics.ts b/x-pack/plugins/task_manager/server/monitoring/task_run_statistics.ts index b792f4ca475f9..da86cfad2a911 100644 --- a/x-pack/plugins/task_manager/server/monitoring/task_run_statistics.ts +++ b/x-pack/plugins/task_manager/server/monitoring/task_run_statistics.ts @@ -103,6 +103,9 @@ type ResultFrequencySummary = ResultFrequency & { export interface SummarizedTaskRunStat extends JsonObject { drift: AveragedStat; + drift_by_type: { + [alertType: string]: AveragedStat; + }; load: AveragedStat; execution: { duration: Record; diff --git a/x-pack/plugins/task_manager/server/plugin.test.ts b/x-pack/plugins/task_manager/server/plugin.test.ts index 6c7f722d4c525..0d9f285164f10 100644 --- a/x-pack/plugins/task_manager/server/plugin.test.ts +++ b/x-pack/plugins/task_manager/server/plugin.test.ts @@ -25,7 +25,10 @@ describe('TaskManagerPlugin', () => { max_poll_inactivity_cycles: 10, request_capacity: 1000, monitored_aggregated_stats_refresh_rate: 5000, - monitored_stats_warn_delayed_task_start_in_seconds: 60, + monitored_stats_health_verbose_log: { + enabled: false, + warn_delayed_task_start_in_seconds: 60, + }, monitored_stats_required_freshness: 5000, monitored_stats_running_average_window: 50, monitored_task_execution_thresholds: { @@ -56,7 +59,10 @@ describe('TaskManagerPlugin', () => { max_poll_inactivity_cycles: 10, request_capacity: 1000, monitored_aggregated_stats_refresh_rate: 5000, - monitored_stats_warn_delayed_task_start_in_seconds: 60, + monitored_stats_health_verbose_log: { + enabled: false, + warn_delayed_task_start_in_seconds: 60, + }, monitored_stats_required_freshness: 5000, monitored_stats_running_average_window: 50, monitored_task_execution_thresholds: { diff --git a/x-pack/plugins/task_manager/server/polling_lifecycle.test.ts b/x-pack/plugins/task_manager/server/polling_lifecycle.test.ts index 66c6805e9160e..73b892c9f59e0 100644 --- a/x-pack/plugins/task_manager/server/polling_lifecycle.test.ts +++ b/x-pack/plugins/task_manager/server/polling_lifecycle.test.ts @@ -45,7 +45,10 @@ describe('TaskPollingLifecycle', () => { max_poll_inactivity_cycles: 10, request_capacity: 1000, monitored_aggregated_stats_refresh_rate: 5000, - monitored_stats_warn_delayed_task_start_in_seconds: 60, + monitored_stats_health_verbose_log: { + enabled: false, + warn_delayed_task_start_in_seconds: 60, + }, monitored_stats_required_freshness: 5000, monitored_stats_running_average_window: 50, monitored_task_execution_thresholds: { diff --git a/x-pack/plugins/task_manager/server/routes/health.test.ts b/x-pack/plugins/task_manager/server/routes/health.test.ts index c14eb7e10b726..735029e90c2d3 100644 --- a/x-pack/plugins/task_manager/server/routes/health.test.ts +++ b/x-pack/plugins/task_manager/server/routes/health.test.ts @@ -67,7 +67,10 @@ describe('healthRoute', () => { id, getTaskManagerConfig({ monitored_stats_required_freshness: 1000, - monitored_stats_warn_delayed_task_start_in_seconds: 100, + monitored_stats_health_verbose_log: { + enabled: true, + warn_delayed_task_start_in_seconds: 100, + }, monitored_aggregated_stats_refresh_rate: 60000, }) ); @@ -114,7 +117,10 @@ describe('healthRoute', () => { id, getTaskManagerConfig({ monitored_stats_required_freshness: 1000, - monitored_stats_warn_delayed_task_start_in_seconds: 120, + monitored_stats_health_verbose_log: { + enabled: true, + warn_delayed_task_start_in_seconds: 120, + }, monitored_aggregated_stats_refresh_rate: 60000, }) ); @@ -173,7 +179,10 @@ describe('healthRoute', () => { id, getTaskManagerConfig({ monitored_stats_required_freshness: 1000, - monitored_stats_warn_delayed_task_start_in_seconds: 120, + monitored_stats_health_verbose_log: { + enabled: true, + warn_delayed_task_start_in_seconds: 120, + }, monitored_aggregated_stats_refresh_rate: 60000, }) ); From 686ac904d913f485e656e5b1f3604bfeb518240d Mon Sep 17 00:00:00 2001 From: Dmitry Tomashevich <39378793+Dmitriynj@users.noreply.github.com> Date: Thu, 24 Jun 2021 18:27:29 +0300 Subject: [PATCH 32/69] [Discover] Move focus on chart toggle in Discover (#103119) * [Discover] move focus on show chart * [Discover] set actual moveFocus flag --- .../main/components/chart/discover_chart.tsx | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/src/plugins/discover/public/application/apps/main/components/chart/discover_chart.tsx b/src/plugins/discover/public/application/apps/main/components/chart/discover_chart.tsx index 210313aac5366..f1967d5b10b3e 100644 --- a/src/plugins/discover/public/application/apps/main/components/chart/discover_chart.tsx +++ b/src/plugins/discover/public/application/apps/main/components/chart/discover_chart.tsx @@ -5,7 +5,7 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import React, { useCallback } from 'react'; +import React, { useCallback, useEffect, useRef } from 'react'; import moment from 'moment'; import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, EuiSpacer } from '@elastic/eui'; import { IUiSettingsClient } from 'kibana/public'; @@ -47,8 +47,21 @@ export function DiscoverChart({ stateContainer: GetStateReturn; timefield?: string; }) { + const chartRef = useRef<{ element: HTMLElement | null; moveFocus: boolean }>({ + element: null, + moveFocus: false, + }); + + useEffect(() => { + if (chartRef.current.moveFocus && chartRef.current.element) { + chartRef.current.element.focus(); + } + }, [state.hideChart]); + const toggleHideChart = useCallback(() => { - stateContainer.setAppState({ hideChart: !state.hideChart }); + const newHideChart = !state.hideChart; + stateContainer.setAppState({ hideChart: newHideChart }); + chartRef.current.moveFocus = !newHideChart; }, [state, stateContainer]); const onChangeInterval = useCallback( @@ -102,9 +115,7 @@ export function DiscoverChart({ { - toggleHideChart(); - }} + onClick={toggleHideChart} data-test-subj="discoverChartToggle" > {!state.hideChart @@ -122,6 +133,8 @@ export function DiscoverChart({ {!state.hideChart && chartData && (
(chartRef.current.element = element)} + tabIndex={-1} aria-label={i18n.translate('discover.histogramOfFoundDocumentsAriaLabel', { defaultMessage: 'Histogram of found documents', })} From f2ebcadc7f526c75e87c32dce365acc6539745ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yulia=20=C4=8Cech?= <6585477+yuliacech@users.noreply.github.com> Date: Thu, 24 Jun 2021 17:51:05 +0200 Subject: [PATCH 33/69] Refactored helpers file into separate domain files (#102383) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../edit_policy/edit_policy.helpers.tsx | 96 ------------------- .../edit_policy/features/cold_phase.test.ts | 42 -------- .../features/delete_phase.helpers.ts | 36 +++++++ .../edit_policy/features/delete_phase.test.ts | 59 +++++------- .../edit_policy/features/frozen_phase.test.ts | 18 +--- .../general_behavior.helpers.ts | 8 +- .../features/request_flyout.test.ts | 7 +- .../edit_policy/features/rollover.helpers.ts | 51 ++++++++++ .../edit_policy/features/rollover.test.ts | 17 ++-- .../features/searchable_snapshots.helpers.ts | 59 ++++++++++++ .../features/searchable_snapshots.test.ts | 25 +++-- .../edit_policy/features/timeline.helpers.ts | 29 ++++++ .../edit_policy/features/timeline.test.ts | 6 +- .../edit_policy/features/timing.helpers.ts | 36 +++++++ .../edit_policy/features/timing.test.ts | 41 ++++++++ .../edit_policy/features/warm_phase.test.ts | 42 -------- .../cold_phase_validation.test.ts | 6 +- .../form_validation/error_indicators.test.ts | 6 +- .../hot_phase_validation.test.ts | 10 +- .../policy_name_validation.test.ts | 10 +- .../form_validation/timing.test.ts | 8 +- .../form_validation/validation.helpers.ts | 47 +++++++++ .../warm_phase_validation.test.ts | 8 +- .../policy_serialization.helpers.ts | 45 +++++++++ .../policy_serialization.test.ts | 25 +++-- .../helpers/actions/errors_actions.ts | 10 +- .../helpers/actions/forcemerge_actions.ts | 2 +- ...ts => form_toggle_and_set_value_action.ts} | 15 +-- .../helpers/actions/index.ts | 12 ++- .../helpers/actions/index_priority_actions.ts | 8 +- .../helpers/actions/phases.ts | 76 +++++++++++++++ .../helpers/actions/replicas_action.ts | 20 ++++ .../helpers/actions/rollover_actions.ts | 16 ++-- .../actions/set_wait_for_snapshot_action.ts | 19 ---- .../helpers/actions/shrink_actions.ts | 10 +- .../actions/snapshot_policy_actions.ts | 27 ++++++ 36 files changed, 611 insertions(+), 341 deletions(-) delete mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx delete mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/cold_phase.test.ts create mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/delete_phase.helpers.ts create mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/rollover.helpers.ts create mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/searchable_snapshots.helpers.ts create mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/timeline.helpers.ts create mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/timing.helpers.ts create mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/timing.test.ts delete mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/warm_phase.test.ts create mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/validation.helpers.ts create mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/serialization/policy_serialization.helpers.ts rename x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/{set_replicas_action.ts => form_toggle_and_set_value_action.ts} (57%) create mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/phases.ts create mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/replicas_action.ts delete mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/set_wait_for_snapshot_action.ts create mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/snapshot_policy_actions.ts diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx deleted file mode 100644 index 6f1c58b2e9b18..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { TestBedConfig } from '@kbn/test/jest'; -import { AppServicesContext } from '../../../public/types'; - -import { Phase } from '../../../common/types'; - -import { - createNodeAllocationActions, - createFormToggleAction, - createFormSetValueAction, - setReplicas, - createSearchableSnapshotActions, - createTogglePhaseAction, - createSavePolicyAction, - createErrorsActions, - createRolloverActions, - createSetWaitForSnapshotAction, - createMinAgeActions, - createShrinkActions, - createFreezeActions, - createForceMergeActions, - createReadonlyActions, - createIndexPriorityActions, -} from '../helpers'; -import { initTestBed } from './init_test_bed'; - -type SetupReturn = ReturnType; -export type EditPolicyTestBed = SetupReturn extends Promise ? U : SetupReturn; - -export const setup = async (arg?: { - appServicesContext?: Partial; - testBedConfig?: Partial; -}) => { - const testBed = await initTestBed(arg); - - const { exists } = testBed; - - return { - ...testBed, - actions: { - togglePhase: createTogglePhaseAction(testBed), - savePolicy: createSavePolicyAction(testBed), - toggleSaveAsNewPolicy: createFormToggleAction(testBed, 'saveAsNewSwitch'), - setPolicyName: createFormSetValueAction(testBed, 'policyNameField'), - errors: { - ...createErrorsActions(testBed), - }, - timeline: { - hasPhase: (phase: Phase) => exists(`ilmTimelinePhase-${phase}`), - }, - rollover: { - ...createRolloverActions(testBed), - }, - hot: { - ...createForceMergeActions(testBed, 'hot'), - ...createIndexPriorityActions(testBed, 'hot'), - ...createShrinkActions(testBed, 'hot'), - ...createReadonlyActions(testBed, 'hot'), - ...createSearchableSnapshotActions(testBed, 'hot'), - }, - warm: { - ...createMinAgeActions(testBed, 'warm'), - setReplicas: (value: string) => setReplicas(testBed, 'warm', value), - ...createShrinkActions(testBed, 'warm'), - ...createForceMergeActions(testBed, 'warm'), - ...createReadonlyActions(testBed, 'warm'), - ...createIndexPriorityActions(testBed, 'warm'), - ...createNodeAllocationActions(testBed, 'warm'), - }, - cold: { - ...createMinAgeActions(testBed, 'cold'), - setReplicas: (value: string) => setReplicas(testBed, 'cold', value), - ...createFreezeActions(testBed, 'cold'), - ...createReadonlyActions(testBed, 'cold'), - ...createIndexPriorityActions(testBed, 'cold'), - ...createSearchableSnapshotActions(testBed, 'cold'), - ...createNodeAllocationActions(testBed, 'cold'), - }, - frozen: { - ...createMinAgeActions(testBed, 'frozen'), - ...createSearchableSnapshotActions(testBed, 'frozen'), - }, - delete: { - isShown: () => exists('delete-phase'), - ...createMinAgeActions(testBed, 'delete'), - setWaitForSnapshotPolicy: createSetWaitForSnapshotAction(testBed), - }, - }, - }; -}; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/cold_phase.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/cold_phase.test.ts deleted file mode 100644 index e5e4267b6270c..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/cold_phase.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { act } from 'react-dom/test-utils'; -import { setupEnvironment } from '../../helpers/setup_environment'; -import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; - -describe(' cold phase', () => { - let testBed: EditPolicyTestBed; - const { server, httpRequestsMockHelpers } = setupEnvironment(); - - beforeAll(() => { - jest.useFakeTimers(); - }); - - afterAll(() => { - jest.useRealTimers(); - server.restore(); - }); - - beforeEach(async () => { - httpRequestsMockHelpers.setDefaultResponses(); - - await act(async () => { - testBed = await setup(); - }); - - const { component } = testBed; - component.update(); - }); - - test('shows timing only when enabled', async () => { - const { actions } = testBed; - expect(actions.cold.hasMinAgeInput()).toBeFalsy(); - await actions.togglePhase('cold'); - expect(actions.cold.hasMinAgeInput()).toBeTruthy(); - }); -}); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/delete_phase.helpers.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/delete_phase.helpers.ts new file mode 100644 index 0000000000000..1914a056528e1 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/delete_phase.helpers.ts @@ -0,0 +1,36 @@ +/* + * 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 { + createMinAgeActions, + createSavePolicyAction, + createSnapshotPolicyActions, + createTogglePhaseAction, +} from '../../helpers'; +import { initTestBed } from '../init_test_bed'; + +type SetupReturn = ReturnType; + +export type DeleteTestBed = SetupReturn extends Promise ? U : SetupReturn; + +export const setupDeleteTestBed = async () => { + const testBed = await initTestBed(); + const { exists } = testBed; + + return { + ...testBed, + actions: { + togglePhase: createTogglePhaseAction(testBed), + savePolicy: createSavePolicyAction(testBed), + delete: { + isShown: () => exists('delete-phase'), + ...createMinAgeActions(testBed, 'delete'), + ...createSnapshotPolicyActions(testBed), + }, + }, + }; +}; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/delete_phase.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/delete_phase.test.ts index 0c24101461f24..6ba41860eb855 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/delete_phase.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/delete_phase.test.ts @@ -8,16 +8,16 @@ import { act } from 'react-dom/test-utils'; import { API_BASE_PATH } from '../../../../common/constants'; import { setupEnvironment } from '../../helpers'; -import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; import { DELETE_PHASE_POLICY, getDefaultHotPhasePolicy, NEW_SNAPSHOT_POLICY_NAME, SNAPSHOT_POLICY_NAME, } from '../constants'; +import { DeleteTestBed, setupDeleteTestBed } from './delete_phase.helpers'; describe(' delete phase', () => { - let testBed: EditPolicyTestBed; + let testBed: DeleteTestBed; const { server, httpRequestsMockHelpers } = setupEnvironment(); afterAll(() => { @@ -32,7 +32,7 @@ describe(' delete phase', () => { ]); await act(async () => { - testBed = await setup(); + testBed = await setupDeleteTestBed(); }); const { component } = testBed; @@ -43,7 +43,7 @@ describe(' delete phase', () => { httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]); await act(async () => { - testBed = await setup(); + testBed = await setupDeleteTestBed(); }); const { component, actions } = testBed; @@ -56,21 +56,6 @@ describe(' delete phase', () => { expect(actions.delete.isShown()).toBeFalsy(); }); - test('shows timing after it was enabled', async () => { - httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]); - - await act(async () => { - testBed = await setup(); - }); - - const { component, actions } = testBed; - component.update(); - - expect(actions.delete.hasMinAgeInput()).toBeFalsy(); - await actions.togglePhase('delete'); - expect(actions.delete.hasMinAgeInput()).toBeTruthy(); - }); - describe('wait for snapshot', () => { test('shows snapshot policy name', () => { expect(testBed.find('snapshotPolicyCombobox').prop('data-currentvalue')).toEqual([ @@ -83,7 +68,7 @@ describe(' delete phase', () => { test('updates snapshot policy name', async () => { const { actions } = testBed; - await actions.delete.setWaitForSnapshotPolicy(NEW_SNAPSHOT_POLICY_NAME); + await actions.delete.setSnapshotPolicy(NEW_SNAPSHOT_POLICY_NAME); await actions.savePolicy(); const expected = { @@ -111,16 +96,16 @@ describe(' delete phase', () => { test('shows a callout when the input is not an existing policy', async () => { const { actions } = testBed; - await actions.delete.setWaitForSnapshotPolicy('my_custom_policy'); - expect(testBed.find('noPoliciesCallout').exists()).toBeFalsy(); - expect(testBed.find('policiesErrorCallout').exists()).toBeFalsy(); - expect(testBed.find('customPolicyCallout').exists()).toBeTruthy(); + await actions.delete.setSnapshotPolicy('my_custom_policy'); + expect(actions.delete.hasNoPoliciesCallout()).toBeFalsy(); + expect(actions.delete.hasPolicyErrorCallout()).toBeFalsy(); + expect(actions.delete.hasCustomPolicyCallout()).toBeTruthy(); }); test('removes the action if field is empty', async () => { const { actions } = testBed; - await actions.delete.setWaitForSnapshotPolicy(''); + await actions.delete.setSnapshotPolicy(''); await actions.savePolicy(); const expected = { @@ -146,26 +131,30 @@ describe(' delete phase', () => { // need to call setup on testBed again for it to use a newly defined snapshot policies response httpRequestsMockHelpers.setLoadSnapshotPolicies([]); await act(async () => { - testBed = await setup(); + testBed = await setupDeleteTestBed(); }); - testBed.component.update(); - expect(testBed.find('customPolicyCallout').exists()).toBeFalsy(); - expect(testBed.find('policiesErrorCallout').exists()).toBeFalsy(); - expect(testBed.find('noPoliciesCallout').exists()).toBeTruthy(); + const { component, actions } = testBed; + component.update(); + + expect(actions.delete.hasCustomPolicyCallout()).toBeFalsy(); + expect(actions.delete.hasPolicyErrorCallout()).toBeFalsy(); + expect(actions.delete.hasNoPoliciesCallout()).toBeTruthy(); }); test('shows a callout when there is an error loading snapshot policies', async () => { // need to call setup on testBed again for it to use a newly defined snapshot policies response httpRequestsMockHelpers.setLoadSnapshotPolicies([], { status: 500, body: 'error' }); await act(async () => { - testBed = await setup(); + testBed = await setupDeleteTestBed(); }); - testBed.component.update(); - expect(testBed.find('customPolicyCallout').exists()).toBeFalsy(); - expect(testBed.find('noPoliciesCallout').exists()).toBeFalsy(); - expect(testBed.find('policiesErrorCallout').exists()).toBeTruthy(); + const { component, actions } = testBed; + component.update(); + + expect(actions.delete.hasCustomPolicyCallout()).toBeFalsy(); + expect(actions.delete.hasNoPoliciesCallout()).toBeFalsy(); + expect(actions.delete.hasPolicyErrorCallout()).toBeTruthy(); }); }); }); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/frozen_phase.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/frozen_phase.test.ts index 982377e2a0365..aaa2b3dafddde 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/frozen_phase.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/frozen_phase.test.ts @@ -6,13 +6,14 @@ */ import { act } from 'react-dom/test-utils'; +import { TestBed } from '@kbn/test/jest'; import { licensingMock } from '../../../../../licensing/public/mocks'; import { setupEnvironment } from '../../helpers'; -import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; +import { initTestBed } from '../init_test_bed'; describe(' frozen phase', () => { - let testBed: EditPolicyTestBed; + let testBed: TestBed; const { server, httpRequestsMockHelpers } = setupEnvironment(); beforeAll(() => { @@ -28,28 +29,19 @@ describe(' frozen phase', () => { httpRequestsMockHelpers.setDefaultResponses(); await act(async () => { - testBed = await setup(); + testBed = await initTestBed(); }); const { component } = testBed; component.update(); }); - test('shows timing only when enabled', async () => { - const { actions, exists } = testBed; - - expect(exists('frozen-phase')).toBe(true); - expect(actions.frozen.hasMinAgeInput()).toBeFalsy(); - await actions.togglePhase('frozen'); - expect(actions.frozen.hasMinAgeInput()).toBeTruthy(); - }); - describe('on non-enterprise license', () => { beforeEach(async () => { httpRequestsMockHelpers.setDefaultResponses(); await act(async () => { - testBed = await setup({ + testBed = await initTestBed({ appServicesContext: { license: licensingMock.createLicense({ license: { type: 'basic' } }), }, diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/general_behavior.helpers.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/general_behavior.helpers.ts index 6b6db2da5946d..384478bcf4c66 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/general_behavior.helpers.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/general_behavior.helpers.ts @@ -5,7 +5,11 @@ * 2.0. */ -import { createNodeAllocationActions, createSavePolicyAction, setReplicas } from '../../../helpers'; +import { + createNodeAllocationActions, + createReplicasAction, + createSavePolicyAction, +} from '../../../helpers'; import { initTestBed } from '../../init_test_bed'; type SetupReturn = ReturnType; @@ -20,7 +24,7 @@ export const setupGeneralNodeAllocation = async () => { actions: { ...createNodeAllocationActions(testBed, 'warm'), savePolicy: createSavePolicyAction(testBed), - setReplicas: (value: string) => setReplicas(testBed, 'warm', value), + ...createReplicasAction(testBed, 'warm'), }, }; }; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/request_flyout.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/request_flyout.test.ts index 1cb895e9ac86a..02a700519cb05 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/request_flyout.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/request_flyout.test.ts @@ -6,11 +6,12 @@ */ import { act } from 'react-dom/test-utils'; +import { TestBed } from '@kbn/test/jest'; import { setupEnvironment } from '../../helpers'; -import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; +import { initTestBed } from '../init_test_bed'; describe(' request flyout', () => { - let testBed: EditPolicyTestBed; + let testBed: TestBed; const { server, httpRequestsMockHelpers } = setupEnvironment(); beforeAll(() => { @@ -26,7 +27,7 @@ describe(' request flyout', () => { httpRequestsMockHelpers.setDefaultResponses(); await act(async () => { - testBed = await setup(); + testBed = await initTestBed(); }); const { component } = testBed; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/rollover.helpers.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/rollover.helpers.ts new file mode 100644 index 0000000000000..b15e956c84b4c --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/rollover.helpers.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 { + createForceMergeActions, + createMinAgeActions, + createReadonlyActions, + createRolloverActions, + createSearchableSnapshotActions, + createShrinkActions, + createTogglePhaseAction, +} from '../../helpers'; +import { initTestBed } from '../init_test_bed'; + +type SetupReturn = ReturnType; + +export type RolloverTestBed = SetupReturn extends Promise ? U : SetupReturn; + +export const setupRolloverTestBed = async () => { + const testBed = await initTestBed(); + + return { + ...testBed, + actions: { + togglePhase: createTogglePhaseAction(testBed), + ...createRolloverActions(testBed), + hot: { + ...createForceMergeActions(testBed, 'hot'), + ...createShrinkActions(testBed, 'hot'), + ...createReadonlyActions(testBed, 'hot'), + ...createSearchableSnapshotActions(testBed, 'hot'), + }, + warm: { + ...createMinAgeActions(testBed, 'warm'), + }, + cold: { + ...createMinAgeActions(testBed, 'cold'), + }, + frozen: { + ...createMinAgeActions(testBed, 'frozen'), + }, + delete: { + ...createMinAgeActions(testBed, 'delete'), + }, + }, + }; +}; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/rollover.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/rollover.test.ts index 8e9586e52577b..432b07efca038 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/rollover.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/rollover.test.ts @@ -6,12 +6,11 @@ */ import { act } from 'react-dom/test-utils'; -import { licensingMock } from '../../../../../licensing/public/mocks'; import { setupEnvironment } from '../../helpers'; -import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; +import { RolloverTestBed, setupRolloverTestBed } from './rollover.helpers'; describe(' rollover', () => { - let testBed: EditPolicyTestBed; + let testBed: RolloverTestBed; const { server, httpRequestsMockHelpers } = setupEnvironment(); afterAll(() => { @@ -22,11 +21,7 @@ describe(' rollover', () => { httpRequestsMockHelpers.setDefaultResponses(); await act(async () => { - testBed = await setup({ - appServicesContext: { - license: licensingMock.createLicense({ license: { type: 'enterprise' } }), - }, - }); + testBed = await setupRolloverTestBed(); }); const { component } = testBed; @@ -35,14 +30,14 @@ describe(' rollover', () => { test('shows forcemerge when rollover enabled', async () => { const { actions } = testBed; - expect(actions.hot.forceMergeFieldExists()).toBeTruthy(); + expect(actions.hot.forceMergeExists()).toBeTruthy(); }); test('hides forcemerge when rollover is disabled', async () => { const { actions } = testBed; await actions.rollover.toggleDefault(); await actions.rollover.toggle(); - expect(actions.hot.forceMergeFieldExists()).toBeFalsy(); + expect(actions.hot.forceMergeExists()).toBeFalsy(); }); test('shows shrink input when rollover enabled', async () => { @@ -71,6 +66,8 @@ describe(' rollover', () => { test('hides and disables searchable snapshot field', async () => { const { actions } = testBed; + + expect(actions.hot.searchableSnapshotsExists()).toBeTruthy(); await actions.rollover.toggleDefault(); await actions.rollover.toggle(); await actions.togglePhase('cold'); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/searchable_snapshots.helpers.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/searchable_snapshots.helpers.ts new file mode 100644 index 0000000000000..cdb5dc16d1964 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/searchable_snapshots.helpers.ts @@ -0,0 +1,59 @@ +/* + * 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 { + createForceMergeActions, + createFreezeActions, + createMinAgeActions, + createReadonlyActions, + createRolloverActions, + createSavePolicyAction, + createSearchableSnapshotActions, + createShrinkActions, + createTogglePhaseAction, +} from '../../helpers'; +import { initTestBed } from '../init_test_bed'; +import { AppServicesContext } from '../../../../public/types'; + +type SetupReturn = ReturnType; + +export type SearchableSnapshotsTestBed = SetupReturn extends Promise ? U : SetupReturn; + +export const setupSearchableSnapshotsTestBed = async (args?: { + appServicesContext?: Partial; +}) => { + const testBed = await initTestBed(args); + + return { + ...testBed, + actions: { + togglePhase: createTogglePhaseAction(testBed), + savePolicy: createSavePolicyAction(testBed), + ...createRolloverActions(testBed), + hot: { + ...createSearchableSnapshotActions(testBed, 'hot'), + ...createForceMergeActions(testBed, 'hot'), + ...createShrinkActions(testBed, 'hot'), + }, + warm: { + ...createForceMergeActions(testBed, 'warm'), + ...createShrinkActions(testBed, 'warm'), + ...createReadonlyActions(testBed, 'warm'), + }, + cold: { + ...createMinAgeActions(testBed, 'cold'), + ...createSearchableSnapshotActions(testBed, 'cold'), + ...createFreezeActions(testBed, 'cold'), + ...createReadonlyActions(testBed, 'cold'), + }, + frozen: { + ...createMinAgeActions(testBed, 'frozen'), + ...createSearchableSnapshotActions(testBed, 'frozen'), + }, + }, + }; +}; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/searchable_snapshots.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/searchable_snapshots.test.ts index d400966cdae38..66f42d5482fdb 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/searchable_snapshots.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/searchable_snapshots.test.ts @@ -9,10 +9,13 @@ import { act } from 'react-dom/test-utils'; import { licensingMock } from '../../../../../licensing/public/mocks'; import { setupEnvironment } from '../../helpers'; import { getDefaultHotPhasePolicy } from '../constants'; -import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; +import { + SearchableSnapshotsTestBed, + setupSearchableSnapshotsTestBed, +} from './searchable_snapshots.helpers'; describe(' searchable snapshots', () => { - let testBed: EditPolicyTestBed; + let testBed: SearchableSnapshotsTestBed; const { server, httpRequestsMockHelpers } = setupEnvironment(); afterAll(() => { @@ -23,7 +26,7 @@ describe(' searchable snapshots', () => { httpRequestsMockHelpers.setDefaultResponses(); await act(async () => { - testBed = await setup(); + testBed = await setupSearchableSnapshotsTestBed(); }); const { component } = testBed; @@ -36,7 +39,7 @@ describe(' searchable snapshots', () => { await actions.togglePhase('warm'); await actions.togglePhase('cold'); - expect(actions.warm.forceMergeFieldExists()).toBeTruthy(); + expect(actions.warm.forceMergeExists()).toBeTruthy(); expect(actions.warm.shrinkExists()).toBeTruthy(); expect(actions.warm.readonlyExists()).toBeTruthy(); expect(actions.cold.searchableSnapshotsExists()).toBeTruthy(); @@ -45,7 +48,7 @@ describe(' searchable snapshots', () => { await actions.hot.setSearchableSnapshot('my-repo'); - expect(actions.warm.forceMergeFieldExists()).toBeFalsy(); + expect(actions.warm.forceMergeExists()).toBeFalsy(); expect(actions.warm.shrinkExists()).toBeFalsy(); expect(actions.warm.readonlyExists()).toBeFalsy(); // searchable snapshot in cold is still visible @@ -60,7 +63,7 @@ describe(' searchable snapshots', () => { await actions.rollover.toggle(); await actions.rollover.toggleDefault(); - expect(actions.hot.forceMergeFieldExists()).toBeTruthy(); + expect(actions.hot.forceMergeExists()).toBeTruthy(); expect(actions.hot.shrinkExists()).toBeTruthy(); expect(actions.hot.searchableSnapshotsExists()).toBeTruthy(); }); @@ -122,7 +125,9 @@ describe(' searchable snapshots', () => { httpRequestsMockHelpers.setListSnapshotRepos({ repositories: ['found-snapshots'] }); await act(async () => { - testBed = await setup({ appServicesContext: { cloud: { isCloudEnabled: true } } }); + testBed = await setupSearchableSnapshotsTestBed({ + appServicesContext: { cloud: { isCloudEnabled: true } }, + }); }); const { component } = testBed; @@ -149,7 +154,9 @@ describe(' searchable snapshots', () => { httpRequestsMockHelpers.setListSnapshotRepos({ repositories: ['found-snapshots'] }); await act(async () => { - testBed = await setup({ appServicesContext: { cloud: { isCloudEnabled: true } } }); + testBed = await setupSearchableSnapshotsTestBed({ + appServicesContext: { cloud: { isCloudEnabled: true } }, + }); }); const { component } = testBed; @@ -184,7 +191,7 @@ describe(' searchable snapshots', () => { httpRequestsMockHelpers.setListSnapshotRepos({ repositories: ['my-repo'] }); await act(async () => { - testBed = await setup({ + testBed = await setupSearchableSnapshotsTestBed({ appServicesContext: { license: licensingMock.createLicense({ license: { type: 'basic' } }), }, diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/timeline.helpers.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/timeline.helpers.ts new file mode 100644 index 0000000000000..8303fdbac4837 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/timeline.helpers.ts @@ -0,0 +1,29 @@ +/* + * 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 { createTogglePhaseAction } from '../../helpers'; +import { initTestBed } from '../init_test_bed'; +import { Phase } from '../../../../common/types'; + +type SetupReturn = ReturnType; + +export type TimelineTestBed = SetupReturn extends Promise ? U : SetupReturn; + +export const setupTimelineTestBed = async () => { + const testBed = await initTestBed(); + const { exists } = testBed; + + return { + ...testBed, + actions: { + togglePhase: createTogglePhaseAction(testBed), + timeline: { + hasPhase: (phase: Phase) => exists(`ilmTimelinePhase-${phase}`), + }, + }, + }; +}; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/timeline.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/timeline.test.ts index a4f2a24bcee8b..33aeb80b38cae 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/timeline.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/timeline.test.ts @@ -7,10 +7,10 @@ import { act } from 'react-dom/test-utils'; import { setupEnvironment } from '../../helpers'; -import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; +import { setupTimelineTestBed, TimelineTestBed } from './timeline.helpers'; describe(' timeline', () => { - let testBed: EditPolicyTestBed; + let testBed: TimelineTestBed; const { server, httpRequestsMockHelpers } = setupEnvironment(); afterAll(() => { @@ -21,7 +21,7 @@ describe(' timeline', () => { httpRequestsMockHelpers.setDefaultResponses(); await act(async () => { - testBed = await setup(); + testBed = await setupTimelineTestBed(); }); const { component } = testBed; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/timing.helpers.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/timing.helpers.ts new file mode 100644 index 0000000000000..57d6f53a21c78 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/timing.helpers.ts @@ -0,0 +1,36 @@ +/* + * 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 { createMinAgeActions, createTogglePhaseAction } from '../../helpers'; +import { initTestBed } from '../init_test_bed'; + +type SetupReturn = ReturnType; + +export type TimingTestBed = SetupReturn extends Promise ? U : SetupReturn; + +export const setupTimingTestBed = async () => { + const testBed = await initTestBed(); + + return { + ...testBed, + actions: { + togglePhase: createTogglePhaseAction(testBed), + warm: { + ...createMinAgeActions(testBed, 'warm'), + }, + cold: { + ...createMinAgeActions(testBed, 'cold'), + }, + frozen: { + ...createMinAgeActions(testBed, 'frozen'), + }, + delete: { + ...createMinAgeActions(testBed, 'delete'), + }, + }, + }; +}; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/timing.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/timing.test.ts new file mode 100644 index 0000000000000..985ee807ed827 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/timing.test.ts @@ -0,0 +1,41 @@ +/* + * 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 { act } from 'react-dom/test-utils'; +import { setupEnvironment } from '../../helpers'; +import { setupTimingTestBed, TimingTestBed } from './timing.helpers'; +import { PhaseWithTiming } from '../../../../common/types'; + +describe(' timing', () => { + let testBed: TimingTestBed; + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + afterAll(() => { + server.restore(); + }); + + beforeEach(async () => { + httpRequestsMockHelpers.setDefaultResponses(); + + await act(async () => { + testBed = await setupTimingTestBed(); + }); + + const { component } = testBed; + component.update(); + }); + + ['warm', 'cold', 'frozen', 'delete'].forEach((phase: string) => { + test(`timing is only shown when ${phase} phase is enabled`, async () => { + const { actions } = testBed; + const phaseWithTiming = phase as PhaseWithTiming; + expect(actions[phaseWithTiming].hasMinAgeInput()).toBeFalsy(); + await actions.togglePhase(phaseWithTiming); + expect(actions[phaseWithTiming].hasMinAgeInput()).toBeTruthy(); + }); + }); +}); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/warm_phase.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/warm_phase.test.ts deleted file mode 100644 index ae9f306483820..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/warm_phase.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { act } from 'react-dom/test-utils'; -import { setupEnvironment } from '../../helpers'; -import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; - -describe(' warm phase', () => { - let testBed: EditPolicyTestBed; - const { server, httpRequestsMockHelpers } = setupEnvironment(); - - beforeAll(() => { - jest.useFakeTimers(); - }); - - afterAll(() => { - jest.useRealTimers(); - server.restore(); - }); - - beforeEach(async () => { - httpRequestsMockHelpers.setDefaultResponses(); - - await act(async () => { - testBed = await setup(); - }); - - const { component } = testBed; - component.update(); - }); - - test('shows timing only when enabled', async () => { - const { actions } = testBed; - expect(actions.warm.hasMinAgeInput()).toBeFalsy(); - await actions.togglePhase('warm'); - expect(actions.warm.hasMinAgeInput()).toBeTruthy(); - }); -}); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/cold_phase_validation.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/cold_phase_validation.test.ts index 9a14571c6ec3b..4725631e6f894 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/cold_phase_validation.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/cold_phase_validation.test.ts @@ -8,10 +8,10 @@ import { act } from 'react-dom/test-utils'; import { i18nTexts } from '../../../../public/application/sections/edit_policy/i18n_texts'; import { setupEnvironment } from '../../helpers'; -import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; +import { setupValidationTestBed, ValidationTestBed } from './validation.helpers'; describe(' cold phase validation', () => { - let testBed: EditPolicyTestBed; + let testBed: ValidationTestBed; const { server, httpRequestsMockHelpers } = setupEnvironment(); beforeAll(() => { @@ -35,7 +35,7 @@ describe(' cold phase validation', () => { ]); await act(async () => { - testBed = await setup(); + testBed = await setupValidationTestBed(); }); const { component, actions } = testBed; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/error_indicators.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/error_indicators.test.ts index 0a047714bd345..1464f683ef766 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/error_indicators.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/error_indicators.test.ts @@ -7,10 +7,10 @@ import { act } from 'react-dom/test-utils'; import { setupEnvironment } from '../../helpers'; -import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; +import { setupValidationTestBed, ValidationTestBed } from './validation.helpers'; describe(' error indicators', () => { - let testBed: EditPolicyTestBed; + let testBed: ValidationTestBed; const { server, httpRequestsMockHelpers } = setupEnvironment(); beforeAll(() => { @@ -26,7 +26,7 @@ describe(' error indicators', () => { httpRequestsMockHelpers.setDefaultResponses(); await act(async () => { - testBed = await setup(); + testBed = await setupValidationTestBed(); }); const { component } = testBed; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/hot_phase_validation.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/hot_phase_validation.test.ts index 296b128eb8f52..6cbc28ec161f2 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/hot_phase_validation.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/hot_phase_validation.test.ts @@ -8,11 +8,11 @@ import { act } from 'react-dom/test-utils'; import { i18nTexts } from '../../../../public/application/sections/edit_policy/i18n_texts'; import { setupEnvironment } from '../../helpers'; -import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; +import { setupValidationTestBed, ValidationTestBed } from './validation.helpers'; describe(' hot phase validation', () => { - let testBed: EditPolicyTestBed; - let actions: EditPolicyTestBed['actions']; + let testBed: ValidationTestBed; + let actions: ValidationTestBed['actions']; const { server, httpRequestsMockHelpers } = setupEnvironment(); beforeAll(() => { @@ -27,7 +27,7 @@ describe(' hot phase validation', () => { beforeEach(async () => { httpRequestsMockHelpers.setLoadPolicies([]); await act(async () => { - testBed = await setup(); + testBed = await setupValidationTestBed(); }); const { component } = testBed; @@ -159,13 +159,11 @@ describe(' hot phase validation', () => { describe('shrink', () => { test(`doesn't allow 0 for shrink`, async () => { - await actions.hot.toggleShrink(); await actions.hot.setShrink('0'); actions.errors.waitForValidation(); actions.errors.expectMessages([i18nTexts.editPolicy.errors.numberGreatThan0Required]); }); test(`doesn't allow -1 for shrink`, async () => { - await actions.hot.toggleShrink(); await actions.hot.setShrink('-1'); actions.errors.waitForValidation(); actions.errors.expectMessages([i18nTexts.editPolicy.errors.numberGreatThan0Required]); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/policy_name_validation.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/policy_name_validation.test.ts index 08b794466da49..799fbf89d47df 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/policy_name_validation.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/policy_name_validation.test.ts @@ -8,12 +8,12 @@ import { act } from 'react-dom/test-utils'; import { i18nTexts } from '../../../../public/application/sections/edit_policy/i18n_texts'; import { setupEnvironment } from '../../helpers'; -import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; import { getGeneratedPolicies } from '../constants'; +import { setupValidationTestBed, ValidationTestBed } from './validation.helpers'; describe(' policy name validation', () => { - let testBed: EditPolicyTestBed; - let actions: EditPolicyTestBed['actions']; + let testBed: ValidationTestBed; + let actions: ValidationTestBed['actions']; const { server, httpRequestsMockHelpers } = setupEnvironment(); beforeAll(() => { @@ -29,7 +29,7 @@ describe(' policy name validation', () => { httpRequestsMockHelpers.setLoadPolicies(getGeneratedPolicies()); await act(async () => { - testBed = await setup(); + testBed = await setupValidationTestBed(); }); const { component } = testBed; @@ -56,7 +56,7 @@ describe(' policy name validation', () => { test(`doesn't allow to save as new policy but using the same name`, async () => { await act(async () => { - testBed = await setup({ + testBed = await setupValidationTestBed({ testBedConfig: { memoryRouter: { initialEntries: [`/policies/edit/testy0`], diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/timing.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/timing.test.ts index ac11e8a162e02..be4f99103b319 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/timing.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/timing.test.ts @@ -10,11 +10,11 @@ import { i18nTexts } from '../../../../public/application/sections/edit_policy/i import { PhaseWithTiming } from '../../../../common/types'; import { setupEnvironment } from '../../helpers'; -import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; +import { setupValidationTestBed, ValidationTestBed } from './validation.helpers'; describe(' timing validation', () => { - let testBed: EditPolicyTestBed; - let actions: EditPolicyTestBed['actions']; + let testBed: ValidationTestBed; + let actions: ValidationTestBed['actions']; const { server, httpRequestsMockHelpers } = setupEnvironment(); beforeAll(() => { @@ -31,7 +31,7 @@ describe(' timing validation', () => { httpRequestsMockHelpers.setLoadPolicies([]); await act(async () => { - testBed = await setup(); + testBed = await setupValidationTestBed(); }); const { component } = testBed; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/validation.helpers.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/validation.helpers.ts new file mode 100644 index 0000000000000..84ee96cd46987 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/validation.helpers.ts @@ -0,0 +1,47 @@ +/* + * 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 { TestBedConfig } from '@kbn/test/jest'; +import { + createColdPhaseActions, + createDeletePhaseActions, + createErrorsActions, + createFormSetValueAction, + createFormToggleAction, + createFrozenPhaseActions, + createHotPhaseActions, + createRolloverActions, + createSavePolicyAction, + createTogglePhaseAction, + createWarmPhaseActions, +} from '../../helpers'; +import { initTestBed } from '../init_test_bed'; + +type SetupReturn = ReturnType; + +export type ValidationTestBed = SetupReturn extends Promise ? U : SetupReturn; + +export const setupValidationTestBed = async (arg?: { testBedConfig?: Partial }) => { + const testBed = await initTestBed(arg); + + return { + ...testBed, + actions: { + togglePhase: createTogglePhaseAction(testBed), + setPolicyName: createFormSetValueAction(testBed, 'policyNameField'), + savePolicy: createSavePolicyAction(testBed), + toggleSaveAsNewPolicy: createFormToggleAction(testBed, 'saveAsNewSwitch'), + ...createRolloverActions(testBed), + ...createErrorsActions(testBed), + ...createHotPhaseActions(testBed), + ...createWarmPhaseActions(testBed), + ...createColdPhaseActions(testBed), + ...createFrozenPhaseActions(testBed), + ...createDeletePhaseActions(testBed), + }, + }; +}; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/warm_phase_validation.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/warm_phase_validation.test.ts index bef99ea8cb891..0b8bfceebfaf4 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/warm_phase_validation.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/warm_phase_validation.test.ts @@ -8,10 +8,10 @@ import { act } from 'react-dom/test-utils'; import { i18nTexts } from '../../../../public/application/sections/edit_policy/i18n_texts'; import { setupEnvironment } from '../../helpers'; -import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; +import { setupValidationTestBed, ValidationTestBed } from './validation.helpers'; describe(' warm phase validation', () => { - let testBed: EditPolicyTestBed; + let testBed: ValidationTestBed; const { server, httpRequestsMockHelpers } = setupEnvironment(); beforeAll(() => { @@ -28,7 +28,7 @@ describe(' warm phase validation', () => { httpRequestsMockHelpers.setLoadPolicies([]); await act(async () => { - testBed = await setup(); + testBed = await setupValidationTestBed(); }); const { component, actions } = testBed; @@ -60,7 +60,6 @@ describe(' warm phase validation', () => { describe('shrink', () => { test(`doesn't allow 0 for shrink`, async () => { const { actions } = testBed; - await actions.warm.toggleShrink(); await actions.warm.setShrink('0'); actions.errors.waitForValidation(); @@ -69,7 +68,6 @@ describe(' warm phase validation', () => { }); test(`doesn't allow -1 for shrink`, async () => { const { actions } = testBed; - await actions.warm.toggleShrink(); await actions.warm.setShrink('-1'); actions.errors.waitForValidation(); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/serialization/policy_serialization.helpers.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/serialization/policy_serialization.helpers.ts new file mode 100644 index 0000000000000..52d4debca9315 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/serialization/policy_serialization.helpers.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { AppServicesContext } from '../../../../public/types'; +import { + createColdPhaseActions, + createDeletePhaseActions, + createFormSetValueAction, + createFrozenPhaseActions, + createHotPhaseActions, + createRolloverActions, + createSavePolicyAction, + createTogglePhaseAction, + createWarmPhaseActions, +} from '../../helpers'; +import { initTestBed } from '../init_test_bed'; + +type SetupReturn = ReturnType; + +export type SerializationTestBed = SetupReturn extends Promise ? U : SetupReturn; + +export const setupSerializationTestBed = async (arg?: { + appServicesContext?: Partial; +}) => { + const testBed = await initTestBed(arg); + + return { + ...testBed, + actions: { + togglePhase: createTogglePhaseAction(testBed), + savePolicy: createSavePolicyAction(testBed), + setPolicyName: createFormSetValueAction(testBed, 'policyNameField'), + ...createRolloverActions(testBed), + ...createHotPhaseActions(testBed), + ...createWarmPhaseActions(testBed), + ...createColdPhaseActions(testBed), + ...createFrozenPhaseActions(testBed), + ...createDeletePhaseActions(testBed), + }, + }; +}; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/serialization/policy_serialization.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/serialization/policy_serialization.test.ts index 8c345cf784f9f..6e164cc06681c 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/serialization/policy_serialization.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/serialization/policy_serialization.test.ts @@ -13,10 +13,10 @@ import { POLICY_WITH_INCLUDE_EXCLUDE, POLICY_WITH_KNOWN_AND_UNKNOWN_FIELDS, } from '../constants'; -import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; +import { SerializationTestBed, setupSerializationTestBed } from './policy_serialization.helpers'; describe(' serialization', () => { - let testBed: EditPolicyTestBed; + let testBed: SerializationTestBed; const { server, httpRequestsMockHelpers } = setupEnvironment(); afterAll(() => { @@ -27,7 +27,7 @@ describe(' serialization', () => { httpRequestsMockHelpers.setDefaultResponses(); await act(async () => { - testBed = await setup(); + testBed = await setupSerializationTestBed(); }); const { component } = testBed; @@ -44,7 +44,7 @@ describe(' serialization', () => { it('preserves policy settings it did not configure', async () => { httpRequestsMockHelpers.setLoadPolicies([POLICY_WITH_KNOWN_AND_UNKNOWN_FIELDS]); await act(async () => { - testBed = await setup(); + testBed = await setupSerializationTestBed(); }); const { component, actions } = testBed; @@ -91,7 +91,7 @@ describe(' serialization', () => { httpRequestsMockHelpers.setLoadPolicies([]); await act(async () => { - testBed = await setup(); + testBed = await setupSerializationTestBed(); }); const { component, actions } = testBed; @@ -125,7 +125,7 @@ describe(' serialization', () => { httpRequestsMockHelpers.setLoadPolicies([]); await act(async () => { - testBed = await setup({ + testBed = await setupSerializationTestBed({ appServicesContext: { license: licensingMock.createLicense({ license: { type: 'basic' } }), }, @@ -171,10 +171,8 @@ describe(' serialization', () => { await actions.hot.toggleForceMerge(); await actions.hot.setForcemergeSegmentsCount('123'); await actions.hot.setBestCompression(true); - await actions.hot.toggleShrink(); await actions.hot.setShrink('2'); await actions.hot.toggleReadonly(); - await actions.hot.toggleIndexPriority(); await actions.hot.setIndexPriority('123'); await actions.savePolicy(); @@ -243,7 +241,7 @@ describe(' serialization', () => { httpRequestsMockHelpers.setDefaultResponses(); await act(async () => { - testBed = await setup(); + testBed = await setupSerializationTestBed(); }); const { component } = testBed; @@ -276,7 +274,6 @@ describe(' serialization', () => { await actions.warm.setDataAllocation('node_attrs'); await actions.warm.setSelectedNodeAttribute('test:123'); await actions.warm.setReplicas('123'); - await actions.warm.toggleShrink(); await actions.warm.setShrink('123'); await actions.warm.toggleForceMerge(); await actions.warm.setForcemergeSegmentsCount('123'); @@ -338,7 +335,7 @@ describe(' serialization', () => { httpRequestsMockHelpers.setLoadSnapshotPolicies([]); await act(async () => { - testBed = await setup(); + testBed = await setupSerializationTestBed(); }); const { component } = testBed; @@ -375,7 +372,7 @@ describe(' serialization', () => { httpRequestsMockHelpers.setDefaultResponses(); await act(async () => { - testBed = await setup(); + testBed = await setupSerializationTestBed(); }); const { component } = testBed; @@ -504,7 +501,7 @@ describe(' serialization', () => { }); await act(async () => { - testBed = await setup(); + testBed = await setupSerializationTestBed(); }); const { component } = testBed; @@ -534,7 +531,7 @@ describe(' serialization', () => { test('default value', async () => { const { actions } = testBed; await actions.togglePhase('delete'); - await actions.delete.setWaitForSnapshotPolicy('test'); + await actions.delete.setSnapshotPolicy('test'); await actions.savePolicy(); const latestRequest = server.requests[server.requests.length - 1]; const entirePolicy = JSON.parse(JSON.parse(latestRequest.requestBody).body); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/errors_actions.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/errors_actions.ts index a92747a95a2ca..7acc6a3e2f26b 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/errors_actions.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/errors_actions.ts @@ -32,9 +32,11 @@ const createExpectMessagesAction = (testBed: TestBed) => ( export const createErrorsActions = (testBed: TestBed) => { const { exists } = testBed; return { - waitForValidation: createWaitForValidationAction(testBed), - haveGlobalCallout: () => exists('policyFormErrorsCallout'), - havePhaseCallout: (phase: Phase) => exists(`phaseErrorIndicator-${phase}`), - expectMessages: createExpectMessagesAction(testBed), + errors: { + waitForValidation: createWaitForValidationAction(testBed), + haveGlobalCallout: () => exists('policyFormErrorsCallout'), + havePhaseCallout: (phase: Phase) => exists(`phaseErrorIndicator-${phase}`), + expectMessages: createExpectMessagesAction(testBed), + }, }; }; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/forcemerge_actions.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/forcemerge_actions.ts index a7e4983165bac..400f3d2070e6a 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/forcemerge_actions.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/forcemerge_actions.ts @@ -25,7 +25,7 @@ export const createForceMergeActions = (testBed: TestBed, phase: Phase) => { const { exists } = testBed; const toggleSelector = `${phase}-forceMergeSwitch`; return { - forceMergeFieldExists: () => exists(toggleSelector), + forceMergeExists: () => exists(toggleSelector), toggleForceMerge: createFormToggleAction(testBed, toggleSelector), setForcemergeSegmentsCount: createFormSetValueAction( testBed, diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/set_replicas_action.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/form_toggle_and_set_value_action.ts similarity index 57% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/set_replicas_action.ts rename to x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/form_toggle_and_set_value_action.ts index b07d7783379fb..643e0f23a9dea 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/set_replicas_action.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/form_toggle_and_set_value_action.ts @@ -6,16 +6,17 @@ */ import { TestBed } from '@kbn/test/jest'; - -import { Phase } from '../../../../common/types'; import { createFormToggleAction } from './form_toggle_action'; import { createFormSetValueAction } from './form_set_value_action'; -export const setReplicas = async (testBed: TestBed, phase: Phase, value: string) => { +export const createFormToggleAndSetValueAction = ( + testBed: TestBed, + toggleSelector: string, + inputSelector: string +) => async (value: string) => { const { exists } = testBed; - - if (!exists(`${phase}-selectedReplicaCount`)) { - await createFormToggleAction(testBed, `${phase}-setReplicasSwitch`)(); + if (!exists(inputSelector)) { + await createFormToggleAction(testBed, toggleSelector)(); } - await createFormSetValueAction(testBed, `${phase}-selectedReplicaCount`)(value); + await createFormSetValueAction(testBed, inputSelector)(value); }; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/index.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/index.ts index 7366bf2f35c70..acfaee3c236e9 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/index.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/index.ts @@ -7,17 +7,25 @@ export { createNodeAllocationActions } from './node_allocation_actions'; export { createTogglePhaseAction } from './toggle_phase_action'; -export { setReplicas } from './set_replicas_action'; +export { createReplicasAction } from './replicas_action'; export { createSavePolicyAction } from './save_policy_action'; export { createFormToggleAction } from './form_toggle_action'; export { createFormSetValueAction } from './form_set_value_action'; +export { createFormToggleAndSetValueAction } from './form_toggle_and_set_value_action'; export { createSearchableSnapshotActions } from './searchable_snapshot_actions'; export { createErrorsActions } from './errors_actions'; export { createRolloverActions } from './rollover_actions'; -export { createSetWaitForSnapshotAction } from './set_wait_for_snapshot_action'; +export { createSnapshotPolicyActions } from './snapshot_policy_actions'; export { createMinAgeActions } from './min_age_actions'; export { createForceMergeActions } from './forcemerge_actions'; export { createReadonlyActions } from './readonly_actions'; export { createIndexPriorityActions } from './index_priority_actions'; export { createShrinkActions } from './shrink_actions'; export { createFreezeActions } from './freeze_actions'; +export { + createHotPhaseActions, + createWarmPhaseActions, + createColdPhaseActions, + createFrozenPhaseActions, + createDeletePhaseActions, +} from './phases'; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/index_priority_actions.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/index_priority_actions.ts index 3b48da2a0c69f..eeab42c408244 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/index_priority_actions.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/index_priority_actions.ts @@ -8,7 +8,7 @@ import { TestBed } from '@kbn/test/jest'; import { Phase } from '../../../../common/types'; import { createFormToggleAction } from './form_toggle_action'; -import { createFormSetValueAction } from './form_set_value_action'; +import { createFormToggleAndSetValueAction } from './form_toggle_and_set_value_action'; export const createIndexPriorityActions = (testBed: TestBed, phase: Phase) => { const { exists } = testBed; @@ -16,6 +16,10 @@ export const createIndexPriorityActions = (testBed: TestBed, phase: Phase) => { return { indexPriorityExists: () => exists(toggleSelector), toggleIndexPriority: createFormToggleAction(testBed, toggleSelector), - setIndexPriority: createFormSetValueAction(testBed, `${phase}-indexPriority`), + setIndexPriority: createFormToggleAndSetValueAction( + testBed, + toggleSelector, + `${phase}-indexPriority` + ), }; }; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/phases.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/phases.ts new file mode 100644 index 0000000000000..18cc0f01ca06c --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/phases.ts @@ -0,0 +1,76 @@ +/* + * 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 { TestBed } from '@kbn/test/jest'; +import { + createForceMergeActions, + createShrinkActions, + createReadonlyActions, + createIndexPriorityActions, + createSearchableSnapshotActions, + createMinAgeActions, + createNodeAllocationActions, + createReplicasAction, + createFreezeActions, + createSnapshotPolicyActions, +} from './'; + +export const createHotPhaseActions = (testBed: TestBed) => { + return { + hot: { + ...createForceMergeActions(testBed, 'hot'), + ...createShrinkActions(testBed, 'hot'), + ...createReadonlyActions(testBed, 'hot'), + ...createIndexPriorityActions(testBed, 'hot'), + ...createSearchableSnapshotActions(testBed, 'hot'), + }, + }; +}; +export const createWarmPhaseActions = (testBed: TestBed) => { + return { + warm: { + ...createMinAgeActions(testBed, 'warm'), + ...createForceMergeActions(testBed, 'warm'), + ...createShrinkActions(testBed, 'warm'), + ...createReadonlyActions(testBed, 'warm'), + ...createIndexPriorityActions(testBed, 'warm'), + ...createNodeAllocationActions(testBed, 'warm'), + ...createReplicasAction(testBed, 'warm'), + }, + }; +}; +export const createColdPhaseActions = (testBed: TestBed) => { + return { + cold: { + ...createMinAgeActions(testBed, 'cold'), + ...createReplicasAction(testBed, 'cold'), + ...createReadonlyActions(testBed, 'cold'), + ...createFreezeActions(testBed, 'cold'), + ...createIndexPriorityActions(testBed, 'cold'), + ...createNodeAllocationActions(testBed, 'cold'), + ...createSearchableSnapshotActions(testBed, 'cold'), + }, + }; +}; + +export const createFrozenPhaseActions = (testBed: TestBed) => { + return { + frozen: { + ...createMinAgeActions(testBed, 'frozen'), + ...createSearchableSnapshotActions(testBed, 'frozen'), + }, + }; +}; + +export const createDeletePhaseActions = (testBed: TestBed) => { + return { + delete: { + ...createMinAgeActions(testBed, 'delete'), + ...createSnapshotPolicyActions(testBed), + }, + }; +}; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/replicas_action.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/replicas_action.ts new file mode 100644 index 0000000000000..f987ce6d0ca2f --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/replicas_action.ts @@ -0,0 +1,20 @@ +/* + * 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 { TestBed } from '@kbn/test/jest'; +import { Phase } from '../../../../common/types'; +import { createFormToggleAndSetValueAction } from './form_toggle_and_set_value_action'; + +export const createReplicasAction = (testBed: TestBed, phase: Phase) => { + return { + setReplicas: createFormToggleAndSetValueAction( + testBed, + `${phase}-setReplicasSwitch`, + `${phase}-selectedReplicaCount` + ), + }; +}; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/rollover_actions.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/rollover_actions.ts index daf4db7fab278..6d05f3d63f577 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/rollover_actions.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/rollover_actions.ts @@ -53,12 +53,14 @@ const createSetMaxSizeAction = (testBed: TestBed) => async (value: string, units export const createRolloverActions = (testBed: TestBed) => { const { exists } = testBed; return { - toggle: createFormToggleAction(testBed, 'rolloverSwitch'), - toggleDefault: createFormToggleAction(testBed, 'useDefaultRolloverSwitch'), - setMaxPrimaryShardSize: createSetPrimaryShardSizeAction(testBed), - setMaxDocs: createFormSetValueAction(testBed, 'hot-selectedMaxDocuments'), - setMaxAge: createSetMaxAgeAction(testBed), - setMaxSize: createSetMaxSizeAction(testBed), - hasSettingRequiredCallout: (): boolean => exists('rolloverSettingsRequired'), + rollover: { + toggle: createFormToggleAction(testBed, 'rolloverSwitch'), + toggleDefault: createFormToggleAction(testBed, 'useDefaultRolloverSwitch'), + setMaxPrimaryShardSize: createSetPrimaryShardSizeAction(testBed), + setMaxDocs: createFormSetValueAction(testBed, 'hot-selectedMaxDocuments'), + setMaxAge: createSetMaxAgeAction(testBed), + setMaxSize: createSetMaxSizeAction(testBed), + hasSettingRequiredCallout: (): boolean => exists('rolloverSettingsRequired'), + }, }; }; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/set_wait_for_snapshot_action.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/set_wait_for_snapshot_action.ts deleted file mode 100644 index a0bc9da8d3063..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/set_wait_for_snapshot_action.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { act } from 'react-dom/test-utils'; -import { TestBed } from '@kbn/test/jest'; - -export const createSetWaitForSnapshotAction = (testBed: TestBed) => async ( - snapshotPolicyName: string -) => { - const { find, component } = testBed; - act(() => { - find('snapshotPolicyCombobox').simulate('change', [{ label: snapshotPolicyName }]); - }); - component.update(); -}; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/shrink_actions.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/shrink_actions.ts index 0531850384124..29c3e4a04a9a1 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/shrink_actions.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/shrink_actions.ts @@ -7,15 +7,17 @@ import { TestBed } from '@kbn/test/jest'; import { Phase } from '../../../../common/types'; -import { createFormToggleAction } from './form_toggle_action'; -import { createFormSetValueAction } from './form_set_value_action'; +import { createFormToggleAndSetValueAction } from './form_toggle_and_set_value_action'; export const createShrinkActions = (testBed: TestBed, phase: Phase) => { const { exists } = testBed; const toggleSelector = `${phase}-shrinkSwitch`; return { shrinkExists: () => exists(toggleSelector), - toggleShrink: createFormToggleAction(testBed, toggleSelector), - setShrink: createFormSetValueAction(testBed, `${phase}-primaryShardCount`), + setShrink: createFormToggleAndSetValueAction( + testBed, + toggleSelector, + `${phase}-primaryShardCount` + ), }; }; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/snapshot_policy_actions.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/snapshot_policy_actions.ts new file mode 100644 index 0000000000000..0a49c3cf295bd --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/snapshot_policy_actions.ts @@ -0,0 +1,27 @@ +/* + * 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 { TestBed } from '@kbn/test/target/types/jest'; +import { act } from 'react-dom/test-utils'; + +const createSetWaitForSnapshotAction = (testBed: TestBed) => async (snapshotPolicyName: string) => { + const { find, component } = testBed; + act(() => { + find('snapshotPolicyCombobox').simulate('change', [{ label: snapshotPolicyName }]); + }); + component.update(); +}; + +export const createSnapshotPolicyActions = (testBed: TestBed) => { + const { exists } = testBed; + return { + setSnapshotPolicy: createSetWaitForSnapshotAction(testBed), + hasCustomPolicyCallout: () => exists('customPolicyCallout'), + hasPolicyErrorCallout: () => exists('policiesErrorCallout'), + hasNoPoliciesCallout: () => exists('noPoliciesCallout'), + }; +}; From aefdb9c2b086878f4daa88ec7cc914fdb4f516ae Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Thu, 24 Jun 2021 09:54:38 -0600 Subject: [PATCH 34/69] [Maps] timeslider play button (#103147) * [Maps] timeslider play button * cancel subscription on unmount * change playback speed to 1750 Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../map_container/map_container.tsx | 4 +- .../timeslider/timeslider.tsx | 60 +++++++++++++++++++ .../maps/public/embeddable/map_embeddable.tsx | 2 + .../routes/map_page/map_app/map_app.tsx | 1 + .../map_app/wait_until_time_layers_load.ts | 7 ++- 5 files changed, 72 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/maps/public/connected_components/map_container/map_container.tsx b/x-pack/plugins/maps/public/connected_components/map_container/map_container.tsx index 788094ee1ab5c..0bdf462cca4b3 100644 --- a/x-pack/plugins/maps/public/connected_components/map_container/map_container.tsx +++ b/x-pack/plugins/maps/public/connected_components/map_container/map_container.tsx @@ -13,6 +13,7 @@ import { i18n } from '@kbn/i18n'; import uuid from 'uuid/v4'; import { Filter } from 'src/plugins/data/public'; import { ActionExecutionContext, Action } from 'src/plugins/ui_actions/public'; +import { Observable } from 'rxjs'; import { MBMap } from '../mb_map'; import { RightSideControls } from '../right_side_controls'; import { Timeslider } from '../timeslider'; @@ -47,6 +48,7 @@ export interface Props { description?: string; settings: MapSettings; layerList: ILayer[]; + waitUntilTimeLayersLoad$: Observable; } interface State { @@ -223,7 +225,7 @@ export class MapContainer extends Component { - + void; isTimesliderOpen: boolean; timeRange: TimeRange; + waitForTimesliceToLoad$: Observable; } interface State { + isPaused: boolean; max: number; min: number; range: number; @@ -44,6 +48,8 @@ export function Timeslider(props: Props) { class KeyedTimeslider extends Component { private _isMounted: boolean = false; + private _timeoutId: number | undefined; + private _subscription: Subscription | undefined; constructor(props: Props) { super(props); @@ -59,6 +65,7 @@ class KeyedTimeslider extends Component { const timeslice: [number, number] = [min, max]; this.state = { + isPaused: true, max, min, range: interval, @@ -68,6 +75,7 @@ class KeyedTimeslider extends Component { } componentWillUnmount() { + this._onPause(); this._isMounted = false; } @@ -118,6 +126,44 @@ class KeyedTimeslider extends Component { } }, 300); + _onPlay = () => { + this.setState({ isPaused: false }); + this._playNextFrame(); + }; + + _onPause = () => { + this.setState({ isPaused: true }); + if (this._subscription) { + this._subscription.unsubscribe(); + this._subscription = undefined; + } + if (this._timeoutId) { + clearTimeout(this._timeoutId); + this._timeoutId = undefined; + } + }; + + _playNextFrame() { + // advance to next frame + this._onNext(); + + // use waitForTimesliceToLoad$ observable to wait until next frame loaded + // .pipe(first()) waits until the first value is emitted from an observable and then automatically unsubscribes + this._subscription = this.props.waitForTimesliceToLoad$.pipe(first()).subscribe(() => { + if (this.state.isPaused) { + return; + } + + // use timeout to display frame for small time period before moving to next frame + this._timeoutId = window.setTimeout(() => { + if (this.state.isPaused) { + return; + } + this._playNextFrame(); + }, 1750); + }); + } + render() { return (
@@ -154,6 +200,20 @@ class KeyedTimeslider extends Component { defaultMessage: 'Next time window', })} /> +
diff --git a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx index 509cece671dd6..4670285aa9eff 100644 --- a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx +++ b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx @@ -73,6 +73,7 @@ import { SavedMap } from '../routes/map_page'; import { getIndexPatternsFromIds } from '../index_pattern_util'; import { getMapAttributeService } from '../map_attribute_service'; import { isUrlDrilldown, toValueClickDataFormat } from '../trigger_actions/trigger_utils'; +import { waitUntilTimeLayersLoad$ } from '../routes/map_page/map_app/wait_until_time_layers_load'; import { MapByValueInput, @@ -345,6 +346,7 @@ export class MapEmbeddable renderTooltipContent={this._renderTooltipContent} title={this.getTitle()} description={this.getDescription()} + waitUntilTimeLayersLoad$={waitUntilTimeLayersLoad$(this._savedMap.getStore())} /> , diff --git a/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx b/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx index 92459ed28ab91..5231aab5d1194 100644 --- a/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx +++ b/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx @@ -455,6 +455,7 @@ export class MapApp extends React.Component { addFilters={this._addFilter} title={this.props.savedMap.getAttributes().title} description={this.props.savedMap.getAttributes().description} + waitUntilTimeLayersLoad$={waitUntilTimeLayersLoad$(this.props.savedMap.getStore())} /> diff --git a/x-pack/plugins/maps/public/routes/map_page/map_app/wait_until_time_layers_load.ts b/x-pack/plugins/maps/public/routes/map_page/map_app/wait_until_time_layers_load.ts index 7e08e49863fdf..1258539456c63 100644 --- a/x-pack/plugins/maps/public/routes/map_page/map_app/wait_until_time_layers_load.ts +++ b/x-pack/plugins/maps/public/routes/map_page/map_app/wait_until_time_layers_load.ts @@ -6,7 +6,7 @@ */ import { from } from 'rxjs'; -import { debounceTime, first, switchMap } from 'rxjs/operators'; +import { debounceTime, first, map, switchMap } from 'rxjs/operators'; import { getLayerList } from '../../../selectors/map_selectors'; import { MapStore } from '../../../reducers/store'; @@ -31,6 +31,11 @@ export function waitUntilTimeLayersLoad$(store: MapStore) { .filter(({ isFilteredByGlobalTime }) => isFilteredByGlobalTime) .some(({ layer }) => layer.isLayerLoading()); return !areTimeLayersStillLoading; + }), + map(() => { + // Observable notifies subscriber when loading is finished + // Return void to not expose internal implemenation details of observabale + return; }) ); } From 7e32f934aa3170a0a1197812bd5f2bce65060b2a Mon Sep 17 00:00:00 2001 From: ymao1 Date: Thu, 24 Jun 2021 12:20:16 -0400 Subject: [PATCH 35/69] [Alerting] Using new es client in alerting functional tests (#102349) * Switching to new es client in alerting tests * Fixing types * Updating functional test * Updating functional test * Updating functional test * Fixing error handling * Fixing types * Fixing error handling * Fixing functional tests * Fixing functional tests Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../common/lib/es_test_index_tool.ts | 11 +- .../common/lib/index.ts | 2 +- .../common/lib/task_manager_utils.ts | 20 ++- .../actions/builtin_action_types/es_index.ts | 4 +- .../es_index_preconfigured.ts | 4 +- .../tests/actions/execute.ts | 18 +-- .../tests/alerting/alerts.ts | 126 ++++++++++-------- .../tests/alerting/create.ts | 13 +- .../tests/alerting/delete.ts | 12 +- .../tests/alerting/disable.ts | 12 +- .../tests/alerting/enable.ts | 21 ++- .../tests/alerting/health.ts | 2 +- .../tests/alerting/rbac_legacy.ts | 12 +- .../actions/builtin_action_types/es_index.ts | 4 +- .../preconfigured_alert_history_connector.ts | 12 +- .../spaces_only/tests/actions/enqueue.ts | 7 +- .../spaces_only/tests/actions/execute.ts | 10 +- .../spaces_only/tests/alerting/alerts_base.ts | 26 ++-- .../builtin_alert_types/es_query/alert.ts | 2 +- .../es_query/create_test_data.ts | 2 +- .../index_threshold/alert.ts | 2 +- .../index_threshold/create_test_data.ts | 2 +- .../index_threshold/fields_endpoint.ts | 2 +- .../index_threshold/indices_endpoint.ts | 2 +- .../time_series_query_endpoint.ts | 2 +- .../spaces_only/tests/alerting/create.ts | 13 +- .../spaces_only/tests/alerting/delete.ts | 6 +- .../spaces_only/tests/alerting/disable.ts | 6 +- .../spaces_only/tests/alerting/enable.ts | 13 +- .../event_log/service_api_integration.ts | 4 +- 30 files changed, 206 insertions(+), 166 deletions(-) diff --git a/x-pack/test/alerting_api_integration/common/lib/es_test_index_tool.ts b/x-pack/test/alerting_api_integration/common/lib/es_test_index_tool.ts index 18655f2d72fda..1d5eb43ebe970 100644 --- a/x-pack/test/alerting_api_integration/common/lib/es_test_index_tool.ts +++ b/x-pack/test/alerting_api_integration/common/lib/es_test_index_tool.ts @@ -59,7 +59,10 @@ export class ESTestIndexTool { } async destroy() { - return await this.es.indices.delete({ index: this.index, ignore: [404] }); + const indexExists = (await this.es.indices.exists({ index: this.index })).body; + if (indexExists) { + return await this.es.indices.delete({ index: this.index }); + } } async search(source: string, reference: string) { @@ -90,10 +93,10 @@ export class ESTestIndexTool { async waitForDocs(source: string, reference: string, numDocs: number = 1) { return await this.retry.try(async () => { const searchResult = await this.search(source, reference); - if (searchResult.hits.total.value < numDocs) { - throw new Error(`Expected ${numDocs} but received ${searchResult.hits.total.value}.`); + if (searchResult.body.hits.total.value < numDocs) { + throw new Error(`Expected ${numDocs} but received ${searchResult.body.hits.total.value}.`); } - return searchResult.hits.hits; + return searchResult.body.hits.hits; }); } } diff --git a/x-pack/test/alerting_api_integration/common/lib/index.ts b/x-pack/test/alerting_api_integration/common/lib/index.ts index 242ce7ed8d884..eeb9c88269667 100644 --- a/x-pack/test/alerting_api_integration/common/lib/index.ts +++ b/x-pack/test/alerting_api_integration/common/lib/index.ts @@ -14,7 +14,7 @@ export { getConsumerUnauthorizedErrorMessage, getProducerUnauthorizedErrorMessage, } from './alert_utils'; -export { TaskManagerUtils } from './task_manager_utils'; +export { TaskManagerUtils, TaskManagerDoc } from './task_manager_utils'; export * from './test_assertions'; export { checkAAD } from './check_aad'; export { getEventLog } from './get_event_log'; diff --git a/x-pack/test/alerting_api_integration/common/lib/task_manager_utils.ts b/x-pack/test/alerting_api_integration/common/lib/task_manager_utils.ts index 73a9d93f7f329..57af1b1bcb035 100644 --- a/x-pack/test/alerting_api_integration/common/lib/task_manager_utils.ts +++ b/x-pack/test/alerting_api_integration/common/lib/task_manager_utils.ts @@ -5,6 +5,12 @@ * 2.0. */ +import { SerializedConcreteTaskInstance } from '../../../../plugins/task_manager/server/task'; + +export interface TaskManagerDoc { + type: string; + task: SerializedConcreteTaskInstance; +} export class TaskManagerUtils { private readonly es: any; private readonly retry: any; @@ -39,8 +45,8 @@ export class TaskManagerUtils { }, }, }); - if (searchResult.hits.total.value) { - throw new Error(`Expected 0 tasks but received ${searchResult.hits.total.value}`); + if (searchResult.body.hits.total.value) { + throw new Error(`Expected 0 tasks but received ${searchResult.body.hits.total.value}`); } }); } @@ -77,8 +83,10 @@ export class TaskManagerUtils { }, }, }); - if (searchResult.hits.total.value) { - throw new Error(`Expected 0 non-idle tasks but received ${searchResult.hits.total.value}`); + if (searchResult.body.hits.total.value) { + throw new Error( + `Expected 0 non-idle tasks but received ${searchResult.body.hits.total.value}` + ); } }); } @@ -108,9 +116,9 @@ export class TaskManagerUtils { }, }, }); - if (searchResult.hits.total.value) { + if (searchResult.body.hits.total.value) { throw new Error( - `Expected 0 action_task_params objects but received ${searchResult.hits.total.value}` + `Expected 0 action_task_params objects but received ${searchResult.body.hits.total.value}` ); } }); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/es_index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/es_index.ts index cd60f0cef1710..3db58cb2adc3d 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/es_index.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/es_index.ts @@ -13,7 +13,7 @@ const ES_TEST_INDEX_NAME = 'functional-test-actions-index'; // eslint-disable-next-line import/no-default-export export default function indexTest({ getService }: FtrProviderContext) { - const es = getService('legacyEs'); + const es = getService('es'); const supertest = getService('supertest'); const esDeleteAllIndices = getService('esDeleteAllIndices'); @@ -273,5 +273,5 @@ async function getTestIndexItems(es: any) { index: ES_TEST_INDEX_NAME, }); - return result.hits.hits; + return result.body.hits.hits; } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/es_index_preconfigured.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/es_index_preconfigured.ts index 48f042473c14e..92a5d7d840276 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/es_index_preconfigured.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/es_index_preconfigured.ts @@ -15,7 +15,7 @@ const ES_TEST_INDEX_NAME = 'functional-test-actions-index-preconfigured'; // eslint-disable-next-line import/no-default-export export default function indexTest({ getService }: FtrProviderContext) { - const es = getService('legacyEs'); + const es = getService('es'); const esDeleteAllIndices = getService('esDeleteAllIndices'); const supertest = getService('supertest'); @@ -57,5 +57,5 @@ async function getTestIndexItems(es: any) { index: ES_TEST_INDEX_NAME, }); - return result.hits.hits; + return result.body.hits.hits; } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/execute.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/execute.ts index 5c578d2d08dae..9091b96ff335a 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/execute.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/execute.ts @@ -23,7 +23,7 @@ const NANOS_IN_MILLIS = 1000 * 1000; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const supertestWithoutAuth = getService('supertestWithoutAuth'); - const es = getService('legacyEs'); + const es = getService('es'); const retry = getService('retry'); const esTestIndexTool = new ESTestIndexTool(es, retry); @@ -97,8 +97,8 @@ export default function ({ getService }: FtrProviderContext) { 'action:test.index-record', reference ); - expect(searchResult.hits.total.value).to.eql(1); - const indexedRecord = searchResult.hits.hits[0]; + expect(searchResult.body.hits.total.value).to.eql(1); + const indexedRecord = searchResult.body.hits.hits[0]; expect(indexedRecord._source).to.eql({ params: { reference, @@ -250,8 +250,8 @@ export default function ({ getService }: FtrProviderContext) { 'action:test.index-record', reference ); - expect(searchResult.hits.total.value).to.eql(1); - const indexedRecord = searchResult.hits.hits[0]; + expect(searchResult.body.hits.total.value).to.eql(1); + const indexedRecord = searchResult.body.hits.hits[0]; expect(indexedRecord._source).to.eql({ params: { reference, @@ -453,8 +453,8 @@ export default function ({ getService }: FtrProviderContext) { case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(200); searchResult = await esTestIndexTool.search('action:test.authorization', reference); - expect(searchResult.hits.total.value).to.eql(1); - indexedRecord = searchResult.hits.hits[0]; + expect(searchResult.body.hits.total.value).to.eql(1); + indexedRecord = searchResult.body.hits.hits[0]; expect(indexedRecord._source.state).to.eql({ callClusterSuccess: false, callScopedClusterSuccess: false, @@ -477,8 +477,8 @@ export default function ({ getService }: FtrProviderContext) { case 'superuser at space1': expect(response.statusCode).to.eql(200); searchResult = await esTestIndexTool.search('action:test.authorization', reference); - expect(searchResult.hits.total.value).to.eql(1); - indexedRecord = searchResult.hits.hits[0]; + expect(searchResult.body.hits.total.value).to.eql(1); + indexedRecord = searchResult.body.hits.hits[0]; expect(indexedRecord._source.state).to.eql({ callClusterSuccess: true, callScopedClusterSuccess: true, 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 b3d83ae22f330..3131649e7c742 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 @@ -7,6 +7,7 @@ import expect from '@kbn/expect'; import { omit } from 'lodash'; +import type { ApiResponse, estypes } from '@elastic/elasticsearch'; import { UserAtSpaceScenarios, Superuser } from '../../scenarios'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; import { @@ -21,13 +22,18 @@ import { getEventLog, } from '../../../common/lib'; import { IValidatedEvent } from '../../../../../plugins/event_log/server'; +import { + TaskRunning, + TaskRunningStage, +} from '../../../../../plugins/task_manager/server/task_running'; +import { ConcreteTaskInstance } from '../../../../../plugins/task_manager/server'; const NANOS_IN_MILLIS = 1000 * 1000; // eslint-disable-next-line import/no-default-export export default function alertTests({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - const es = getService('legacyEs'); + const es = getService('es'); const retry = getService('retry'); const supertestWithoutAuth = getService('supertestWithoutAuth'); const esTestIndexTool = new ESTestIndexTool(es, retry); @@ -128,11 +134,11 @@ export default function alertTests({ getService }: FtrProviderContext) { 'alert:test.always-firing', reference ); - expect(alertSearchResult.hits.total.value).to.eql(1); - const alertSearchResultWithoutDates = omit(alertSearchResult.hits.hits[0]._source, [ - 'alertInfo.createdAt', - 'alertInfo.updatedAt', - ]); + expect(alertSearchResult.body.hits.total.value).to.eql(1); + const alertSearchResultWithoutDates = omit( + alertSearchResult.body.hits.hits[0]._source, + ['alertInfo.createdAt', 'alertInfo.updatedAt'] + ); expect(alertSearchResultWithoutDates).to.eql({ source: 'alert:test.always-firing', reference, @@ -171,10 +177,10 @@ export default function alertTests({ getService }: FtrProviderContext) { ruleTypeName: 'Test: Always Firing', }, }); - expect(alertSearchResult.hits.hits[0]._source.alertInfo.createdAt).to.match( + expect(alertSearchResult.body.hits.hits[0]._source.alertInfo.createdAt).to.match( /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/ ); - expect(alertSearchResult.hits.hits[0]._source.alertInfo.updatedAt).to.match( + expect(alertSearchResult.body.hits.hits[0]._source.alertInfo.updatedAt).to.match( /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/ ); @@ -183,8 +189,8 @@ export default function alertTests({ getService }: FtrProviderContext) { 'action:test.index-record', reference ); - expect(actionSearchResult.hits.total.value).to.eql(1); - expect(actionSearchResult.hits.hits[0]._source).to.eql({ + expect(actionSearchResult.body.hits.total.value).to.eql(1); + expect(actionSearchResult.body.hits.hits[0]._source).to.eql({ config: { unencrypted: `This value shouldn't get encrypted`, }, @@ -275,11 +281,11 @@ instanceStateValue: true 'alert:test.always-firing', reference ); - expect(alertSearchResult.hits.total.value).to.eql(1); - const alertSearchResultWithoutDates = omit(alertSearchResult.hits.hits[0]._source, [ - 'alertInfo.createdAt', - 'alertInfo.updatedAt', - ]); + expect(alertSearchResult.body.hits.total.value).to.eql(1); + const alertSearchResultWithoutDates = omit( + alertSearchResult.body.hits.hits[0]._source, + ['alertInfo.createdAt', 'alertInfo.updatedAt'] + ); expect(alertSearchResultWithoutDates).to.eql({ source: 'alert:test.always-firing', reference, @@ -319,10 +325,10 @@ instanceStateValue: true }, }); - expect(alertSearchResult.hits.hits[0]._source.alertInfo.createdAt).to.match( + expect(alertSearchResult.body.hits.hits[0]._source.alertInfo.createdAt).to.match( /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/ ); - expect(alertSearchResult.hits.hits[0]._source.alertInfo.updatedAt).to.match( + expect(alertSearchResult.body.hits.hits[0]._source.alertInfo.updatedAt).to.match( /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/ ); // Ensure only 1 action executed with proper params @@ -330,8 +336,8 @@ instanceStateValue: true 'action:test.index-record', reference ); - expect(actionSearchResult.hits.total.value).to.eql(1); - expect(actionSearchResult.hits.hits[0]._source).to.eql({ + expect(actionSearchResult.body.hits.total.value).to.eql(1); + expect(actionSearchResult.body.hits.hits[0]._source).to.eql({ config: { unencrypted: 'ignored-but-required', }, @@ -410,9 +416,9 @@ instanceStateValue: true reference2 ); - expect(alertSearchResult.hits.total.value).to.be.greaterThan(0); + expect(alertSearchResult.body.hits.total.value).to.be.greaterThan(0); const alertSearchResultInfoWithoutDates = omit( - alertSearchResult.hits.hits[0]._source.alertInfo, + alertSearchResult.body.hits.hits[0]._source.alertInfo, ['createdAt', 'updatedAt'] ); expect(alertSearchResultInfoWithoutDates).to.eql({ @@ -445,16 +451,16 @@ instanceStateValue: true ruleTypeName: 'Test: Always Firing', }); - expect(alertSearchResult.hits.hits[0]._source.alertInfo.createdAt).to.match( + expect(alertSearchResult.body.hits.hits[0]._source.alertInfo.createdAt).to.match( /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/ ); - expect(alertSearchResult.hits.hits[0]._source.alertInfo.updatedAt).to.match( + expect(alertSearchResult.body.hits.hits[0]._source.alertInfo.updatedAt).to.match( /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/ ); }); it('should handle custom retry logic when appropriate', async () => { - const testStart = new Date(); + const testStart = new Date().toISOString(); // We have to provide the test.rate-limit the next runAt, for testing purposes const retryDate = new Date(Date.now() + 60000); @@ -525,8 +531,12 @@ instanceStateValue: true objectRemover.add(space.id, response.body.id, 'rule', 'alerting'); // Wait for the task to be attempted once and idle - const scheduledActionTask = await retry.try(async () => { - const searchResult = await es.search({ + const scheduledActionTask: estypes.SearchHit< + TaskRunning + > = await retry.try(async () => { + const searchResult: ApiResponse< + estypes.SearchResponse> + > = await es.search({ index: '.kibana_task_manager', body: { query: { @@ -559,12 +569,12 @@ instanceStateValue: true }, }, }); - expect(searchResult.hits.total.value).to.eql(1); - return searchResult.hits.hits[0]; + expect((searchResult.body.hits.total as estypes.SearchTotalHits).value).to.eql(1); + return searchResult.body.hits.hits[0]; }); // Ensure the next runAt is set to the retryDate by custom logic - expect(scheduledActionTask._source.task.runAt).to.eql(retryDate.toISOString()); + expect(scheduledActionTask._source!.task.runAt).to.eql(retryDate.toISOString()); break; default: throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); @@ -620,21 +630,21 @@ instanceStateValue: true // Ensure only 1 document exists with proper params searchResult = await esTestIndexTool.search('alert:test.authorization', reference); - expect(searchResult.hits.total.value).to.eql(1); - expect(searchResult.hits.hits[0]._source.state).to.eql({ + expect(searchResult.body.hits.total.value).to.eql(1); + expect(searchResult.body.hits.hits[0]._source.state).to.eql({ callClusterSuccess: false, callScopedClusterSuccess: false, savedObjectsClientSuccess: false, callClusterError: { - ...searchResult.hits.hits[0]._source.state.callClusterError, + ...searchResult.body.hits.hits[0]._source.state.callClusterError, }, callScopedClusterError: { - ...searchResult.hits.hits[0]._source.state.callScopedClusterError, + ...searchResult.body.hits.hits[0]._source.state.callScopedClusterError, }, savedObjectsClientError: { - ...searchResult.hits.hits[0]._source.state.savedObjectsClientError, + ...searchResult.body.hits.hits[0]._source.state.savedObjectsClientError, output: { - ...searchResult.hits.hits[0]._source.state.savedObjectsClientError.output, + ...searchResult.body.hits.hits[0]._source.state.savedObjectsClientError.output, statusCode: 403, }, }, @@ -651,15 +661,15 @@ instanceStateValue: true // Ensure only 1 document exists with proper params searchResult = await esTestIndexTool.search('alert:test.authorization', reference); - expect(searchResult.hits.total.value).to.eql(1); - expect(searchResult.hits.hits[0]._source.state).to.eql({ + expect(searchResult.body.hits.total.value).to.eql(1); + expect(searchResult.body.hits.hits[0]._source.state).to.eql({ callClusterSuccess: true, callScopedClusterSuccess: true, savedObjectsClientSuccess: false, savedObjectsClientError: { - ...searchResult.hits.hits[0]._source.state.savedObjectsClientError, + ...searchResult.body.hits.hits[0]._source.state.savedObjectsClientError, output: { - ...searchResult.hits.hits[0]._source.state.savedObjectsClientError.output, + ...searchResult.body.hits.hits[0]._source.state.savedObjectsClientError.output, statusCode: 404, }, }, @@ -737,21 +747,21 @@ instanceStateValue: true // Ensure only 1 document with proper params exists searchResult = await esTestIndexTool.search('action:test.authorization', reference); - expect(searchResult.hits.total.value).to.eql(1); - expect(searchResult.hits.hits[0]._source.state).to.eql({ + expect(searchResult.body.hits.total.value).to.eql(1); + expect(searchResult.body.hits.hits[0]._source.state).to.eql({ callClusterSuccess: false, callScopedClusterSuccess: false, savedObjectsClientSuccess: false, callClusterError: { - ...searchResult.hits.hits[0]._source.state.callClusterError, + ...searchResult.body.hits.hits[0]._source.state.callClusterError, }, callScopedClusterError: { - ...searchResult.hits.hits[0]._source.state.callScopedClusterError, + ...searchResult.body.hits.hits[0]._source.state.callScopedClusterError, }, savedObjectsClientError: { - ...searchResult.hits.hits[0]._source.state.savedObjectsClientError, + ...searchResult.body.hits.hits[0]._source.state.savedObjectsClientError, output: { - ...searchResult.hits.hits[0]._source.state.savedObjectsClientError.output, + ...searchResult.body.hits.hits[0]._source.state.savedObjectsClientError.output, statusCode: 403, }, }, @@ -776,15 +786,15 @@ instanceStateValue: true // Ensure only 1 document with proper params exists searchResult = await esTestIndexTool.search('action:test.authorization', reference); - expect(searchResult.hits.total.value).to.eql(1); - expect(searchResult.hits.hits[0]._source.state).to.eql({ + expect(searchResult.body.hits.total.value).to.eql(1); + expect(searchResult.body.hits.hits[0]._source.state).to.eql({ callClusterSuccess: true, callScopedClusterSuccess: true, savedObjectsClientSuccess: false, savedObjectsClientError: { - ...searchResult.hits.hits[0]._source.state.savedObjectsClientError, + ...searchResult.body.hits.hits[0]._source.state.savedObjectsClientError, output: { - ...searchResult.hits.hits[0]._source.state.savedObjectsClientError.output, + ...searchResult.body.hits.hits[0]._source.state.savedObjectsClientError.output, statusCode: 404, }, }, @@ -842,7 +852,7 @@ instanceStateValue: true 'action:test.index-record', reference ); - expect(searchResult.hits.total.value).to.eql(1); + expect(searchResult.body.hits.total.value).to.eql(1); break; default: throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); @@ -921,8 +931,8 @@ instanceStateValue: true 'action:test.index-record', reference ); - expect(searchResult.hits.total.value).to.eql(2); - const messages: string[] = searchResult.hits.hits.map( + expect(searchResult.body.hits.total.value).to.eql(2); + const messages: string[] = searchResult.body.hits.hits.map( (hit: { _source: { params: { message: string } } }) => hit._source.params.message ); expect(messages.sort()).to.eql(['from:default', 'from:other']); @@ -995,8 +1005,8 @@ instanceStateValue: true 'action:test.index-record', reference ); - expect(searchResult.hits.total.value).to.eql(2); - const messages: string[] = searchResult.hits.hits.map( + expect(searchResult.body.hits.total.value).to.eql(2); + const messages: string[] = searchResult.body.hits.hits.map( (hit: { _source: { params: { message: string } } }) => hit._source.params.message ); expect(messages.sort()).to.eql(['from:default:next', 'from:default:prev']); @@ -1058,7 +1068,7 @@ instanceStateValue: true 'action:test.index-record', reference ); - expect(searchResult.hits.total.value).to.eql(2); + expect(searchResult.body.hits.total.value).to.eql(2); break; default: throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); @@ -1116,7 +1126,7 @@ instanceStateValue: true 'action:test.index-record', reference ); - expect(executedActionsResult.hits.total.value).to.eql(0); + expect(executedActionsResult.body.hits.total.value).to.eql(0); break; default: throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); @@ -1174,7 +1184,7 @@ instanceStateValue: true 'action:test.index-record', reference ); - expect(executedActionsResult.hits.total.value).to.eql(0); + expect(executedActionsResult.body.hits.total.value).to.eql(0); break; default: throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); @@ -1233,7 +1243,7 @@ instanceStateValue: true 'action:test.index-record', reference ); - expect(searchResult.hits.total.value).to.eql(1); + expect(searchResult.body.hits.total.value).to.eql(1); break; default: throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/create.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/create.ts index 70cafe407de29..f481eaded4eb2 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/create.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/create.ts @@ -6,6 +6,7 @@ */ import expect from '@kbn/expect'; +import type { ApiResponse, estypes } from '@elastic/elasticsearch'; import { UserAtSpaceScenarios } from '../../scenarios'; import { checkAAD, @@ -14,13 +15,14 @@ import { getUrlPrefix, ObjectRemover, getProducerUnauthorizedErrorMessage, + TaskManagerDoc, } from '../../../common/lib'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function createAlertTests({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - const es = getService('legacyEs'); + const es = getService('es'); const supertestWithoutAuth = getService('supertestWithoutAuth'); describe('create', () => { @@ -28,11 +30,12 @@ export default function createAlertTests({ getService }: FtrProviderContext) { after(() => objectRemover.removeAll()); - async function getScheduledTask(id: string) { - return await es.get({ + async function getScheduledTask(id: string): Promise { + const scheduledTask: ApiResponse> = await es.get({ id: `task:${id}`, index: '.kibana_task_manager', }); + return scheduledTask.body._source!; } for (const scenario of UserAtSpaceScenarios) { @@ -127,9 +130,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { expect(Date.parse(response.body.created_at)).to.be.greaterThan(0); expect(Date.parse(response.body.updated_at)).to.be.greaterThan(0); - const { _source: taskRecord } = await getScheduledTask( - response.body.scheduled_task_id - ); + const taskRecord = await getScheduledTask(response.body.scheduled_task_id); expect(taskRecord.type).to.eql('task'); expect(taskRecord.task.taskType).to.eql('alerting:test.noop'); expect(JSON.parse(taskRecord.task.params)).to.eql({ diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/delete.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/delete.ts index 2cbb16ababd10..d43fb2e7d835f 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/delete.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/delete.ts @@ -19,7 +19,7 @@ import { FtrProviderContext } from '../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function createDeleteTests({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - const es = getService('legacyEs'); + const es = getService('es'); const retry = getService('retry'); const supertestWithoutAuth = getService('supertestWithoutAuth'); @@ -78,7 +78,7 @@ export default function createDeleteTests({ getService }: FtrProviderContext) { await getScheduledTask(createdAlert.scheduled_task_id); throw new Error('Should have removed scheduled task'); } catch (e) { - expect(e.status).to.eql(404); + expect(e.meta.statusCode).to.eql(404); } break; default: @@ -131,7 +131,7 @@ export default function createDeleteTests({ getService }: FtrProviderContext) { await getScheduledTask(createdAlert.scheduled_task_id); throw new Error('Should have removed scheduled task'); } catch (e) { - expect(e.status).to.eql(404); + expect(e.meta.statusCode).to.eql(404); } break; default: @@ -198,7 +198,7 @@ export default function createDeleteTests({ getService }: FtrProviderContext) { await getScheduledTask(createdAlert.scheduled_task_id); throw new Error('Should have removed scheduled task'); } catch (e) { - expect(e.status).to.eql(404); + expect(e.meta.statusCode).to.eql(404); } break; default: @@ -258,7 +258,7 @@ export default function createDeleteTests({ getService }: FtrProviderContext) { await getScheduledTask(createdAlert.scheduled_task_id); throw new Error('Should have removed scheduled task'); } catch (e) { - expect(e.status).to.eql(404); + expect(e.meta.statusCode).to.eql(404); } break; default: @@ -353,7 +353,7 @@ export default function createDeleteTests({ getService }: FtrProviderContext) { await getScheduledTask(createdAlert.scheduled_task_id); throw new Error('Should have removed scheduled task'); } catch (e) { - expect(e.status).to.eql(404); + expect(e.meta.statusCode).to.eql(404); } break; default: diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/disable.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/disable.ts index b265451bbd632..66f01000ede5e 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/disable.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/disable.ts @@ -20,7 +20,7 @@ import { // eslint-disable-next-line import/no-default-export export default function createDisableAlertTests({ getService }: FtrProviderContext) { - const es = getService('legacyEs'); + const es = getService('es'); const retry = getService('retry'); const supertest = getService('supertest'); const supertestWithoutAuth = getService('supertestWithoutAuth'); @@ -101,7 +101,7 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte await getScheduledTask(createdAlert.scheduled_task_id); throw new Error('Should have removed scheduled task'); } catch (e) { - expect(e.status).to.eql(404); + expect(e.meta.statusCode).to.eql(404); } // Ensure AAD isn't broken await checkAAD({ @@ -157,7 +157,7 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte await getScheduledTask(createdAlert.scheduled_task_id); throw new Error('Should have removed scheduled task'); } catch (e) { - expect(e.status).to.eql(404); + expect(e.meta.statusCode).to.eql(404); } break; default: @@ -217,7 +217,7 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte await getScheduledTask(createdAlert.scheduled_task_id); throw new Error('Should have removed scheduled task'); } catch (e) { - expect(e.status).to.eql(404); + expect(e.meta.statusCode).to.eql(404); } break; default: @@ -273,7 +273,7 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte await getScheduledTask(createdAlert.scheduled_task_id); throw new Error('Should have removed scheduled task'); } catch (e) { - expect(e.status).to.eql(404); + expect(e.meta.statusCode).to.eql(404); } break; default: @@ -332,7 +332,7 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte await getScheduledTask(createdAlert.scheduled_task_id); throw new Error('Should have removed scheduled task'); } catch (e) { - expect(e.status).to.eql(404); + expect(e.meta.statusCode).to.eql(404); } // Ensure AAD isn't broken await checkAAD({ diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/enable.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/enable.ts index 70e286b795720..d836f615e5349 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/enable.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/enable.ts @@ -6,6 +6,7 @@ */ import expect from '@kbn/expect'; +import type { ApiResponse, estypes } from '@elastic/elasticsearch'; import { UserAtSpaceScenarios } from '../../scenarios'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; import { @@ -16,11 +17,12 @@ import { ObjectRemover, getConsumerUnauthorizedErrorMessage, getProducerUnauthorizedErrorMessage, + TaskManagerDoc, } from '../../../common/lib'; // eslint-disable-next-line import/no-default-export export default function createEnableAlertTests({ getService }: FtrProviderContext) { - const es = getService('legacyEs'); + const es = getService('es'); const retry = getService('retry'); const supertest = getService('supertest'); const supertestWithoutAuth = getService('supertestWithoutAuth'); @@ -30,11 +32,12 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex after(() => objectRemover.removeAll()); - async function getScheduledTask(id: string) { - return await es.get({ + async function getScheduledTask(id: string): Promise { + const scheduledTask: ApiResponse> = await es.get({ id: `task:${id}`, index: '.kibana_task_manager', }); + return scheduledTask.body._source!; } for (const scenario of UserAtSpaceScenarios) { @@ -119,9 +122,7 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex .auth(user.username, user.password) .expect(200); expect(typeof updatedAlert.scheduled_task_id).to.eql('string'); - const { _source: taskRecord } = await getScheduledTask( - updatedAlert.scheduled_task_id - ); + const taskRecord = await getScheduledTask(updatedAlert.scheduled_task_id); expect(taskRecord.type).to.eql('task'); expect(taskRecord.task.taskType).to.eql('alerting:test.noop'); expect(JSON.parse(taskRecord.task.params)).to.eql({ @@ -182,7 +183,7 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex await getScheduledTask(createdAlert.scheduled_task_id); throw new Error('Should have removed scheduled task'); } catch (e) { - expect(e.status).to.eql(404); + expect(e.meta.statusCode).to.eql(404); } break; default: @@ -292,7 +293,7 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex await getScheduledTask(createdAlert.scheduled_task_id); throw new Error('Should have removed scheduled task'); } catch (e) { - expect(e.status).to.eql(404); + expect(e.meta.statusCode).to.eql(404); } break; default: @@ -351,9 +352,7 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex .auth(user.username, user.password) .expect(200); expect(typeof updatedAlert.scheduled_task_id).to.eql('string'); - const { _source: taskRecord } = await getScheduledTask( - updatedAlert.scheduled_task_id - ); + const taskRecord = await getScheduledTask(updatedAlert.scheduled_task_id); expect(taskRecord.type).to.eql('task'); expect(taskRecord.task.taskType).to.eql('alerting:test.noop'); expect(JSON.parse(taskRecord.task.params)).to.eql({ diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/health.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/health.ts index 668de3eb4fb9e..21e5c782d185c 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/health.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/health.ts @@ -20,7 +20,7 @@ import { // eslint-disable-next-line import/no-default-export export default function createFindTests({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - const es = getService('legacyEs'); + const es = getService('es'); const retry = getService('retry'); const supertestWithoutAuth = getService('supertestWithoutAuth'); const esTestIndexTool = new ESTestIndexTool(es, retry); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rbac_legacy.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rbac_legacy.ts index 2294cbcc95aa4..7bc3353898598 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rbac_legacy.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rbac_legacy.ts @@ -14,7 +14,7 @@ import { setupSpacesAndUsers } from '..'; // eslint-disable-next-line import/no-default-export export default function alertTests({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - const es = getService('legacyEs'); + const es = getService('es'); const retry = getService('retry'); const esArchiver = getService('esArchiver'); const supertestWithoutAuth = getService('supertestWithoutAuth'); @@ -204,11 +204,11 @@ export default function alertTests({ getService }: FtrProviderContext) { // ensure the alert still runs and that it can schedule actions const numberOfAlertExecutions = ( await esTestIndexTool.search('alert:test.always-firing', reference) - ).hits.total.value; + ).body.hits.total.value; const numberOfActionExecutions = ( await esTestIndexTool.search('action:test.index-record', reference) - ).hits.total.value; + ).body.hits.total.value; // wait for alert to execute and for its action to be scheduled and run await retry.try(async () => { @@ -222,8 +222,10 @@ export default function alertTests({ getService }: FtrProviderContext) { reference ); - expect(alertSearchResult.hits.total.value).to.be.greaterThan(numberOfAlertExecutions); - expect(actionSearchResult.hits.total.value).to.be.greaterThan( + expect(alertSearchResult.body.hits.total.value).to.be.greaterThan( + numberOfAlertExecutions + ); + expect(actionSearchResult.body.hits.total.value).to.be.greaterThan( numberOfActionExecutions ); }); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/es_index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/es_index.ts index 2496800b8071c..3f4cef25ff65e 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/es_index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/es_index.ts @@ -13,7 +13,7 @@ const ES_TEST_INDEX_NAME = 'functional-test-actions-index'; // eslint-disable-next-line import/no-default-export export default function indexTest({ getService }: FtrProviderContext) { - const es = getService('legacyEs'); + const es = getService('es'); const supertest = getService('supertest'); const esDeleteAllIndices = getService('esDeleteAllIndices'); @@ -149,5 +149,5 @@ async function getTestIndexItems(es: any) { index: ES_TEST_INDEX_NAME, }); - return result.hits.hits; + return result.body.hits.hits; } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/preconfigured_alert_history_connector.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/preconfigured_alert_history_connector.ts index cf8a0f99d4394..fe0f5d3ecbade 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/preconfigured_alert_history_connector.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/preconfigured_alert_history_connector.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; - +import type { ApiResponse, estypes } from '@elastic/elasticsearch'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { getTestAlertData, ObjectRemover } from '../../../../common/lib'; import { AlertHistoryDefaultIndexName } from '../../../../../../plugins/actions/common'; @@ -17,7 +17,7 @@ const ALERT_HISTORY_OVERRIDE_INDEX = 'kibana-alert-history-not-the-default'; export default function preconfiguredAlertHistoryConnectorTests({ getService, }: FtrProviderContext) { - const es = getService('legacyEs'); + const es = getService('es'); const supertest = getService('supertest'); const retry = getService('retry'); const esDeleteAllIndices = getService('esDeleteAllIndices'); @@ -66,10 +66,10 @@ export default function preconfiguredAlertHistoryConnectorTests({ await waitForStatus(response.body.id, new Set(['active'])); await retry.try(async () => { - const result = await es.search({ + const result: ApiResponse> = await es.search({ index: AlertHistoryDefaultIndexName, }); - const indexedItems = result.hits.hits; + const indexedItems = result.body.hits.hits; expect(indexedItems.length).to.eql(1); const indexedDoc = indexedItems[0]._source; @@ -104,10 +104,10 @@ export default function preconfiguredAlertHistoryConnectorTests({ await waitForStatus(response.body.id, new Set(['active'])); await retry.try(async () => { - const result = await es.search({ + const result: ApiResponse> = await es.search({ index: ALERT_HISTORY_OVERRIDE_INDEX, }); - const indexedItems = result.hits.hits; + const indexedItems = result.body.hits.hits; expect(indexedItems.length).to.eql(1); const indexedDoc = indexedItems[0]._source; diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/enqueue.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/enqueue.ts index b6e47df315273..f937e63840937 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/enqueue.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/enqueue.ts @@ -6,6 +6,7 @@ */ import expect from '@kbn/expect'; +import type { estypes } from '@elastic/elasticsearch'; import { Spaces } from '../../scenarios'; import { ESTestIndexTool, @@ -18,7 +19,7 @@ import { FtrProviderContext } from '../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - const es = getService('legacyEs'); + const es = getService('es'); const retry = getService('retry'); const esTestIndexTool = new ESTestIndexTool(es, retry); @@ -70,7 +71,7 @@ export default function ({ getService }: FtrProviderContext) { }); it('should cleanup task after a failure', async () => { - const testStart = new Date(); + const testStart = new Date().toISOString(); const { body: createdAction } = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`) .set('kbn-xsrf', 'foo') @@ -135,7 +136,7 @@ export default function ({ getService }: FtrProviderContext) { }, }, }); - expect(searchResult.hits.total.value).to.eql(0); + expect((searchResult.body.hits.total as estypes.SearchTotalHits).value).to.eql(0); }); }); }); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/execute.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/execute.ts index 38f3a17f317c2..d765512d6b5f1 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/execute.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/execute.ts @@ -22,7 +22,7 @@ const NANOS_IN_MILLIS = 1000 * 1000; // eslint-disable-next-line import/no-default-export export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - const es = getService('legacyEs'); + const es = getService('es'); const retry = getService('retry'); const esTestIndexTool = new ESTestIndexTool(es, retry); @@ -76,8 +76,8 @@ export default function ({ getService }: FtrProviderContext) { expect(response.status).to.eql(200); expect(response.body).to.be.an('object'); const searchResult = await esTestIndexTool.search('action:test.index-record', reference); - expect(searchResult.hits.total.value).to.eql(1); - const indexedRecord = searchResult.hits.hits[0]; + expect(searchResult.body.hits.total.value).to.eql(1); + const indexedRecord = searchResult.body.hits.hits[0]; expect(indexedRecord._source).to.eql({ params: { reference, @@ -211,8 +211,8 @@ export default function ({ getService }: FtrProviderContext) { expect(response.status).to.eql(200); const searchResult = await esTestIndexTool.search('action:test.authorization', reference); - expect(searchResult.hits.total.value).to.eql(1); - const indexedRecord = searchResult.hits.hits[0]; + expect(searchResult.body.hits.total.value).to.eql(1); + const indexedRecord = searchResult.body.hits.hits[0]; expect(indexedRecord._source.state).to.eql({ callClusterSuccess: true, callScopedClusterSuccess: true, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_base.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_base.ts index 2ddea4e3ef299..999135993d069 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_base.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_base.ts @@ -7,6 +7,7 @@ import expect from '@kbn/expect'; import { omit } from 'lodash'; +import type { ApiResponse, estypes } from '@elastic/elasticsearch'; import { Response as SupertestResponse } from 'supertest'; import { RecoveredActionGroup } from '../../../../../plugins/alerting/common'; import { Space } from '../../../common/types'; @@ -21,10 +22,15 @@ import { ensureDatetimeIsWithinRange, TaskManagerUtils, } from '../../../common/lib'; +import { + TaskRunning, + TaskRunningStage, +} from '../../../../../plugins/task_manager/server/task_running'; +import { ConcreteTaskInstance } from '../../../../../plugins/task_manager/server'; export function alertTests({ getService }: FtrProviderContext, space: Space) { const supertestWithoutAuth = getService('supertestWithoutAuth'); - const es = getService('legacyEs'); + const es = getService('es'); const retry = getService('retry'); const esTestIndexTool = new ESTestIndexTool(es, retry); const taskManagerUtils = new TaskManagerUtils(es, retry); @@ -292,7 +298,7 @@ instanceStateValue: true await taskManagerUtils.waitForActionTaskParamsToBeCleanedUp(testStart); const actionTestRecord = await esTestIndexTool.search('action:test.index-record', reference); - expect(actionTestRecord.hits.total.value).to.eql(0); + expect(actionTestRecord.body.hits.total.value).to.eql(0); objectRemover.add(space.id, alertId, 'rule', 'alerting'); }); @@ -327,7 +333,7 @@ instanceStateValue: true it('should handle custom retry logic', async () => { // We'll use this start time to query tasks created after this point - const testStart = new Date(); + const testStart = new Date().toISOString(); // We have to provide the test.rate-limit the next runAt, for testing purposes const retryDate = new Date(Date.now() + 60000); @@ -370,8 +376,12 @@ instanceStateValue: true expect(response.statusCode).to.eql(200); objectRemover.add(space.id, response.body.id, 'rule', 'alerting'); - const scheduledActionTask = await retry.try(async () => { - const searchResult = await es.search({ + const scheduledActionTask: estypes.SearchHit< + TaskRunning + > = await retry.try(async () => { + const searchResult: ApiResponse< + estypes.SearchResponse> + > = await es.search({ index: '.kibana_task_manager', body: { query: { @@ -404,10 +414,10 @@ instanceStateValue: true }, }, }); - expect(searchResult.hits.total.value).to.eql(1); - return searchResult.hits.hits[0]; + expect((searchResult.body.hits.total as estypes.SearchTotalHits).value).to.eql(1); + return searchResult.body.hits.hits[0]; }); - expect(scheduledActionTask._source.task.runAt).to.eql(retryDate.toISOString()); + expect(scheduledActionTask._source!.task.runAt).to.eql(retryDate.toISOString()); }); it('should have proper callCluster and savedObjectsClient authorization for alert type executor', async () => { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/alert.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/alert.ts index ebc03ffb0e952..29f2ed40be790 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/alert.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/alert.ts @@ -32,7 +32,7 @@ const ES_GROUPS_TO_WRITE = 3; export default function alertTests({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const retry = getService('retry'); - const es = getService('legacyEs'); + const es = getService('es'); const esTestIndexTool = new ESTestIndexTool(es, retry); const esTestIndexToolOutput = new ESTestIndexTool(es, retry, ES_TEST_OUTPUT_INDEX_NAME); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/create_test_data.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/create_test_data.ts index 8c8d8e84132a9..f3c707c58af1c 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/create_test_data.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/create_test_data.ts @@ -54,7 +54,7 @@ async function createEsDocument(es: any, epochMillis: number, testedValue: numbe body: document, }); - if (response.result !== 'created') { + if (response.body.result !== 'created') { throw new Error(`document not created: ${JSON.stringify(response)}`); } } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/alert.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/alert.ts index 3d7e391d7530f..6ea4cbf0e96ba 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/alert.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/alert.ts @@ -31,7 +31,7 @@ const ALERT_INTERVAL_MILLIS = ALERT_INTERVAL_SECONDS * 1000; export default function alertTests({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const retry = getService('retry'); - const es = getService('legacyEs'); + const es = getService('es'); const esTestIndexTool = new ESTestIndexTool(es, retry); const esTestIndexToolOutput = new ESTestIndexTool(es, retry, ES_TEST_OUTPUT_INDEX_NAME); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/create_test_data.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/create_test_data.ts index 9c36831f61f76..b9faadcd3d4b7 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/create_test_data.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/create_test_data.ts @@ -66,7 +66,7 @@ async function createEsDocument(es: any, epochMillis: number, testedValue: numbe }); // console.log(`writing document to ${ES_TEST_INDEX_NAME}:`, JSON.stringify(document, null, 4)); - if (response.result !== 'created') { + if (response.body.result !== 'created') { throw new Error(`document not created: ${JSON.stringify(response)}`); } } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/fields_endpoint.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/fields_endpoint.ts index 4971a09f9632c..0a48e206e020e 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/fields_endpoint.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/fields_endpoint.ts @@ -17,7 +17,7 @@ const API_URI = 'api/triggers_actions_ui/data/_fields'; export default function fieldsEndpointTests({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const retry = getService('retry'); - const es = getService('legacyEs'); + const es = getService('es'); const esTestIndexTool = new ESTestIndexTool(es, retry); describe('fields endpoint', () => { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/indices_endpoint.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/indices_endpoint.ts index d994ada5842d3..6d4f4a6aa6cc9 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/indices_endpoint.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/indices_endpoint.ts @@ -18,7 +18,7 @@ const API_URI = 'api/triggers_actions_ui/data/_indices'; export default function indicesEndpointTests({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const retry = getService('retry'); - const es = getService('legacyEs'); + const es = getService('es'); const esTestIndexTool = new ESTestIndexTool(es, retry); describe('indices endpoint', () => { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/time_series_query_endpoint.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/time_series_query_endpoint.ts index 49fe427e50af4..741f198607c3e 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/time_series_query_endpoint.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/time_series_query_endpoint.ts @@ -51,7 +51,7 @@ const START_DATE_MINUS_2INTERVALS = getStartDate(-2 * INTERVAL_MILLIS); export default function timeSeriesQueryEndpointTests({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const retry = getService('retry'); - const es = getService('legacyEs'); + const es = getService('es'); const esTestIndexTool = new ESTestIndexTool(es, retry); describe('time_series_query endpoint', () => { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts index 96534c192d67c..6f0f78b6d63ee 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts @@ -6,6 +6,7 @@ */ import expect from '@kbn/expect'; +import type { ApiResponse, estypes } from '@elastic/elasticsearch'; import { Spaces } from '../../scenarios'; import { checkAAD, @@ -13,24 +14,26 @@ import { getTestAlertData, ObjectRemover, getConsumerUnauthorizedErrorMessage, + TaskManagerDoc, } from '../../../common/lib'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function createAlertTests({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - const es = getService('legacyEs'); + const es = getService('es'); describe('create', () => { const objectRemover = new ObjectRemover(supertest); after(() => objectRemover.removeAll()); - async function getScheduledTask(id: string) { - return await es.get({ + async function getScheduledTask(id: string): Promise { + const scheduledTask: ApiResponse> = await es.get({ id: `task:${id}`, index: '.kibana_task_manager', }); + return scheduledTask.body._source!; } it('should handle create alert request appropriately', async () => { @@ -96,7 +99,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { expect(Date.parse(response.body.updated_at)).to.eql(Date.parse(response.body.created_at)); expect(typeof response.body.scheduled_task_id).to.be('string'); - const { _source: taskRecord } = await getScheduledTask(response.body.scheduled_task_id); + const taskRecord = await getScheduledTask(response.body.scheduled_task_id); expect(taskRecord.type).to.eql('task'); expect(taskRecord.task.taskType).to.eql('alerting:test.noop'); expect(JSON.parse(taskRecord.task.params)).to.eql({ @@ -328,7 +331,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { expect(Date.parse(response.body.updatedAt)).to.eql(Date.parse(response.body.createdAt)); expect(typeof response.body.scheduledTaskId).to.be('string'); - const { _source: taskRecord } = await getScheduledTask(response.body.scheduledTaskId); + const taskRecord = await getScheduledTask(response.body.scheduledTaskId); expect(taskRecord.type).to.eql('task'); expect(taskRecord.task.taskType).to.eql('alerting:test.noop'); expect(JSON.parse(taskRecord.task.params)).to.eql({ diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/delete.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/delete.ts index 04ae217d47760..0a2df70b6316a 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/delete.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/delete.ts @@ -13,7 +13,7 @@ import { FtrProviderContext } from '../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function createDeleteTests({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - const es = getService('legacyEs'); + const es = getService('es'); describe('delete', () => { const objectRemover = new ObjectRemover(supertest); @@ -43,7 +43,7 @@ export default function createDeleteTests({ getService }: FtrProviderContext) { await getScheduledTask(createdAlert.scheduledTaskId); throw new Error('Should have removed scheduled task'); } catch (e) { - expect(e.status).to.eql(404); + expect(e.meta.statusCode).to.eql(404); } }); @@ -81,7 +81,7 @@ export default function createDeleteTests({ getService }: FtrProviderContext) { await getScheduledTask(createdAlert.scheduledTaskId); throw new Error('Should have removed scheduled task'); } catch (e) { - expect(e.status).to.eql(404); + expect(e.meta.statusCode).to.eql(404); } }); }); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/disable.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/disable.ts index 60749343cf269..7e93cf453929b 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/disable.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/disable.ts @@ -18,7 +18,7 @@ import { // eslint-disable-next-line import/no-default-export export default function createDisableAlertTests({ getService }: FtrProviderContext) { - const es = getService('legacyEs'); + const es = getService('es'); const supertestWithoutAuth = getService('supertestWithoutAuth'); describe('disable', () => { @@ -48,7 +48,7 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte await getScheduledTask(createdAlert.scheduledTaskId); throw new Error('Should have removed scheduled task'); } catch (e) { - expect(e.status).to.eql(404); + expect(e.meta.statusCode).to.eql(404); } // Ensure AAD isn't broken @@ -93,7 +93,7 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte await getScheduledTask(createdAlert.scheduledTaskId); throw new Error('Should have removed scheduled task'); } catch (e) { - expect(e.status).to.eql(404); + expect(e.meta.statusCode).to.eql(404); } // Ensure AAD isn't broken diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/enable.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/enable.ts index f0b0d1f34a277..881931252ed5f 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/enable.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/enable.ts @@ -6,6 +6,7 @@ */ import expect from '@kbn/expect'; +import type { ApiResponse, estypes } from '@elastic/elasticsearch'; import { Spaces } from '../../scenarios'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; import { @@ -14,11 +15,12 @@ import { getUrlPrefix, getTestAlertData, ObjectRemover, + TaskManagerDoc, } from '../../../common/lib'; // eslint-disable-next-line import/no-default-export export default function createEnableAlertTests({ getService }: FtrProviderContext) { - const es = getService('legacyEs'); + const es = getService('es'); const supertestWithoutAuth = getService('supertestWithoutAuth'); describe('enable', () => { @@ -27,11 +29,12 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex after(() => objectRemover.removeAll()); - async function getScheduledTask(id: string) { - return await es.get({ + async function getScheduledTask(id: string): Promise { + const scheduledTask: ApiResponse> = await es.get({ id: `task:${id}`, index: '.kibana_task_manager', }); + return scheduledTask.body._source!; } it('should handle enable alert request appropriately', async () => { @@ -49,7 +52,7 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex .set('kbn-xsrf', 'foo') .expect(200); expect(typeof updatedAlert.scheduled_task_id).to.eql('string'); - const { _source: taskRecord } = await getScheduledTask(updatedAlert.scheduled_task_id); + const taskRecord = await getScheduledTask(updatedAlert.scheduled_task_id); expect(taskRecord.type).to.eql('task'); expect(taskRecord.task.taskType).to.eql('alerting:test.noop'); expect(JSON.parse(taskRecord.task.params)).to.eql({ @@ -100,7 +103,7 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex .set('kbn-xsrf', 'foo') .expect(200); expect(typeof updatedAlert.scheduled_task_id).to.eql('string'); - const { _source: taskRecord } = await getScheduledTask(updatedAlert.scheduled_task_id); + const taskRecord = await getScheduledTask(updatedAlert.scheduled_task_id); expect(taskRecord.type).to.eql('task'); expect(taskRecord.task.taskType).to.eql('alerting:test.noop'); expect(JSON.parse(taskRecord.task.params)).to.eql({ diff --git a/x-pack/test/plugin_api_integration/test_suites/event_log/service_api_integration.ts b/x-pack/test/plugin_api_integration/test_suites/event_log/service_api_integration.ts index 960deb692ac8d..e17b1400e6781 100644 --- a/x-pack/test/plugin_api_integration/test_suites/event_log/service_api_integration.ts +++ b/x-pack/test/plugin_api_integration/test_suites/event_log/service_api_integration.ts @@ -12,7 +12,7 @@ import { IEvent } from '../../../../plugins/event_log/server'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { - const es = getService('legacyEs'); + const es = getService('es'); const supertest = getService('supertest'); const log = getService('log'); const config = getService('config'); @@ -41,7 +41,7 @@ export default function ({ getService }: FtrProviderContext) { .find((val: string) => val === '--xpack.eventLog.indexEntries=true'); const result = await isIndexingEntries(); const exists = await es.indices.exists({ index: '.kibana-event-log-*' }); - expect(exists).to.be.eql(true); + expect(exists.body).to.be.eql(true); expect(configValue).to.be.eql( `--xpack.eventLog.indexEntries=${result.body.isIndexingEntries}` ); From dd20b8adb65c2d974ee8c04fe34fd6b3aac36b97 Mon Sep 17 00:00:00 2001 From: Aleh Zasypkin Date: Thu, 24 Jun 2021 18:22:14 +0200 Subject: [PATCH 36/69] Avoid using deprecated camelCase parameters for SAML APIs. (#103091) --- .../security/server/authentication/providers/saml.test.ts | 8 ++++---- .../security/server/authentication/providers/saml.ts | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/security/server/authentication/providers/saml.test.ts b/x-pack/plugins/security/server/authentication/providers/saml.test.ts index dfcdb66e61c35..4a32383d18dec 100644 --- a/x-pack/plugins/security/server/authentication/providers/saml.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/saml.test.ts @@ -1186,7 +1186,7 @@ describe('SAMLAuthenticationProvider', () => { expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({ method: 'POST', path: '/_security/saml/invalidate', - body: { queryString: 'SAMLRequest=xxx%20yyy', realm: 'test-realm' }, + body: { query_string: 'SAMLRequest=xxx%20yyy', realm: 'test-realm' }, }); }); @@ -1286,7 +1286,7 @@ describe('SAMLAuthenticationProvider', () => { expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({ method: 'POST', path: '/_security/saml/invalidate', - body: { queryString: 'SAMLRequest=xxx%20yyy', realm: 'test-realm' }, + body: { query_string: 'SAMLRequest=xxx%20yyy', realm: 'test-realm' }, }); }); @@ -1305,7 +1305,7 @@ describe('SAMLAuthenticationProvider', () => { expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({ method: 'POST', path: '/_security/saml/invalidate', - body: { queryString: 'SAMLRequest=xxx%20yyy', realm: 'test-realm' }, + body: { query_string: 'SAMLRequest=xxx%20yyy', realm: 'test-realm' }, }); }); @@ -1324,7 +1324,7 @@ describe('SAMLAuthenticationProvider', () => { expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({ method: 'POST', path: '/_security/saml/invalidate', - body: { queryString: 'SAMLRequest=xxx%20yyy', realm: 'test-realm' }, + body: { query_string: 'SAMLRequest=xxx%20yyy', realm: 'test-realm' }, }); }); diff --git a/x-pack/plugins/security/server/authentication/providers/saml.ts b/x-pack/plugins/security/server/authentication/providers/saml.ts index ea818e5df6e12..37e7e868e4d3d 100644 --- a/x-pack/plugins/security/server/authentication/providers/saml.ts +++ b/x-pack/plugins/security/server/authentication/providers/saml.ts @@ -624,9 +624,9 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider { await this.options.client.asInternalUser.transport.request({ method: 'POST', path: '/_security/saml/invalidate', - // Elasticsearch expects `queryString` without leading `?`, so we should strip it with `slice`. + // Elasticsearch expects `query_string` without leading `?`, so we should strip it with `slice`. body: { - queryString: request.url.search ? request.url.search.slice(1) : '', + query_string: request.url.search ? request.url.search.slice(1) : '', realm: this.realm, }, }) From 5e898734d5c5fcd4d1d9cffaf4a02d78b94e6bd7 Mon Sep 17 00:00:00 2001 From: Bryan Clement Date: Thu, 24 Jun 2021 09:31:57 -0700 Subject: [PATCH 37/69] [Asset management] Osquery app bug squashing (#102406) * only display healthy agents to query * updated toasts to clear on update * null checking aggBuckets * properly display expired actions * clear the error toasts on success * review comments Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../action_results/action_results_summary.tsx | 58 ++++++++++++------- .../action_results/use_action_results.ts | 14 ++--- .../public/actions/use_action_details.ts | 10 ++-- .../osquery/public/actions/use_all_actions.ts | 10 ++-- .../agent_policies/use_agent_policies.ts | 10 ++-- .../public/agent_policies/use_agent_policy.ts | 10 ++-- .../osquery/public/agents/use_agent_groups.ts | 10 ++-- .../public/agents/use_agent_policies.ts | 10 ++-- .../osquery/public/agents/use_agent_status.ts | 10 ++-- .../osquery/public/agents/use_all_agents.ts | 25 ++++---- .../public/agents/use_osquery_policies.ts | 10 ++-- .../public/common/hooks/use_error_toast.tsx | 26 +++++++++ .../common/hooks/use_osquery_integration.tsx | 9 ++- .../public/live_queries/form/index.tsx | 28 ++++++--- .../osquery/public/queries/edit/tabs.tsx | 11 +++- .../osquery/public/results/use_all_results.ts | 10 ++-- .../routes/live_queries/details/index.tsx | 23 ++++++-- .../active_state_switch.tsx | 5 +- .../scheduled_query_groups/form/index.tsx | 5 +- .../osquery/server/lib/parse_agent_groups.ts | 4 +- 20 files changed, 186 insertions(+), 112 deletions(-) create mode 100644 x-pack/plugins/osquery/public/common/hooks/use_error_toast.tsx diff --git a/x-pack/plugins/osquery/public/action_results/action_results_summary.tsx b/x-pack/plugins/osquery/public/action_results/action_results_summary.tsx index 23eaaeac1439d..257c89047aab0 100644 --- a/x-pack/plugins/osquery/public/action_results/action_results_summary.tsx +++ b/x-pack/plugins/osquery/public/action_results/action_results_summary.tsx @@ -36,6 +36,7 @@ const StyledEuiCard = styled(EuiCard)` interface ActionResultsSummaryProps { actionId: string; + expirationDate: Date; agentIds?: string[]; isLive?: boolean; } @@ -48,6 +49,7 @@ const renderErrorMessage = (error: string) => ( const ActionResultsSummaryComponent: React.FC = ({ actionId, + expirationDate, agentIds, isLive, }) => { @@ -56,6 +58,7 @@ const ActionResultsSummaryComponent: React.FC = ({ const [pageIndex, setPageIndex] = useState(0); // @ts-expect-error update types const [pageSize, setPageSize] = useState(50); + const expired = useMemo(() => expirationDate < new Date(), [expirationDate]); const { // @ts-expect-error update types data: { aggregations, edges }, @@ -66,7 +69,7 @@ const ActionResultsSummaryComponent: React.FC = ({ limit: pageSize, direction: Direction.asc, sortField: '@timestamp', - isLive, + isLive: !expired && isLive, }); const { data: logsResults } = useAllResults({ @@ -79,7 +82,7 @@ const ActionResultsSummaryComponent: React.FC = ({ direction: Direction.asc, }, ], - isLive, + isLive: !expired && isLive, }); const notRespondedCount = useMemo(() => { @@ -108,9 +111,13 @@ const ActionResultsSummaryComponent: React.FC = ({ description: aggregations.successful, }, { - title: i18n.translate('xpack.osquery.liveQueryActionResults.summary.pendingLabelText', { - defaultMessage: 'Not yet responded', - }), + title: expired + ? i18n.translate('xpack.osquery.liveQueryActionResults.summary.expiredLabelText', { + defaultMessage: 'Expired', + }) + : i18n.translate('xpack.osquery.liveQueryActionResults.summary.pendingLabelText', { + defaultMessage: 'Not yet responded', + }), description: notRespondedCount, }, { @@ -124,7 +131,7 @@ const ActionResultsSummaryComponent: React.FC = ({ ), }, ], - [agentIds, aggregations.failed, aggregations.successful, notRespondedCount] + [agentIds, aggregations.failed, aggregations.successful, notRespondedCount, expired] ); const renderAgentIdColumn = useCallback( @@ -158,23 +165,30 @@ const ActionResultsSummaryComponent: React.FC = ({ [logsResults] ); - const renderStatusColumn = useCallback((_, item) => { - if (!item.fields.completed_at) { - return i18n.translate('xpack.osquery.liveQueryActionResults.table.pendingStatusText', { - defaultMessage: 'pending', - }); - } + const renderStatusColumn = useCallback( + (_, item) => { + if (!item.fields.completed_at) { + return expired + ? i18n.translate('xpack.osquery.liveQueryActionResults.table.expiredStatusText', { + defaultMessage: 'expired', + }) + : i18n.translate('xpack.osquery.liveQueryActionResults.table.pendingStatusText', { + defaultMessage: 'pending', + }); + } - if (item.fields['error.keyword']) { - return i18n.translate('xpack.osquery.liveQueryActionResults.table.errorStatusText', { - defaultMessage: 'error', - }); - } + if (item.fields['error.keyword']) { + return i18n.translate('xpack.osquery.liveQueryActionResults.table.errorStatusText', { + defaultMessage: 'error', + }); + } - return i18n.translate('xpack.osquery.liveQueryActionResults.table.successStatusText', { - defaultMessage: 'success', - }); - }, []); + return i18n.translate('xpack.osquery.liveQueryActionResults.table.successStatusText', { + defaultMessage: 'success', + }); + }, + [expired] + ); const columns = useMemo( () => [ @@ -227,7 +241,7 @@ const ActionResultsSummaryComponent: React.FC = ({ - {notRespondedCount ? : null} + {!expired && notRespondedCount ? : null} { - const { - data, - notifications: { toasts }, - } = useKibana().services; + const { data } = useKibana().services; + const setErrorToast = useErrorToast(); return useQuery( ['actionResults', { actionId }], @@ -103,9 +102,9 @@ export const useActionResults = ({ aggregations: { totalResponded, // @ts-expect-error update types - successful: aggsBuckets.find((bucket) => bucket.key === 'success')?.doc_count ?? 0, + successful: aggsBuckets?.find((bucket) => bucket.key === 'success')?.doc_count ?? 0, // @ts-expect-error update types - failed: aggsBuckets.find((bucket) => bucket.key === 'error')?.doc_count ?? 0, + failed: aggsBuckets?.find((bucket) => bucket.key === 'error')?.doc_count ?? 0, }, inspect: getInspectResponse(responseData, {} as InspectResponse), }; @@ -124,8 +123,9 @@ export const useActionResults = ({ refetchInterval: isLive ? 1000 : false, keepPreviousData: true, enabled: !skip && !!agentIds?.length, + onSuccess: () => setErrorToast(), onError: (error: Error) => - toasts.addError(error, { + setErrorToast(error, { title: i18n.translate('xpack.osquery.action_results.fetchError', { defaultMessage: 'Error while fetching action results', }), diff --git a/x-pack/plugins/osquery/public/actions/use_action_details.ts b/x-pack/plugins/osquery/public/actions/use_action_details.ts index bb260cd78ca76..445912b27bc93 100644 --- a/x-pack/plugins/osquery/public/actions/use_action_details.ts +++ b/x-pack/plugins/osquery/public/actions/use_action_details.ts @@ -18,6 +18,7 @@ import { import { ESTermQuery } from '../../common/typed_json'; import { getInspectResponse, InspectResponse } from './helpers'; +import { useErrorToast } from '../common/hooks/use_error_toast'; export interface ActionDetailsArgs { actionDetails: Record; @@ -33,10 +34,8 @@ interface UseActionDetails { } export const useActionDetails = ({ actionId, filterQuery, skip = false }: UseActionDetails) => { - const { - data, - notifications: { toasts }, - } = useKibana().services; + const { data } = useKibana().services; + const setErrorToast = useErrorToast(); return useQuery( ['actionDetails', { actionId, filterQuery }], @@ -61,8 +60,9 @@ export const useActionDetails = ({ actionId, filterQuery, skip = false }: UseAct }, { enabled: !skip, + onSuccess: () => setErrorToast(), onError: (error: Error) => - toasts.addError(error, { + setErrorToast(error, { title: i18n.translate('xpack.osquery.action_details.fetchError', { defaultMessage: 'Error while fetching action details', }), diff --git a/x-pack/plugins/osquery/public/actions/use_all_actions.ts b/x-pack/plugins/osquery/public/actions/use_all_actions.ts index 375d108c4dd8b..ae872d3c1ed52 100644 --- a/x-pack/plugins/osquery/public/actions/use_all_actions.ts +++ b/x-pack/plugins/osquery/public/actions/use_all_actions.ts @@ -21,6 +21,7 @@ import { import { ESTermQuery } from '../../common/typed_json'; import { generateTablePaginationOptions, getInspectResponse, InspectResponse } from './helpers'; +import { useErrorToast } from '../common/hooks/use_error_toast'; export interface ActionsArgs { actions: ActionEdges; @@ -48,10 +49,8 @@ export const useAllActions = ({ filterQuery, skip = false, }: UseAllActions) => { - const { - data, - notifications: { toasts }, - } = useKibana().services; + const { data } = useKibana().services; + const setErrorToast = useErrorToast(); return useQuery( ['actions', { activePage, direction, limit, sortField }], @@ -82,8 +81,9 @@ export const useAllActions = ({ { keepPreviousData: true, enabled: !skip, + onSuccess: () => setErrorToast(), onError: (error: Error) => - toasts.addError(error, { + setErrorToast(error, { title: i18n.translate('xpack.osquery.all_actions.fetchError', { defaultMessage: 'Error while fetching actions', }), diff --git a/x-pack/plugins/osquery/public/agent_policies/use_agent_policies.ts b/x-pack/plugins/osquery/public/agent_policies/use_agent_policies.ts index d4bd0a1f4277f..6f87610667198 100644 --- a/x-pack/plugins/osquery/public/agent_policies/use_agent_policies.ts +++ b/x-pack/plugins/osquery/public/agent_policies/use_agent_policies.ts @@ -14,12 +14,11 @@ import { GetAgentPoliciesResponse, GetAgentPoliciesResponseItem, } from '../../../fleet/common'; +import { useErrorToast } from '../common/hooks/use_error_toast'; export const useAgentPolicies = () => { - const { - http, - notifications: { toasts }, - } = useKibana().services; + const { http } = useKibana().services; + const setErrorToast = useErrorToast(); return useQuery( ['agentPolicies'], @@ -34,8 +33,9 @@ export const useAgentPolicies = () => { placeholderData: [], keepPreviousData: true, select: (response) => response.items, + onSuccess: () => setErrorToast(), onError: (error) => - toasts.addError(error as Error, { + setErrorToast(error as Error, { title: i18n.translate('xpack.osquery.agent_policies.fetchError', { defaultMessage: 'Error while fetching agent policies', }), diff --git a/x-pack/plugins/osquery/public/agent_policies/use_agent_policy.ts b/x-pack/plugins/osquery/public/agent_policies/use_agent_policy.ts index e87d8d1c9f28e..dcebf136b6773 100644 --- a/x-pack/plugins/osquery/public/agent_policies/use_agent_policy.ts +++ b/x-pack/plugins/osquery/public/agent_policies/use_agent_policy.ts @@ -10,6 +10,7 @@ import { useQuery } from 'react-query'; import { i18n } from '@kbn/i18n'; import { useKibana } from '../common/lib/kibana'; import { agentPolicyRouteService } from '../../../fleet/common'; +import { useErrorToast } from '../common/hooks/use_error_toast'; interface UseAgentPolicy { policyId: string; @@ -17,10 +18,8 @@ interface UseAgentPolicy { } export const useAgentPolicy = ({ policyId, skip }: UseAgentPolicy) => { - const { - http, - notifications: { toasts }, - } = useKibana().services; + const { http } = useKibana().services; + const setErrorToast = useErrorToast(); return useQuery( ['agentPolicy', { policyId }], @@ -29,8 +28,9 @@ export const useAgentPolicy = ({ policyId, skip }: UseAgentPolicy) => { enabled: !skip, keepPreviousData: true, select: (response) => response.item, + onSuccess: () => setErrorToast(), onError: (error: Error) => - toasts.addError(error, { + setErrorToast(error, { title: i18n.translate('xpack.osquery.agent_policy_details.fetchError', { defaultMessage: 'Error while fetching agent policy details', }), diff --git a/x-pack/plugins/osquery/public/agents/use_agent_groups.ts b/x-pack/plugins/osquery/public/agents/use_agent_groups.ts index 44737af9d3477..bfa224a23135b 100644 --- a/x-pack/plugins/osquery/public/agents/use_agent_groups.ts +++ b/x-pack/plugins/osquery/public/agents/use_agent_groups.ts @@ -18,6 +18,7 @@ import { import { generateTablePaginationOptions, processAggregations } from './helpers'; import { Overlap, Group } from './types'; +import { useErrorToast } from '../common/hooks/use_error_toast'; interface UseAgentGroups { osqueryPolicies: string[]; @@ -25,10 +26,8 @@ interface UseAgentGroups { } export const useAgentGroups = ({ osqueryPolicies, osqueryPoliciesLoading }: UseAgentGroups) => { - const { - data, - notifications: { toasts }, - } = useKibana().services; + const { data } = useKibana().services; + const setErrorToast = useErrorToast(); const { agentPoliciesLoading, agentPolicyById } = useAgentPolicies(osqueryPolicies); const [platforms, setPlatforms] = useState([]); @@ -100,8 +99,9 @@ export const useAgentGroups = ({ osqueryPolicies, osqueryPoliciesLoading }: UseA }, { enabled: !osqueryPoliciesLoading && !agentPoliciesLoading, + onSuccess: () => setErrorToast(), onError: (error) => - toasts.addError(error as Error, { + setErrorToast(error as Error, { title: i18n.translate('xpack.osquery.agent_groups.fetchError', { defaultMessage: 'Error while fetching agent groups', }), diff --git a/x-pack/plugins/osquery/public/agents/use_agent_policies.ts b/x-pack/plugins/osquery/public/agents/use_agent_policies.ts index ecb95fff8838e..115b5af9d3a1b 100644 --- a/x-pack/plugins/osquery/public/agents/use_agent_policies.ts +++ b/x-pack/plugins/osquery/public/agents/use_agent_policies.ts @@ -10,20 +10,20 @@ import { useQueries, UseQueryResult } from 'react-query'; import { i18n } from '@kbn/i18n'; import { useKibana } from '../common/lib/kibana'; import { agentPolicyRouteService, GetOneAgentPolicyResponse } from '../../../fleet/common'; +import { useErrorToast } from '../common/hooks/use_error_toast'; export const useAgentPolicies = (policyIds: string[] = []) => { - const { - http, - notifications: { toasts }, - } = useKibana().services; + const { http } = useKibana().services; + const setErrorToast = useErrorToast(); const agentResponse = useQueries( policyIds.map((policyId) => ({ queryKey: ['agentPolicy', policyId], queryFn: () => http.get(agentPolicyRouteService.getInfoPath(policyId)), enabled: policyIds.length > 0, + onSuccess: () => setErrorToast(), onError: (error) => - toasts.addError(error as Error, { + setErrorToast(error as Error, { title: i18n.translate('xpack.osquery.action_policy_details.fetchError', { defaultMessage: 'Error while fetching policy details', }), diff --git a/x-pack/plugins/osquery/public/agents/use_agent_status.ts b/x-pack/plugins/osquery/public/agents/use_agent_status.ts index 4954eb0dc80c4..c8bc8d2fe5c0e 100644 --- a/x-pack/plugins/osquery/public/agents/use_agent_status.ts +++ b/x-pack/plugins/osquery/public/agents/use_agent_status.ts @@ -9,6 +9,7 @@ import { i18n } from '@kbn/i18n'; import { useQuery } from 'react-query'; import { GetAgentStatusResponse, agentRouteService } from '../../../fleet/common'; +import { useErrorToast } from '../common/hooks/use_error_toast'; import { useKibana } from '../common/lib/kibana'; interface UseAgentStatus { @@ -17,10 +18,8 @@ interface UseAgentStatus { } export const useAgentStatus = ({ policyId, skip }: UseAgentStatus) => { - const { - http, - notifications: { toasts }, - } = useKibana().services; + const { http } = useKibana().services; + const setErrorToast = useErrorToast(); return useQuery( ['agentStatus', policyId], @@ -38,8 +37,9 @@ export const useAgentStatus = ({ policyId, skip }: UseAgentStatus) => { { enabled: !skip, select: (response) => response.results, + onSuccess: () => setErrorToast(), onError: (error) => - toasts.addError(error as Error, { + setErrorToast(error as Error, { title: i18n.translate('xpack.osquery.agent_status.fetchError', { defaultMessage: 'Error while fetching agent status', }), diff --git a/x-pack/plugins/osquery/public/agents/use_all_agents.ts b/x-pack/plugins/osquery/public/agents/use_all_agents.ts index 674deb3b339bd..30ba4d2f57907 100644 --- a/x-pack/plugins/osquery/public/agents/use_all_agents.ts +++ b/x-pack/plugins/osquery/public/agents/use_all_agents.ts @@ -9,6 +9,7 @@ import { i18n } from '@kbn/i18n'; import { useQuery } from 'react-query'; import { GetAgentsResponse, agentRouteService } from '../../../fleet/common'; +import { useErrorToast } from '../common/hooks/use_error_toast'; import { useKibana } from '../common/lib/kibana'; interface UseAllAgents { @@ -28,36 +29,30 @@ export const useAllAgents = ( opts: RequestOptions = { perPage: 9000 } ) => { const { perPage } = opts; - const { - http, - notifications: { toasts }, - } = useKibana().services; + const { http } = useKibana().services; + const setErrorToast = useErrorToast(); const { isLoading: agentsLoading, data: agentData } = useQuery( ['agents', osqueryPolicies, searchValue, perPage], () => { - const kueryFragments: string[] = []; - if (osqueryPolicies.length) { - kueryFragments.push(`${osqueryPolicies.map((p) => `policy_id:${p}`).join(' or ')}`); - } + const policyFragment = osqueryPolicies.map((p) => `policy_id:${p}`).join(' or '); + let kuery = `last_checkin_status: online and (${policyFragment})`; if (searchValue) { - kueryFragments.push( - `local_metadata.host.hostname:*${searchValue}* or local_metadata.elastic.agent.id:*${searchValue}*` - ); + kuery += `and (local_metadata.host.hostname:*${searchValue}* or local_metadata.elastic.agent.id:*${searchValue}*)`; } return http.get(agentRouteService.getListPath(), { query: { - kuery: kueryFragments.map((frag) => `(${frag})`).join(' and '), + kuery, perPage, - showInactive: true, }, }); }, { - enabled: !osqueryPoliciesLoading, + enabled: !osqueryPoliciesLoading && osqueryPolicies.length > 0, + onSuccess: () => setErrorToast(), onError: (error) => - toasts.addError(error as Error, { + setErrorToast(error as Error, { title: i18n.translate('xpack.osquery.agents.fetchError', { defaultMessage: 'Error while fetching agents', }), diff --git a/x-pack/plugins/osquery/public/agents/use_osquery_policies.ts b/x-pack/plugins/osquery/public/agents/use_osquery_policies.ts index 0eb94af73e3a8..9064dac1ae5d0 100644 --- a/x-pack/plugins/osquery/public/agents/use_osquery_policies.ts +++ b/x-pack/plugins/osquery/public/agents/use_osquery_policies.ts @@ -12,12 +12,11 @@ import { i18n } from '@kbn/i18n'; import { useKibana } from '../common/lib/kibana'; import { packagePolicyRouteService, PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../fleet/common'; import { OSQUERY_INTEGRATION_NAME } from '../../common'; +import { useErrorToast } from '../common/hooks/use_error_toast'; export const useOsqueryPolicies = () => { - const { - http, - notifications: { toasts }, - } = useKibana().services; + const { http } = useKibana().services; + const setErrorToast = useErrorToast(); const { isLoading: osqueryPoliciesLoading, data: osqueryPolicies = [] } = useQuery( ['osqueryPolicies'], @@ -30,8 +29,9 @@ export const useOsqueryPolicies = () => { { select: (response) => uniq(response.items.map((p: { policy_id: string }) => p.policy_id)), + onSuccess: () => setErrorToast(), onError: (error: Error) => - toasts.addError(error, { + setErrorToast(error, { title: i18n.translate('xpack.osquery.osquery_policies.fetchError', { defaultMessage: 'Error while fetching osquery policies', }), diff --git a/x-pack/plugins/osquery/public/common/hooks/use_error_toast.tsx b/x-pack/plugins/osquery/public/common/hooks/use_error_toast.tsx new file mode 100644 index 0000000000000..fb17803a9d57b --- /dev/null +++ b/x-pack/plugins/osquery/public/common/hooks/use_error_toast.tsx @@ -0,0 +1,26 @@ +/* + * 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 { ErrorToastOptions, Toast } from 'kibana/public'; +import { useState } from 'react'; +import { useKibana } from '../../common/lib/kibana'; + +export const useErrorToast = () => { + const [errorToast, setErrorToast] = useState(); + const { + notifications: { toasts }, + } = useKibana().services; + return (error?: unknown, opts?: ErrorToastOptions) => { + if (errorToast) { + toasts.remove(errorToast); + } + if (error) { + // @ts-expect-error update types + setErrorToast(toasts.addError(error, opts)); + } + }; +}; diff --git a/x-pack/plugins/osquery/public/common/hooks/use_osquery_integration.tsx b/x-pack/plugins/osquery/public/common/hooks/use_osquery_integration.tsx index ccfb407eab58b..236fdb1af1815 100644 --- a/x-pack/plugins/osquery/public/common/hooks/use_osquery_integration.tsx +++ b/x-pack/plugins/osquery/public/common/hooks/use_osquery_integration.tsx @@ -12,12 +12,11 @@ import { useQuery } from 'react-query'; import { GetPackagesResponse, epmRouteService } from '../../../../fleet/common'; import { OSQUERY_INTEGRATION_NAME } from '../../../common'; import { useKibana } from '../lib/kibana'; +import { useErrorToast } from './use_error_toast'; export const useOsqueryIntegration = () => { - const { - http, - notifications: { toasts }, - } = useKibana().services; + const { http } = useKibana().services; + const setErrorToast = useErrorToast(); return useQuery( 'integrations', @@ -31,7 +30,7 @@ export const useOsqueryIntegration = () => { select: ({ response }: GetPackagesResponse) => find(['name', OSQUERY_INTEGRATION_NAME], response), onError: (error: Error) => - toasts.addError(error, { + setErrorToast(error, { title: i18n.translate('xpack.osquery.osquery_integration.fetchError', { defaultMessage: 'Error while fetching osquery integration', }), diff --git a/x-pack/plugins/osquery/public/live_queries/form/index.tsx b/x-pack/plugins/osquery/public/live_queries/form/index.tsx index 4cf2d4aa4fe91..6f2d1afec6fe9 100644 --- a/x-pack/plugins/osquery/public/live_queries/form/index.tsx +++ b/x-pack/plugins/osquery/public/live_queries/form/index.tsx @@ -19,6 +19,7 @@ import { useKibana } from '../../common/lib/kibana'; import { ResultTabs } from '../../queries/edit/tabs'; import { queryFieldValidation } from '../../common/validations'; import { fieldValidators } from '../../shared_imports'; +import { useErrorToast } from '../../common/hooks/use_error_toast'; const FORM_ID = 'liveQueryForm'; @@ -35,10 +36,9 @@ const LiveQueryFormComponent: React.FC = ({ // onSubmit, onSuccess, }) => { - const { - http, - notifications: { toasts }, - } = useKibana().services; + const { http } = useKibana().services; + + const setErrorToast = useErrorToast(); const { data, @@ -53,14 +53,20 @@ const LiveQueryFormComponent: React.FC = ({ body: JSON.stringify(payload), }), { - onSuccess, + onSuccess: () => { + setErrorToast(); + if (onSuccess) { + onSuccess(); + } + }, onError: (error) => { - // @ts-expect-error update types - toasts.addError(error, { title: error.body.error, toastMessage: error.body.message }); + setErrorToast(error); }, } ); + const expirationDate = useMemo(() => new Date(data?.actions[0].expiration), [data?.actions]); + const formSchema = { query: { type: FIELD_TYPES.TEXT, @@ -173,7 +179,12 @@ const LiveQueryFormComponent: React.FC = ({ defaultMessage: 'Check results', }), children: actionId ? ( - + ) : null, status: resultsStatus, }, @@ -185,6 +196,7 @@ const LiveQueryFormComponent: React.FC = ({ queryComponentProps, queryStatus, queryValueProvided, + expirationDate, resultsStatus, submit, ] diff --git a/x-pack/plugins/osquery/public/queries/edit/tabs.tsx b/x-pack/plugins/osquery/public/queries/edit/tabs.tsx index 978c3f938f1d6..2c9421606ea30 100644 --- a/x-pack/plugins/osquery/public/queries/edit/tabs.tsx +++ b/x-pack/plugins/osquery/public/queries/edit/tabs.tsx @@ -14,6 +14,7 @@ import { ActionResultsSummary } from '../../action_results/action_results_summar interface ResultTabsProps { actionId: string; agentIds?: string[]; + expirationDate: Date; isLive?: boolean; startDate?: string; endDate?: string; @@ -22,6 +23,7 @@ interface ResultTabsProps { const ResultTabsComponent: React.FC = ({ actionId, agentIds, + expirationDate, endDate, isLive, startDate, @@ -34,7 +36,12 @@ const ResultTabsComponent: React.FC = ({ content: ( <> - + ), }, @@ -55,7 +62,7 @@ const ResultTabsComponent: React.FC = ({ ), }, ], - [actionId, agentIds, endDate, isLive, startDate] + [actionId, agentIds, endDate, isLive, startDate, expirationDate] ); return ( diff --git a/x-pack/plugins/osquery/public/results/use_all_results.ts b/x-pack/plugins/osquery/public/results/use_all_results.ts index d5e2bbc886940..1121898410278 100644 --- a/x-pack/plugins/osquery/public/results/use_all_results.ts +++ b/x-pack/plugins/osquery/public/results/use_all_results.ts @@ -21,6 +21,7 @@ import { import { ESTermQuery } from '../../common/typed_json'; import { generateTablePaginationOptions, getInspectResponse, InspectResponse } from './helpers'; +import { useErrorToast } from '../common/hooks/use_error_toast'; export interface ResultsArgs { results: ResultEdges; @@ -50,10 +51,8 @@ export const useAllResults = ({ skip = false, isLive = false, }: UseAllResults) => { - const { - data, - notifications: { toasts }, - } = useKibana().services; + const { data } = useKibana().services; + const setErrorToast = useErrorToast(); return useQuery( ['allActionResults', { actionId, activePage, limit, sort }], @@ -81,8 +80,9 @@ export const useAllResults = ({ { refetchInterval: isLive ? 1000 : false, enabled: !skip, + onSuccess: () => setErrorToast(), onError: (error: Error) => - toasts.addError(error, { + setErrorToast(error, { title: i18n.translate('xpack.osquery.results.fetchError', { defaultMessage: 'Error while fetching results', }), diff --git a/x-pack/plugins/osquery/public/routes/live_queries/details/index.tsx b/x-pack/plugins/osquery/public/routes/live_queries/details/index.tsx index 5a80e12d0fef3..64a1fb0791e83 100644 --- a/x-pack/plugins/osquery/public/routes/live_queries/details/index.tsx +++ b/x-pack/plugins/osquery/public/routes/live_queries/details/index.tsx @@ -43,6 +43,10 @@ const LiveQueryDetailsPageComponent = () => { const liveQueryListProps = useRouterNavigate('live_queries'); const { data } = useActionDetails({ actionId }); + const expirationDate = useMemo(() => new Date(data?.actionDetails._source.expiration), [ + data?.actionDetails, + ]); + const expired = useMemo(() => expirationDate < new Date(), [expirationDate]); const { data: actionResultsData } = useActionResults({ actionId, activePage: 0, @@ -78,6 +82,18 @@ const LiveQueryDetailsPageComponent = () => { [liveQueryListProps] ); + const failed = useMemo(() => { + let result = actionResultsData?.aggregations.failed; + if (expired) { + result = '-'; + if (data?.actionDetails?.fields?.agents && actionResultsData?.aggregations) { + result = + data.actionDetails.fields.agents.length - actionResultsData.aggregations.successful; + } + } + return result; + }, [expired, actionResultsData?.aggregations, data?.actionDetails?.fields?.agents]); + const RightColumn = useMemo( () => ( @@ -114,15 +130,13 @@ const LiveQueryDetailsPageComponent = () => { /> - - {actionResultsData?.aggregations.failed} - + {failed} ), - [actionResultsData?.aggregations.failed, data?.actionDetails?.fields?.agents?.length] + [data?.actionDetails?.fields?.agents?.length, failed] ); return ( @@ -133,6 +147,7 @@ const LiveQueryDetailsPageComponent = () => { theme.eui.paddingSizes.s}; @@ -36,6 +37,7 @@ const ActiveStateSwitchComponent: React.FC = ({ item }) http, notifications: { toasts }, } = useKibana().services; + const setErrorToast = useErrorToast(); const [confirmationModal, setConfirmationModal] = useState(false); const hideConfirmationModal = useCallback(() => setConfirmationModal(false), []); @@ -51,6 +53,7 @@ const ActiveStateSwitchComponent: React.FC = ({ item }) { onSuccess: (response) => { queryClient.invalidateQueries('scheduledQueries'); + setErrorToast(); toasts.addSuccess( response.item.enabled ? i18n.translate( @@ -75,7 +78,7 @@ const ActiveStateSwitchComponent: React.FC = ({ item }) }, onError: (error) => { // @ts-expect-error update types - toasts.addError(error, { title: error.body.error, toastMessage: error.body.message }); + setErrorToast(error, { title: error.body.error, toastMessage: error.body.message }); }, } ); diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/form/index.tsx b/x-pack/plugins/osquery/public/scheduled_query_groups/form/index.tsx index 64efdf61fc735..c940b1f8527b5 100644 --- a/x-pack/plugins/osquery/public/scheduled_query_groups/form/index.tsx +++ b/x-pack/plugins/osquery/public/scheduled_query_groups/form/index.tsx @@ -45,6 +45,7 @@ import { PolicyIdComboBoxField } from './policy_id_combobox_field'; import { QueriesField } from './queries_field'; import { ConfirmDeployAgentPolicyModal } from './confirmation_modal'; import { useAgentPolicies } from '../../agent_policies'; +import { useErrorToast } from '../../common/hooks/use_error_toast'; const GhostFormField = () => <>; @@ -68,6 +69,7 @@ const ScheduledQueryGroupFormComponent: React.FC = http, notifications: { toasts }, } = useKibana().services; + const setErrorToast = useErrorToast(); const [showConfirmationModal, setShowConfirmationModal] = useState(false); const handleHideConfirmationModal = useCallback(() => setShowConfirmationModal(false), []); @@ -110,6 +112,7 @@ const ScheduledQueryGroupFormComponent: React.FC = return; } + setErrorToast(); navigateToApp(PLUGIN_ID, { path: `scheduled_query_groups/${data.item.id}` }); toasts.addSuccess( i18n.translate('xpack.osquery.scheduledQueryGroup.form.updateSuccessToastMessageText', { @@ -122,7 +125,7 @@ const ScheduledQueryGroupFormComponent: React.FC = }, onError: (error) => { // @ts-expect-error update types - toasts.addError(error, { title: error.body.error, toastMessage: error.body.message }); + setErrorToast(error, { title: error.body.error, toastMessage: error.body.message }); }, } ); diff --git a/x-pack/plugins/osquery/server/lib/parse_agent_groups.ts b/x-pack/plugins/osquery/server/lib/parse_agent_groups.ts index a120d7deddf50..8fe60f59f01d7 100644 --- a/x-pack/plugins/osquery/server/lib/parse_agent_groups.ts +++ b/x-pack/plugins/osquery/server/lib/parse_agent_groups.ts @@ -63,7 +63,7 @@ export const parseAgentSelection = async ( perPage, page, kuery, - showInactive: true, + showInactive: false, }); return { results: res.agents.map((agent) => agent.id), total: res.total }; }); @@ -84,7 +84,7 @@ export const parseAgentSelection = async ( perPage, page, kuery, - showInactive: true, + showInactive: false, }); return { results: res.agents.map((agent) => agent.id), total: res.total }; }); From 119845483f695431a153c9ce92a98b5803e7241b Mon Sep 17 00:00:00 2001 From: Pete Harverson Date: Thu, 24 Jun 2021 17:44:13 +0100 Subject: [PATCH 38/69] [ML] Fixes data frame analytics models list pipelines tab (#103235) --- .../components/models_management/expanded_row.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/expanded_row.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/expanded_row.tsx index 93be45bbdaf97..87a3f10992c06 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/expanded_row.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/expanded_row.tsx @@ -430,7 +430,7 @@ export const ExpandedRow: FC = ({ item }) => { Date: Thu, 24 Jun 2021 17:49:56 +0100 Subject: [PATCH 39/69] [ML] Add description and owner to kibana.json for ML owned plugins (#103254) --- x-pack/plugins/data_visualizer/kibana.json | 7 ++++++- x-pack/plugins/file_upload/kibana.json | 7 ++++++- x-pack/plugins/ml/kibana.json | 7 ++++++- x-pack/plugins/transform/kibana.json | 7 ++++++- 4 files changed, 24 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/data_visualizer/kibana.json b/x-pack/plugins/data_visualizer/kibana.json index 00eb3d7bf142c..01aca7c2bbaee 100644 --- a/x-pack/plugins/data_visualizer/kibana.json +++ b/x-pack/plugins/data_visualizer/kibana.json @@ -27,5 +27,10 @@ ], "extraPublicDirs": [ "common" - ] + ], + "owner": { + "name": "Machine Learning UI", + "githubTeam": "ml-ui" + }, + "description": "The Data Visualizer tools help you understand your data, by analyzing the metrics and fields in a log file or an existing Elasticsearch index." } diff --git a/x-pack/plugins/file_upload/kibana.json b/x-pack/plugins/file_upload/kibana.json index 6f93874cdbcaa..e69c5e34bc09b 100644 --- a/x-pack/plugins/file_upload/kibana.json +++ b/x-pack/plugins/file_upload/kibana.json @@ -16,5 +16,10 @@ ], "extraPublicDirs": [ "common" - ] + ], + "owner": { + "name": "Machine Learning UI", + "githubTeam": "ml-ui" + }, + "description": "The file upload plugin contains components and services for uploading a file, analyzing its data, and then importing the data into an Elasticsearch index. Supported file types include CSV, TSV, newline-delimited JSON and GeoJSON." } diff --git a/x-pack/plugins/ml/kibana.json b/x-pack/plugins/ml/kibana.json index 7b3f457106033..7f3ad80968b7a 100644 --- a/x-pack/plugins/ml/kibana.json +++ b/x-pack/plugins/ml/kibana.json @@ -43,5 +43,10 @@ ], "extraPublicDirs": [ "common" - ] + ], + "owner": { + "name": "Machine Learning UI", + "githubTeam": "ml-ui" + }, + "description": "This plugin provides access to the machine learning features provided by Elastic." } diff --git a/x-pack/plugins/transform/kibana.json b/x-pack/plugins/transform/kibana.json index 4216ac9761e86..c9f6beeee5aff 100644 --- a/x-pack/plugins/transform/kibana.json +++ b/x-pack/plugins/transform/kibana.json @@ -23,5 +23,10 @@ "kibanaUtils", "kibanaReact", "ml" - ] + ], + "owner": { + "name": "Machine Learning UI", + "githubTeam": "ml-ui" + }, + "description": "This plugin provides access to the transforms features provided by Elastic. Transforms enable you to convert existing Elasticsearch indices into summarized indices, which provide opportunities for new insights and analytics." } From bfb98053d6b361a18acd58e7f0ab34eb8a43a0ac Mon Sep 17 00:00:00 2001 From: Janeen Mikell-Straughn <57149392+jmikell821@users.noreply.github.com> Date: Thu, 24 Jun 2021 12:53:56 -0400 Subject: [PATCH 40/69] [DOCS] Security Overview (#103151) * updating overview topic for Kibana * formatting fixes * small formatting tweaks * small formatting tweaks * Update index.asciidoc Updating index file; removing siem-UI and machine learning topics from the TOC. * [DOCS] Change part to chapter * Update index.asciidoc * Adding attribute Co-authored-by: lcawl --- docs/siem/index.asciidoc | 198 +++++++++++++++++++++++++++++---------- 1 file changed, 151 insertions(+), 47 deletions(-) diff --git a/docs/siem/index.asciidoc b/docs/siem/index.asciidoc index 18895f0533fd7..05b1ec0b5b797 100644 --- a/docs/siem/index.asciidoc +++ b/docs/siem/index.asciidoc @@ -1,60 +1,164 @@ +[chapter] [role="xpack"] [[xpack-siem]] -= Elastic Security += Elastic Security overview +++++ +Security +++++ -[partintro] --- +https://www.elastic.co/security[Elastic Security] combines SIEM threat detection features with endpoint +prevention and response capabilities in one solution. These analytical and +protection capabilities, leveraged by the speed and extensibility of +Elasticsearch, enable analysts to defend their organization from threats before +damage and loss occur. -Elastic Security combines SIEM threat detection features with endpoint -prevention and response capabilities in one solution, including: +Elastic Security provides the following security benefits and capabilities: -* A detection engine to identify attacks and system misconfiguration +* A detection engine to identify attacks and system misconfigurations * A workspace for event triage and investigations * Interactive visualizations to investigate process relationships -* Embedded case management and automated actions -* Detection of signatureless attacks with prebuilt {ml} anomaly jobs and -detection rules +* Inbuilt case management with automated actions +* Detection of signatureless attacks with prebuilt machine learning anomaly jobs +and detection rules -[role="screenshot"] -image::siem/images/overview-ui.png[Elastic Security in Kibana] - -[float] -== Add data - -Kibana provides step-by-step instructions to help you add data. The -{security-guide}[Security Guide] is a good source for more -detailed information and instructions. - -[float] -=== {Beats} - -https://www.elastic.co/products/beats/auditbeat[{auditbeat}], -https://www.elastic.co/products/beats/filebeat[{filebeat}], -https://www.elastic.co/products/beats/winlogbeat[{winlogbeat}], and -https://www.elastic.co/products/beats/packetbeat[{packetbeat}] -send security events and other data to Elasticsearch. +[discrete] +== Elastic Security components and workflow -The default index patterns for Elastic Security events are `auditbeat-*`, `winlogbeat-*`, -`filebeat-*`, `packetbeat-*`, `endgame-*`, `logs-*`, and `apm-*-transaction*`. To change the default pattern patterns, go to *Stack Management > Advanced Settings > securitySolution:defaultIndex*. +The following diagram provides a comprehensive illustration of the Elastic Security workflow. -[float] -=== Elastic Security endpoint agent - -The agent detects and protects against malware, and ships host and network -events directly to Elastic Security. - -[float] -=== Elastic Common Schema (ECS) for normalizing data - -The {ecs-ref}[Elastic Common Schema (ECS)] defines a common set of fields to be -used for storing event data in Elasticsearch. ECS helps users normalize their -event data to better analyze, visualize, and correlate the data represented in -their events. - -Elastic Security can ingest and normalize events from ECS-compatible data sources. +[role="screenshot"] +image::../siem/images/workflow.png[] + +Here's an overview of the flow and its components: + +* Data is shipped from your hosts to {es} via beat modules and the Elastic https://www.elastic.co/endpoint-security/[Endpoint Security agent integration]. This integration provides capabilities such as collecting events, detecting and preventing {security-guide}/detection-engine-overview.html#malware-prevention[malicious activity], and artifact delivery. The {fleet-guide}/fleet-overview.html[{fleet}] app is used to +install and manage agents and integrations on your hosts. ++ +The Endpoint Security integration ships the following data sets: ++ +*** *Windows*: Process, network, file, DNS, registry, DLL and driver loads, +malware security detections +*** *Linux/macOS*: Process, network, file ++ +* https://www.elastic.co/integrations?solution=security[Beat modules]: {beats} +are lightweight data shippers. Beat modules provide a way of collecting and +parsing specific data sets from common sources, such as cloud and OS events, +logs, and metrics. Common security-related modules are listed {security-guide}/ingest-data.html#enable-beat-modules[here]. +* The {security-app} in {kib} is used to manage the *Detection engine*, +*Cases*, and *Timeline*, as well as administer hosts running Endpoint Security: +** Detection engine: Automatically searches for suspicious host and network +activity via the following: +*** {security-guide}/detection-engine-overview.html#detection-engine-overview[Detection rules]: Periodically search the data +({es} indices) sent from your hosts for suspicious events. When a suspicious +event is discovered, a detection alert is generated. External systems, such as +Slack and email, can be used to send notifications when alerts are generated. +You can create your own rules and make use of our {security-guide}/prebuilt-rules.html[prebuilt ones]. +*** {security-guide}/detections-ui-exceptions.html[Exceptions]: Reduce noise and the number of +false positives. Exceptions are associated with rules and prevent alerts when +an exception's conditions are met. *Value lists* contain source event +values that can be used as part of an exception's conditions. When +Elastic {endpoint-sec} is installed on your hosts, you can add malware exceptions +directly to the endpoint from the Security app. +*** {security-guide}/machine-learning.html#included-jobs[{ml-cap} jobs]: Automatic anomaly detection of host and +network events. Anomaly scores are provided per host and can be used with +detection rules. +** {security-guide}/timelines-ui.html[Timeline]: Workspace for investigating alerts and events. +Timelines use queries and filters to drill down into events related to +a specific incident. Timeline templates are attached to rules and use predefined +queries when alerts are investigated. Timelines can be saved and shared with +others, as well as attached to Cases. +** {security-guide}/cases-overview.html[Cases]: An internal system for opening, tracking, and sharing +security issues directly in the Security app. Cases can be integrated with +external ticketing systems. +** {security-guide}/admin-page-ov.html[Administration]: View and manage hosts running {endpoint-sec}. + +{security-guide}/ingest-data.html[Ingest data to Elastic Security] and {security-guide}/install-endpoint.html[Configure and install the Elastic Endpoint integration] describe how to ship security-related +data to {es}. + + +For more background information, see: + +* https://www.elastic.co/products/elasticsearch[{es}]: A real-time, +distributed storage, search, and analytics engine. {es} excels at indexing +streams of semi-structured data, such as logs or metrics. +* https://www.elastic.co/products/kibana[{kib}]: An open-source analytics and +visualization platform designed to work with {es}. You use {kib} to search, +view, and interact with data stored in {es} indices. You can easily compile +advanced data analysis and visualize your data in a variety of charts, tables, +and maps. + +[discrete] +=== Compatibility with cold tier nodes + +Cold tier is a {ref}/data-tiers.html[data tier] that holds time-series data that is accessed only occasionally. In {stack} version >=7.11.0, {elastic-sec} supports cold tier data for the following {es} indices: + +* Index patterns specified in `securitySolution:defaultIndex` +* Index patterns specified in the definitions of detection rules, except for indicator match rules +* Index patterns specified in the data sources selector on various {security-app} pages + +{elastic-sec} does NOT support cold tier data for the following {es} indices: + +* Index patterns controlled by {elastic-sec}, including signals and list indices +* Index patterns specified in indicator match rules + +Using cold tier data for unsupported indices may result in detection rule timeouts and overall performance degradation. + +[discrete] +[[self-protection]] +==== Elastic Endpoint self-protection + +Self-protection means that {elastic-endpoint} has guards against users and attackers that may try to interfere with its functionality. This protection feature is consistently enhanced to prevent attackers who may attempt to use newer, more sophisticated tactics to interfere with the {elastic-endpoint}. Self-protection is enabled by default when {elastic-endpoint} installs on supported platforms, listed below. + +Self-protection is enabled on the following 64-bit Windows versions: + +* Windows 8.1 +* Windows 10 +* Windows Server 2012 R2 +* Windows Server 2016 +* Windows Server 2019 + +And on the following macOS versions: + +* macOS 10.15 (Catalina) +* macOS 11 (Big Sur) + +NOTE: Other Windows and macOS variants (and all Linux distributions) do not have self-protection. + +For {stack} version >= 7.11.0, self-protection defines the following permissions: + +* Users -- even Administrator/root -- *cannot* delete {elastic-endpoint} files (located at `c:\Program Files\Elastic\Endpoint` on Windows, and `/Library/Elastic/Endpoint` on macOS). +* Users *cannot* terminate the {elastic-endpoint} program or service. +* Administrator/root users *can* read the endpoint's files. On Windows, the easiest way to read Endpoint files is to start an Administrator `cmd.exe` prompt. On macOS, an Administrator can use the `sudo` command. +* Administrator/root users *can* stop the {elastic-agent}'s service. On Windows, run the `sc stop "Elastic Agent"` command. On macOS, run the `sudo launchctl stop elastic-agent` command. + + +[discrete] +[[siem-integration]] +=== Integration with other Elastic products + +You can use {elastic-sec} with other Elastic products and features to help you +identify and investigate suspicious activity: + +* https://www.elastic.co/products/stack/machine-learning[{ml-cap}] +* https://www.elastic.co/products/stack/alerting[Alerting] +* https://www.elastic.co/products/stack/canvas[Canvas] + +[discrete] +[[data-sources]] +=== APM transaction data sources + +By default, {elastic-sec} monitors {apm-app-ref}/apm-getting-started.html[APM] +`apm-*-transaction*` indices. To add additional APM indices, update the +index patterns in the `securitySolution:defaultIndex` setting ({kib} -> Stack Management -> Advanced Settings -> `securitySolution:defaultIndex`). --- +[discrete] +[[ecs-compliant-reqs]] +=== ECS compliance data requirements +The {ecs-ref}[Elastic Common Schema (ECS)] defines a common set of fields used for +storing event data in Elasticsearch. ECS helps users normalize their event data +to better analyze, visualize, and correlate the data represented in their +events. {elastic-sec} supports events and indicator index data from any ECS-compliant data source. -include::siem-ui.asciidoc[] -include::machine-learning.asciidoc[] +IMPORTANT: {elastic-sec} requires {ecs-ref}[ECS-compliant data]. If you use third-party data collectors to ship data to {es}, the data must be mapped to ECS. +{security-guide}/siem-field-reference.html[Elastic Security ECS field reference] lists ECS fields used in {elastic-sec}. From eb8e9d7cc9a36b2a6cf1da3561aed572da6710eb Mon Sep 17 00:00:00 2001 From: John Schulz Date: Thu, 24 Jun 2021 12:56:48 -0400 Subject: [PATCH 41/69] [Fleet] Remove duplication between two files #103282 ## Summary `public/applications/integrations/constants.tsx` and `public/applications/integrations/sections/epm/constants.tsx` are identical except for this line in `public/applications/integrations/constants.tsx` ```ts export * from '../../constants'; ``` This PR removes all the duplication from the "upper" file (`public/applications/integrations/constants.tsx`) and leaves the other code "down" in `/sections/epm/` closer to where it's used. Initially, I deleted `public/applications/integrations/constants.tsx` entirely but several files do `import` the constants it exports, so I left it. --- .../applications/integrations/constants.tsx | 53 ------------------- .../detail/assets/assets_accordion.tsx | 2 +- 2 files changed, 1 insertion(+), 54 deletions(-) diff --git a/x-pack/plugins/fleet/public/applications/integrations/constants.tsx b/x-pack/plugins/fleet/public/applications/integrations/constants.tsx index 08197e18fec02..f2cb57301f49c 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/constants.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/constants.tsx @@ -5,57 +5,4 @@ * 2.0. */ -import type { IconType } from '@elastic/eui'; - -import type { ServiceName } from '../../types'; -import { ElasticsearchAssetType, KibanaAssetType } from '../../types'; - export * from '../../constants'; - -// only allow Kibana assets for the kibana key, ES asssets for elasticsearch, etc -type ServiceNameToAssetTypes = Record, KibanaAssetType[]> & - Record, ElasticsearchAssetType[]>; - -export const DisplayedAssets: ServiceNameToAssetTypes = { - kibana: Object.values(KibanaAssetType), - elasticsearch: Object.values(ElasticsearchAssetType), -}; -export type DisplayedAssetType = KibanaAssetType | ElasticsearchAssetType; - -export const AssetTitleMap: Record = { - dashboard: 'Dashboard', - ilm_policy: 'ILM Policy', - ingest_pipeline: 'Ingest Pipeline', - transform: 'Transform', - index_pattern: 'Index Pattern', - index_template: 'Index Template', - component_template: 'Component Template', - search: 'Saved Search', - visualization: 'Visualization', - map: 'Map', - data_stream_ilm_policy: 'Data Stream ILM Policy', - lens: 'Lens', - security_rule: 'Security Rule', - ml_module: 'ML Module', -}; - -export const ServiceTitleMap: Record = { - kibana: 'Kibana', - elasticsearch: 'Elasticsearch', -}; - -export const AssetIcons: Record = { - dashboard: 'dashboardApp', - index_pattern: 'indexPatternApp', - search: 'searchProfilerApp', - visualization: 'visualizeApp', - map: 'emsApp', - lens: 'lensApp', - security_rule: 'securityApp', - ml_module: 'mlApp', -}; - -export const ServiceIcons: Record = { - elasticsearch: 'logoElasticsearch', - kibana: 'logoKibana', -}; diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/assets_accordion.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/assets_accordion.tsx index abfdd88d27162..12d4a0014b976 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/assets_accordion.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/assets_accordion.tsx @@ -20,7 +20,7 @@ import { EuiNotificationBadge, } from '@elastic/eui'; -import { AssetTitleMap } from '../../../../../constants'; +import { AssetTitleMap } from '../../../constants'; import { getHrefToObjectInKibanaApp, useStartServices } from '../../../../../hooks'; From 5af69edfbaf58d3874b043c1fc3674080a1b2430 Mon Sep 17 00:00:00 2001 From: Dmitry Shevchenko Date: Thu, 24 Jun 2021 19:17:09 +0200 Subject: [PATCH 42/69] Fix "Deleted rule" badge is not displayed if 'Rule Name' contains more than 55 words (#103164) --- .../__snapshots__/index.test.tsx.snap | 4 ++-- .../__snapshots__/title.test.tsx.snap | 16 +++++++------ .../common/components/header_page/index.tsx | 12 ++++++++-- .../common/components/header_page/title.tsx | 23 +++++++++++++++---- .../common/components/header_page/types.ts | 2 ++ .../detection_engine/rules/details/index.tsx | 22 +++++++++++------- .../network/pages/details/index.test.tsx | 2 +- 7 files changed, 57 insertions(+), 24 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/header_page/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/header_page/__snapshots__/index.test.tsx.snap index 9cb9f28612b15..d00bd7040c164 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_page/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/header_page/__snapshots__/index.test.tsx.snap @@ -6,7 +6,7 @@ exports[`HeaderPage it renders 1`] = ` alignItems="center" bottomBorder={true} > - + - + diff --git a/x-pack/plugins/security_solution/public/common/components/header_page/__snapshots__/title.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/header_page/__snapshots__/title.test.tsx.snap index ca02d0ac5cb2d..176902ae38d20 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_page/__snapshots__/title.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/header_page/__snapshots__/title.test.tsx.snap @@ -4,20 +4,22 @@ exports[`Title it renders 1`] = ` -

- - Test title - + + + Test title + + -

+
`; diff --git a/x-pack/plugins/security_solution/public/common/components/header_page/index.tsx b/x-pack/plugins/security_solution/public/common/components/header_page/index.tsx index 1c87d70c0c7cb..75453f2d759fb 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_page/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/header_page/index.tsx @@ -55,6 +55,14 @@ const Badge = (styled(EuiBadge)` ` as unknown) as typeof EuiBadge; Badge.displayName = 'Badge'; +const HeaderSection = styled(EuiPageHeaderSection)` + // Without min-width: 0, as a flex child, it wouldn't shrink properly + // and could overflow its parent. + min-width: 0; + max-width: 100%; +`; +HeaderSection.displayName = 'HeaderSection'; + interface BackOptions { href: LinkIconProps['href']; text: LinkIconProps['children']; @@ -105,7 +113,7 @@ const HeaderPageComponent: React.FC = ({ return ( <> - + {backOptions && ( = ({ {subtitle && } {subtitle2 && } {border && isLoading && } - + {children && ( diff --git a/x-pack/plugins/security_solution/public/common/components/header_page/title.tsx b/x-pack/plugins/security_solution/public/common/components/header_page/title.tsx index 471d539ea03f4..a99132d5c7f62 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_page/title.tsx +++ b/x-pack/plugins/security_solution/public/common/components/header_page/title.tsx @@ -24,6 +24,19 @@ const Badge = (styled(EuiBadge)` ` as unknown) as typeof EuiBadge; Badge.displayName = 'Badge'; +const Header = styled.h1` + display: flex; + align-items: center; +`; +Header.displayName = 'Header'; + +const TitleWrapper = styled.span` + // Without min-width: 0, as a flex child, it wouldn't shrink properly + // and could overflow its parent. + min-width: 0; +`; +TitleWrapper.displayName = 'TitleWrapper'; + interface Props { badgeOptions?: BadgeOptions; title: TitleProp; @@ -32,9 +45,11 @@ interface Props { const TitleComponent: React.FC = ({ draggableArguments, title, badgeOptions }) => ( -

+
{!draggableArguments ? ( - {title} + + {title} + ) : ( = ({ draggableArguments, title, badgeOptio tooltipPosition="bottom" /> ) : ( - + {badgeOptions.text} )} )} -

+
); diff --git a/x-pack/plugins/security_solution/public/common/components/header_page/types.ts b/x-pack/plugins/security_solution/public/common/components/header_page/types.ts index e95d0c8e1e69c..f099144eeb4be 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_page/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/header_page/types.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { EuiBadgeProps } from '@elastic/eui'; import type React from 'react'; export type TitleProp = string | React.ReactNode; @@ -17,4 +18,5 @@ export interface BadgeOptions { beta?: boolean; text: string; tooltip?: string; + color?: EuiBadgeProps['color']; } 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 b4f1af41a0606..92679cb2662d7 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 @@ -18,7 +18,6 @@ import { EuiTabs, EuiToolTip, EuiWindowEvent, - EuiBadge, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { noop } from 'lodash/fp'; @@ -115,6 +114,7 @@ import { NeedAdminForUpdateRulesCallOut } from '../../../../components/callouts/ import { getRuleStatusText } from '../../../../../../common/detection_engine/utils'; import { MissingPrivilegesCallOut } from '../../../../components/callouts/missing_privileges_callout'; import { useRuleWithFallback } from '../../../../containers/detection_engine/rules/use_rule_with_fallback'; +import { BadgeOptions } from '../../../../../common/components/header_page/types'; /** * Need a 100% height here to account for the graph/analyze tool, which sets no explicit height parameters, but fills the available space. @@ -253,15 +253,20 @@ const RuleDetailsPageComponent = () => { const title = useMemo( () => ( <> - {rule?.name}{' '} - {ruleLoading ? ( - - ) : ( - !isExistingRule && {i18n.DELETED_RULE} - )} + {rule?.name} {ruleLoading && } ), - [rule, ruleLoading, isExistingRule] + [rule, ruleLoading] + ); + const badgeOptions = useMemo( + () => + !ruleLoading && !isExistingRule + ? { + text: i18n.DELETED_RULE, + color: 'default', + } + : undefined, + [isExistingRule, ruleLoading] ); const subTitle = useMemo( () => @@ -595,6 +600,7 @@ const RuleDetailsPageComponent = () => { , ]} title={title} + badgeOptions={badgeOptions} > diff --git a/x-pack/plugins/security_solution/public/network/pages/details/index.test.tsx b/x-pack/plugins/security_solution/public/network/pages/details/index.test.tsx index a9a97f6bac652..d9d5ac9241f19 100644 --- a/x-pack/plugins/security_solution/public/network/pages/details/index.test.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/details/index.test.tsx @@ -149,7 +149,7 @@ describe('Network Details', () => { ); expect( wrapper - .find('[data-test-subj="network-details-headline"] [data-test-subj="header-page-title"]') + .find('[data-test-subj="network-details-headline"] h1[data-test-subj="header-page-title"]') .text() ).toEqual('fe80::24ce:f7ff:fede:a571'); }); From 5abac25ba3ca4fb20dbba49e354da7f1a6285a46 Mon Sep 17 00:00:00 2001 From: Marco Liberati Date: Thu, 24 Jun 2021 19:19:20 +0200 Subject: [PATCH 43/69] [Lens] Update formula icons (#103287) * :lipstick: Updated formula reference icon * :lipstick: Replace wordwrap icons --- .../definitions/formula/editor/formula_editor.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_editor.tsx index d1b0ec8876feb..83a782b519248 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_editor.tsx @@ -585,7 +585,6 @@ export function FormulaEditor({
- {/* TODO: Replace `bolt` with `wordWrap` icon (after latest EUI is deployed) and hook up button to enable/disable word wrapping. */} setIsHelpOpen(!isHelpOpen)} > - + @@ -747,7 +746,7 @@ export function FormulaEditor({ setIsHelpOpen(!isHelpOpen)} - iconType="help" + iconType="documentation" color="text" size="s" aria-label={i18n.translate( From bf6c53bb45a3488000c836a8cba1432f1f2dc2eb Mon Sep 17 00:00:00 2001 From: Tim Roes Date: Thu, 24 Jun 2021 19:31:24 +0200 Subject: [PATCH 44/69] Improved Visualize button in field popover (#103099) * Improve field popover * Slightly improve type safteyness * Add unit tests for visualize trigger utils * Remove unused div Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- ...iscover_field_details_footer.test.tsx.snap | 705 ------------------ .../components/sidebar/discover_field.tsx | 37 +- .../sidebar/discover_field_details.scss | 10 - .../sidebar/discover_field_details.test.tsx | 41 +- .../sidebar/discover_field_details.tsx | 100 +-- .../discover_field_details_footer.test.tsx | 71 -- .../sidebar/discover_field_details_footer.tsx | 59 -- .../sidebar/discover_field_visualize.tsx | 66 ++ .../components/sidebar/lib/get_warnings.ts | 33 - .../lib/visualize_trigger_utils.test.ts | 116 +++ .../sidebar/lib/visualize_trigger_utils.ts | 84 ++- .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - 13 files changed, 288 insertions(+), 1038 deletions(-) delete mode 100644 src/plugins/discover/public/application/apps/main/components/sidebar/__snapshots__/discover_field_details_footer.test.tsx.snap delete mode 100644 src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_details.scss delete mode 100644 src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_details_footer.test.tsx delete mode 100644 src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_details_footer.tsx create mode 100644 src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_visualize.tsx delete mode 100644 src/plugins/discover/public/application/apps/main/components/sidebar/lib/get_warnings.ts create mode 100644 src/plugins/discover/public/application/apps/main/components/sidebar/lib/visualize_trigger_utils.test.ts diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/__snapshots__/discover_field_details_footer.test.tsx.snap b/src/plugins/discover/public/application/apps/main/components/sidebar/__snapshots__/discover_field_details_footer.test.tsx.snap deleted file mode 100644 index f976b961d8520..0000000000000 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/__snapshots__/discover_field_details_footer.test.tsx.snap +++ /dev/null @@ -1,705 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`discover sidebar field details footer renders properly 1`] = ` - - -
- -
- -
- - - -
-
-
-
-
-
-
-`; diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field.tsx index 26a3c482e9d3c..301866c762fbd 100644 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field.tsx +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field.tsx @@ -8,7 +8,7 @@ import './discover_field.scss'; -import React, { useState, useCallback, memo } from 'react'; +import React, { useState, useCallback, memo, useMemo } from 'react'; import { EuiPopover, EuiPopoverTitle, @@ -18,6 +18,7 @@ import { EuiIcon, EuiFlexGroup, EuiFlexItem, + EuiSpacer, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { UiCounterMetricType } from '@kbn/analytics'; @@ -27,7 +28,7 @@ import { FieldIcon, FieldButton } from '../../../../../../../kibana_react/public import { FieldDetails } from './types'; import { IndexPatternField, IndexPattern } from '../../../../../../../data/public'; import { getFieldTypeName } from './lib/get_field_type_name'; -import { DiscoverFieldDetailsFooter } from './discover_field_details_footer'; +import { DiscoverFieldVisualize } from './discover_field_visualize'; function wrapOnDot(str?: string) { // u200B is a non-width white-space character, which allows @@ -172,6 +173,7 @@ const MultiFields: React.FC = memo( })} + {multiFields.map((entry) => ( multiFields?.map((f) => f.field), [multiFields]); + if (field.type === '_source') { return ( {multiFields && ( - - )} - {!details.error && ( - + <> + + + )} + )} diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_details.scss b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_details.scss deleted file mode 100644 index ca48d67f75dec..0000000000000 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_details.scss +++ /dev/null @@ -1,10 +0,0 @@ -.dscFieldDetails { - color: $euiTextColor; - margin-bottom: $euiSizeS; -} - -.dscFieldDetails__visualizeBtn { - @include euiFontSizeXS; - height: $euiSizeL !important; - min-width: $euiSize * 4; -} diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_details.test.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_details.test.tsx index a798abb60b833..8c9ad5bc9708a 100644 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_details.test.tsx +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_details.test.tsx @@ -25,10 +25,11 @@ const indexPattern = getStubIndexPattern( ); describe('discover sidebar field details', function () { + const onAddFilter = jest.fn(); const defaultProps = { indexPattern, details: { buckets: [], error: '', exists: 1, total: 2, columns: [] }, - onAddFilter: jest.fn(), + onAddFilter, }; function mountComponent(field: IndexPatternField) { @@ -36,7 +37,7 @@ describe('discover sidebar field details', function () { return mountWithIntl(); } - it('should enable the visualize link for a number field', function () { + it('click on addFilter calls the function', function () { const visualizableField = new IndexPatternField({ name: 'bytes', type: 'number', @@ -47,37 +48,9 @@ describe('discover sidebar field details', function () { aggregatable: true, readFromDocValues: true, }); - const comp = mountComponent(visualizableField); - expect(findTestSubject(comp, 'fieldVisualize-bytes')).toBeTruthy(); - }); - - it('should disable the visualize link for an _id field', function () { - const conflictField = new IndexPatternField({ - name: '_id', - type: 'string', - esTypes: ['_id'], - count: 0, - scripted: false, - searchable: true, - aggregatable: true, - readFromDocValues: true, - }); - const comp = mountComponent(conflictField); - expect(findTestSubject(comp, 'fieldVisualize-_id')).toEqual({}); - }); - - it('should disable the visualize link for an unknown field', function () { - const unknownField = new IndexPatternField({ - name: 'test', - type: 'unknown', - esTypes: ['double'], - count: 0, - scripted: false, - searchable: true, - aggregatable: true, - readFromDocValues: true, - }); - const comp = mountComponent(unknownField); - expect(findTestSubject(comp, 'fieldVisualize-test')).toEqual({}); + const component = mountComponent(visualizableField); + const onAddButton = findTestSubject(component, 'onAddFilterButton'); + onAddButton.simulate('click'); + expect(onAddFilter).toHaveBeenCalledWith('_exists_', visualizableField.name, '+'); }); }); diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_details.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_details.tsx index ffa7b30de5280..e29799b720e21 100644 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_details.tsx +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_details.tsx @@ -6,27 +6,18 @@ * Side Public License, v 1. */ -import React, { useState, useEffect } from 'react'; -import { EuiIconTip, EuiText, EuiButton, EuiSpacer } from '@elastic/eui'; +import React from 'react'; +import { EuiText, EuiSpacer, EuiLink } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { METRIC_TYPE, UiCounterMetricType } from '@kbn/analytics'; import { DiscoverFieldBucket } from './discover_field_bucket'; -import { getWarnings } from './lib/get_warnings'; -import { - triggerVisualizeActions, - isFieldVisualizable, - getVisualizeHref, -} from './lib/visualize_trigger_utils'; import { Bucket, FieldDetails } from './types'; import { IndexPatternField, IndexPattern } from '../../../../../../../data/public'; -import './discover_field_details.scss'; interface DiscoverFieldDetailsProps { field: IndexPatternField; indexPattern: IndexPattern; details: FieldDetails; onAddFilter: (field: IndexPatternField | string, value: string, type: '+' | '-') => void; - trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void; } export function DiscoverFieldDetails({ @@ -34,46 +25,12 @@ export function DiscoverFieldDetails({ indexPattern, details, onAddFilter, - trackUiMetric, }: DiscoverFieldDetailsProps) { - const warnings = getWarnings(field); - const [showVisualizeLink, setShowVisualizeLink] = useState(false); - const [visualizeLink, setVisualizeLink] = useState(''); - - useEffect(() => { - isFieldVisualizable(field, indexPattern.id, details.columns).then( - (flag) => { - setShowVisualizeLink(flag); - // get href only if Visualize button is enabled - getVisualizeHref(field, indexPattern.id, details.columns).then( - (uri) => { - if (uri) setVisualizeLink(uri); - }, - () => { - setVisualizeLink(''); - } - ); - }, - () => { - setShowVisualizeLink(false); - } - ); - }, [field, indexPattern.id, details.columns]); - - const handleVisualizeLinkClick = (event: React.MouseEvent) => { - // regular link click. let the uiActions code handle the navigation and show popup if needed - event.preventDefault(); - if (trackUiMetric) { - trackUiMetric(METRIC_TYPE.CLICK, 'visualize_link_click'); - } - triggerVisualizeActions(field, indexPattern.id, details.columns); - }; - return ( <> -
- {details.error && {details.error}} - {!details.error && ( + {details.error && {details.error}} + {!details.error && ( + <>
{details.buckets.map((bucket: Bucket, idx: number) => ( ))}
- )} - - {showVisualizeLink && ( - <> - - {/* eslint-disable-next-line @elastic/eui/href-or-on-click */} - handleVisualizeLinkClick(e)} - href={visualizeLink} - size="s" - className="dscFieldDetails__visualizeBtn" - data-test-subj={`fieldVisualize-${field.name}`} - > + + + {!indexPattern.metaFields.includes(field.name) && !field.scripted ? ( + onAddFilter('_exists_', field.name, '+')} + data-test-subj="onAddFilterButton" + > + + + ) : ( - - {warnings.length > 0 && ( - )} - - )} -
+ + + )} ); } diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_details_footer.test.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_details_footer.test.tsx deleted file mode 100644 index aa93b2a663736..0000000000000 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_details_footer.test.tsx +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React from 'react'; -import { findTestSubject } from '@elastic/eui/lib/test'; -import { mountWithIntl } from '@kbn/test/jest'; -// @ts-expect-error -import stubbedLogstashFields from '../../../../../__fixtures__/logstash_fields'; -import { coreMock } from '../../../../../../../../core/public/mocks'; -import { IndexPatternField } from '../../../../../../../data/public'; -import { getStubIndexPattern } from '../../../../../../../data/public/test_utils'; -import { DiscoverFieldDetailsFooter } from './discover_field_details_footer'; - -const indexPattern = getStubIndexPattern( - 'logstash-*', - (cfg: unknown) => cfg, - 'time', - stubbedLogstashFields(), - coreMock.createSetup() -); - -describe('discover sidebar field details footer', function () { - const onAddFilter = jest.fn(); - const defaultProps = { - indexPattern, - details: { buckets: [], error: '', exists: 1, total: 2, columns: [] }, - onAddFilter, - }; - - function mountComponent(field: IndexPatternField) { - const compProps = { ...defaultProps, field }; - return mountWithIntl(); - } - - it('renders properly', function () { - const visualizableField = new IndexPatternField({ - name: 'bytes', - type: 'number', - esTypes: ['long'], - count: 10, - scripted: false, - searchable: true, - aggregatable: true, - readFromDocValues: true, - }); - const component = mountComponent(visualizableField); - expect(component).toMatchSnapshot(); - }); - - it('click on addFilter calls the function', function () { - const visualizableField = new IndexPatternField({ - name: 'bytes', - type: 'number', - esTypes: ['long'], - count: 10, - scripted: false, - searchable: true, - aggregatable: true, - readFromDocValues: true, - }); - const component = mountComponent(visualizableField); - const onAddButton = findTestSubject(component, 'onAddFilterButton'); - onAddButton.simulate('click'); - expect(onAddFilter).toHaveBeenCalledWith('_exists_', visualizableField.name, '+'); - }); -}); diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_details_footer.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_details_footer.tsx deleted file mode 100644 index 148dfc67c3e41..0000000000000 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_details_footer.tsx +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React from 'react'; -import { EuiLink, EuiPopoverFooter, EuiText } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { IndexPatternField } from '../../../../../../../data/common/index_patterns/fields'; -import { IndexPattern } from '../../../../../../../data/common/index_patterns/index_patterns'; -import { FieldDetails } from './types'; - -interface DiscoverFieldDetailsFooterProps { - field: IndexPatternField; - indexPattern: IndexPattern; - details: FieldDetails; - onAddFilter: (field: IndexPatternField | string, value: string, type: '+' | '-') => void; -} - -export function DiscoverFieldDetailsFooter({ - field, - indexPattern, - details, - onAddFilter, -}: DiscoverFieldDetailsFooterProps) { - return ( - - - {!indexPattern.metaFields.includes(field.name) && !field.scripted ? ( - onAddFilter('_exists_', field.name, '+')} - data-test-subj="onAddFilterButton" - > - - - ) : ( - - )} - - - ); -} diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_visualize.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_visualize.tsx new file mode 100644 index 0000000000000..baf740531e6bf --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_visualize.tsx @@ -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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useEffect, useState } from 'react'; +import { EuiButton, EuiPopoverFooter } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { METRIC_TYPE, UiCounterMetricType } from '@kbn/analytics'; +import type { IndexPattern, IndexPatternField } from 'src/plugins/data/common'; + +import { triggerVisualizeActions, VisualizeInformation } from './lib/visualize_trigger_utils'; +import type { FieldDetails } from './types'; +import { getVisualizeInformation } from './lib/visualize_trigger_utils'; + +interface Props { + field: IndexPatternField; + indexPattern: IndexPattern; + details: FieldDetails; + multiFields?: IndexPatternField[]; + trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void; +} + +export const DiscoverFieldVisualize: React.FC = React.memo( + ({ field, indexPattern, details, trackUiMetric, multiFields }) => { + const [visualizeInfo, setVisualizeInfo] = useState(); + + useEffect(() => { + getVisualizeInformation(field, indexPattern.id, details.columns, multiFields).then( + setVisualizeInfo + ); + }, [details.columns, field, indexPattern, multiFields]); + + if (!visualizeInfo) { + return null; + } + + const handleVisualizeLinkClick = (event: React.MouseEvent) => { + // regular link click. let the uiActions code handle the navigation and show popup if needed + event.preventDefault(); + trackUiMetric?.(METRIC_TYPE.CLICK, 'visualize_link_click'); + triggerVisualizeActions(visualizeInfo.field, indexPattern.id, details.columns); + }; + + return ( + + {/* eslint-disable-next-line @elastic/eui/href-or-on-click */} + + + + + ); + } +); diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/lib/get_warnings.ts b/src/plugins/discover/public/application/apps/main/components/sidebar/lib/get_warnings.ts deleted file mode 100644 index 60ce5351e2cd3..0000000000000 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/lib/get_warnings.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { i18n } from '@kbn/i18n'; -import { IndexPatternField } from '../../../../../../../../data/public'; - -export function getWarnings(field: IndexPatternField) { - let warnings = []; - - if (field.scripted) { - warnings.push( - i18n.translate( - 'discover.fieldChooser.discoverField.scriptedFieldsTakeLongExecuteDescription', - { - defaultMessage: 'Scripted fields can take a long time to execute.', - } - ) - ); - } - - if (warnings.length > 1) { - warnings = warnings.map(function (warning, i) { - return (i > 0 ? '\n' : '') + (i + 1) + ' - ' + warning; - }); - } - - return warnings; -} diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/lib/visualize_trigger_utils.test.ts b/src/plugins/discover/public/application/apps/main/components/sidebar/lib/visualize_trigger_utils.test.ts new file mode 100644 index 0000000000000..0a61bf1ea6029 --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/lib/visualize_trigger_utils.test.ts @@ -0,0 +1,116 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { IndexPatternField } from 'src/plugins/data/common'; +import type { Action } from 'src/plugins/ui_actions/public'; +import { getVisualizeInformation } from './visualize_trigger_utils'; + +const field = { + name: 'fieldName', + type: 'string', + esTypes: ['text'], + count: 1, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + visualizable: true, +} as IndexPatternField; + +const mockGetActions = jest.fn>>, [string, { fieldName: string }]>( + () => Promise.resolve([]) +); + +jest.mock('../../../../../../kibana_services', () => ({ + getUiActions: () => ({ + getTriggerCompatibleActions: mockGetActions, + }), +})); + +const action: Action = { + id: 'action', + type: 'VISUALIZE_FIELD', + getIconType: () => undefined, + getDisplayName: () => 'Action', + isCompatible: () => Promise.resolve(true), + execute: () => Promise.resolve(), +}; + +describe('visualize_trigger_utils', () => { + afterEach(() => { + mockGetActions.mockReset(); + }); + + describe('getVisualizeInformation', () => { + it('should return for a visualizeable field with an action', async () => { + mockGetActions.mockResolvedValue([action]); + const information = await getVisualizeInformation(field, '1', [], undefined); + expect(information).not.toBeUndefined(); + expect(information?.field).toHaveProperty('name', 'fieldName'); + expect(information?.href).toBeUndefined(); + }); + + it('should return field and href from the action', async () => { + mockGetActions.mockResolvedValue([{ ...action, getHref: () => Promise.resolve('hreflink') }]); + const information = await getVisualizeInformation(field, '1', [], undefined); + expect(information).not.toBeUndefined(); + expect(information?.field).toHaveProperty('name', 'fieldName'); + expect(information).toHaveProperty('href', 'hreflink'); + }); + + it('should return undefined if no field has a compatible action', async () => { + mockGetActions.mockResolvedValue([]); + const information = await getVisualizeInformation( + { ...field, name: 'rootField' } as IndexPatternField, + '1', + [], + [ + { ...field, name: 'multi1' }, + { ...field, name: 'multi2' }, + ] as IndexPatternField[] + ); + expect(information).toBeUndefined(); + }); + + it('should return information for the root field, when multi fields and root are having actions', async () => { + mockGetActions.mockResolvedValue([action]); + const information = await getVisualizeInformation( + { ...field, name: 'rootField' } as IndexPatternField, + '1', + [], + [ + { ...field, name: 'multi1' }, + { ...field, name: 'multi2' }, + ] as IndexPatternField[] + ); + expect(information).not.toBeUndefined(); + expect(information?.field).toHaveProperty('name', 'rootField'); + }); + + it('should return information for first multi field that has a compatible action', async () => { + mockGetActions.mockImplementation(async (_, { fieldName }) => { + if (fieldName === 'multi2' || fieldName === 'multi3') { + return [action]; + } + return []; + }); + const information = await getVisualizeInformation( + { ...field, name: 'rootField' } as IndexPatternField, + '1', + [], + [ + { ...field, name: 'multi1' }, + { ...field, name: 'multi2' }, + { ...field, name: 'multi3' }, + ] as IndexPatternField[] + ); + expect(information).not.toBeUndefined(); + expect(information?.field).toHaveProperty('name', 'multi2'); + }); + }); +}); diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/lib/visualize_trigger_utils.ts b/src/plugins/discover/public/application/apps/main/components/sidebar/lib/visualize_trigger_utils.ts index 2fabaa0ddd100..f00b430e5acef 100644 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/lib/visualize_trigger_utils.ts +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/lib/visualize_trigger_utils.ts @@ -41,30 +41,6 @@ async function getCompatibleActions( return compatibleActions; } -export async function getVisualizeHref( - field: IndexPatternField, - indexPatternId: string | undefined, - contextualFields: string[] -) { - if (!indexPatternId) return undefined; - const triggerOptions = { - indexPatternId, - fieldName: field.name, - contextualFields, - trigger: getTrigger(field.type), - }; - const compatibleActions = await getCompatibleActions( - field.name, - indexPatternId, - contextualFields, - getTriggerConstant(field.type) - ); - // enable the link only if only one action is registered - return compatibleActions.length === 1 - ? compatibleActions[0].getHref?.(triggerOptions) - : undefined; -} - export function triggerVisualizeActions( field: IndexPatternField, indexPatternId: string | undefined, @@ -80,21 +56,55 @@ export function triggerVisualizeActions( getUiActions().getTrigger(trigger).exec(triggerOptions); } -export async function isFieldVisualizable( +export interface VisualizeInformation { + field: IndexPatternField; + href?: string; +} + +/** + * Returns the field name and potentially href of the field or the first multi-field + * that has a compatible visualize uiAction. + */ +export async function getVisualizeInformation( field: IndexPatternField, indexPatternId: string | undefined, - contextualFields: string[] -) { + contextualFields: string[], + multiFields: IndexPatternField[] = [] +): Promise { if (field.name === '_id' || !indexPatternId) { - // for first condition you'd get a 'Fielddata access on the _id field is disallowed' error on ES side. - return false; + // _id fields are not visualizeable in ES + return undefined; } - const trigger = getTriggerConstant(field.type); - const compatibleActions = await getCompatibleActions( - field.name, - indexPatternId, - contextualFields, - trigger - ); - return compatibleActions.length > 0 && field.visualizable; + + for (const f of [field, ...multiFields]) { + if (!f.visualizable) { + continue; + } + // Retrieve compatible actions for the specific field + const actions = await getCompatibleActions( + f.name, + indexPatternId, + contextualFields, + getTriggerConstant(f.type) + ); + + // if the field has compatible actions use this field for visualizing + if (actions.length > 0) { + const triggerOptions = { + indexPatternId, + fieldName: f.name, + contextualFields, + trigger: getTrigger(f.type), + }; + + return { + field: f, + // We use the href of the first action always. Multiple actions will only work + // via the modal shown by triggerVisualizeActions that should be called via onClick. + href: await actions[0].getHref?.(triggerOptions), + }; + } + } + + return undefined; } diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 837716ec9dd5a..5f86e020a0407 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -1625,7 +1625,6 @@ "discover.fieldChooser.detailViews.filterOutValueButtonAriaLabel": "{field}を除外:\"{value}\"", "discover.fieldChooser.detailViews.filterValueButtonAriaLabel": "{field}を除外:\"{value}\"", "discover.fieldChooser.detailViews.valueOfRecordsText": "{value} / {totalValue}件のレコード", - "discover.fieldChooser.detailViews.visualizeLinkText": "可視化", "discover.fieldChooser.discoverField.addButtonAriaLabel": "{field}を表に追加", "discover.fieldChooser.discoverField.addFieldTooltip": "フィールドを列として追加", "discover.fieldChooser.discoverField.deleteFieldLabel": "インデックスパターンフィールドを削除", @@ -1634,7 +1633,6 @@ "discover.fieldChooser.discoverField.multiFields": "マルチフィールド", "discover.fieldChooser.discoverField.removeButtonAriaLabel": "{field}を表から削除", "discover.fieldChooser.discoverField.removeFieldTooltip": "フィールドを表から削除", - "discover.fieldChooser.discoverField.scriptedFieldsTakeLongExecuteDescription": "スクリプトフィールドは実行に時間がかかる場合があります。", "discover.fieldChooser.fieldCalculator.analysisIsNotAvailableForGeoFieldsErrorMessage": "ジオフィールドは分析できません。", "discover.fieldChooser.fieldCalculator.analysisIsNotAvailableForObjectFieldsErrorMessage": "オブジェクトフィールドは分析できません。", "discover.fieldChooser.fieldCalculator.fieldIsNotPresentInDocumentsErrorMessage": "このフィールドはElasticsearchマッピングに表示されますが、ドキュメントテーブルの{hitsLength}件のドキュメントには含まれません。可視化や検索は可能な場合があります。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 0192566db0731..acad67a0b1c7a 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -1634,7 +1634,6 @@ "discover.fieldChooser.detailViews.filterOutValueButtonAriaLabel": "筛除 {field}:“{value}”", "discover.fieldChooser.detailViews.filterValueButtonAriaLabel": "筛留 {field}:“{value}”", "discover.fieldChooser.detailViews.valueOfRecordsText": "{value} / {totalValue} 条记录", - "discover.fieldChooser.detailViews.visualizeLinkText": "Visualize", "discover.fieldChooser.discoverField.addButtonAriaLabel": "将 {field} 添加到表中", "discover.fieldChooser.discoverField.addFieldTooltip": "将字段添加为列", "discover.fieldChooser.discoverField.deleteFieldLabel": "删除索引模式字段", @@ -1643,7 +1642,6 @@ "discover.fieldChooser.discoverField.multiFields": "多字段", "discover.fieldChooser.discoverField.removeButtonAriaLabel": "从表中移除 {field}", "discover.fieldChooser.discoverField.removeFieldTooltip": "从表中移除字段", - "discover.fieldChooser.discoverField.scriptedFieldsTakeLongExecuteDescription": "脚本字段执行时间会很长。", "discover.fieldChooser.fieldCalculator.analysisIsNotAvailableForGeoFieldsErrorMessage": "分析不适用于地理字段。", "discover.fieldChooser.fieldCalculator.analysisIsNotAvailableForObjectFieldsErrorMessage": "分析不适用于对象字段。", "discover.fieldChooser.fieldCalculator.fieldIsNotPresentInDocumentsErrorMessage": "此字段在您的 Elasticsearch 映射中,但不在文档表中显示的 {hitsLength} 个文档中。您可能仍能够基于它可视化或搜索。", From 23c8d181989b69b8178d0c89c757fc9ce0e9df68 Mon Sep 17 00:00:00 2001 From: Spencer Date: Thu, 24 Jun 2021 10:59:49 -0700 Subject: [PATCH 45/69] [ui-shared-deps] reuse react-beautiful-dnd from eui (#102834) Co-authored-by: spalger --- packages/kbn-ui-shared-deps/src/entry.js | 1 + packages/kbn-ui-shared-deps/src/index.js | 2 ++ 2 files changed, 3 insertions(+) diff --git a/packages/kbn-ui-shared-deps/src/entry.js b/packages/kbn-ui-shared-deps/src/entry.js index b8d21a473c65f..0e91c45ae6392 100644 --- a/packages/kbn-ui-shared-deps/src/entry.js +++ b/packages/kbn-ui-shared-deps/src/entry.js @@ -40,6 +40,7 @@ export const ElasticEui = require('@elastic/eui'); export const ElasticEuiLibServices = require('@elastic/eui/lib/services'); export const ElasticEuiLibServicesFormat = require('@elastic/eui/lib/services/format'); export const ElasticEuiChartsTheme = require('@elastic/eui/dist/eui_charts_theme'); +export const ReactBeautifulDnD = require('react-beautiful-dnd'); export const Theme = require('./theme.ts'); export const Lodash = require('lodash'); export const LodashFp = require('lodash/fp'); diff --git a/packages/kbn-ui-shared-deps/src/index.js b/packages/kbn-ui-shared-deps/src/index.js index c5853dc091875..36c2e6b02879e 100644 --- a/packages/kbn-ui-shared-deps/src/index.js +++ b/packages/kbn-ui-shared-deps/src/index.js @@ -85,6 +85,8 @@ exports.externals = { '@elastic/eui/dist/eui_charts_theme': '__kbnSharedDeps__.ElasticEuiChartsTheme', '@elastic/eui/dist/eui_theme_light.json': '__kbnSharedDeps__.Theme.euiLightVars', '@elastic/eui/dist/eui_theme_dark.json': '__kbnSharedDeps__.Theme.euiDarkVars', + // transient dep of eui + 'react-beautiful-dnd': '__kbnSharedDeps__.ReactBeautifulDnD', lodash: '__kbnSharedDeps__.Lodash', 'lodash/fp': '__kbnSharedDeps__.LodashFp', fflate: '__kbnSharedDeps__.Fflate', From fb7b596841be2c115cd2ef78d62eeaf5d994aa78 Mon Sep 17 00:00:00 2001 From: Kyle Pollich Date: Thu, 24 Jun 2021 14:29:56 -0400 Subject: [PATCH 46/69] Fix missing setting modal in integrations app (#103317) --- .../public/applications/integrations/app.tsx | 30 +++++++++++++------ 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/fleet/public/applications/integrations/app.tsx b/x-pack/plugins/fleet/public/applications/integrations/app.tsx index c69b6805e0e86..cad51a54d7074 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/app.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/app.tsx @@ -7,7 +7,7 @@ import React, { memo, useEffect, useState } from 'react'; import type { AppMountParameters } from 'kibana/public'; -import { EuiCode, EuiEmptyPrompt, EuiErrorBoundary, EuiPanel } from '@elastic/eui'; +import { EuiCode, EuiEmptyPrompt, EuiErrorBoundary, EuiPanel, EuiPortal } from '@elastic/eui'; import type { History } from 'history'; import { createHashHistory } from 'history'; import { Router, Redirect, Route, Switch } from 'react-router-dom'; @@ -29,10 +29,10 @@ import type { FleetConfigType, FleetStartServices } from '../../plugin'; import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; import { EuiThemeProvider } from '../../../../../../src/plugins/kibana_react/common'; -import { AgentPolicyContextProvider } from './hooks'; +import { AgentPolicyContextProvider, useUrlModal } from './hooks'; import { INTEGRATIONS_ROUTING_PATHS } from './constants'; -import { Error, Loading } from './components'; +import { Error, Loading, SettingFlyout } from './components'; import type { UIExtensionsStorage } from './types'; @@ -234,12 +234,24 @@ export const IntegrationsAppContext: React.FC<{ ); export const AppRoutes = memo(() => { + const { modal, setModal } = useUrlModal(); return ( - - - - - - + <> + {modal === 'settings' && ( + + { + setModal(null); + }} + /> + + )} + + + + + + + ); }); From fbcf405f156ab99ab40d0de6993479712a24f55b Mon Sep 17 00:00:00 2001 From: Josh Dover <1813008+joshdover@users.noreply.github.com> Date: Thu, 24 Jun 2021 20:47:38 +0200 Subject: [PATCH 47/69] Add telemetry for Elastic Cloud (#102390) --- NOTICE.txt | 4 + package.json | 9 +- x-pack/plugins/cloud/public/fullstory.ts | 100 +++++++++++ .../plugins/cloud/public/plugin.test.mocks.ts | 13 ++ x-pack/plugins/cloud/public/plugin.test.ts | 164 ++++++++++++++++-- x-pack/plugins/cloud/public/plugin.ts | 79 ++++++++- .../cloud/server/assets/fullstory_library.js | 13 ++ x-pack/plugins/cloud/server/config.test.ts | 6 +- x-pack/plugins/cloud/server/plugin.ts | 8 + .../cloud/server/routes/fullstory.test.ts | 62 +++++++ .../plugins/cloud/server/routes/fullstory.ts | 75 ++++++++ x-pack/test/cloud_integration/config.ts | 87 ++++++++++ x-pack/test/cloud_integration/constants.ts | 8 + .../fixtures/saml/saml_provider/kibana.json | 7 + .../fixtures/saml/saml_provider/metadata.xml | 41 +++++ .../saml/saml_provider/server/index.ts | 15 ++ .../saml/saml_provider/server/init_routes.ts | 58 +++++++ .../saml/saml_provider/server/saml_tools.ts | 161 +++++++++++++++++ .../ftr_provider_context.d.ts | 13 ++ .../test/cloud_integration/tests/fullstory.ts | 66 +++++++ yarn.lock | 5 + 21 files changed, 973 insertions(+), 21 deletions(-) create mode 100644 x-pack/plugins/cloud/public/fullstory.ts create mode 100644 x-pack/plugins/cloud/public/plugin.test.mocks.ts create mode 100644 x-pack/plugins/cloud/server/assets/fullstory_library.js create mode 100644 x-pack/plugins/cloud/server/routes/fullstory.test.ts create mode 100644 x-pack/plugins/cloud/server/routes/fullstory.ts create mode 100644 x-pack/test/cloud_integration/config.ts create mode 100644 x-pack/test/cloud_integration/constants.ts create mode 100644 x-pack/test/cloud_integration/fixtures/saml/saml_provider/kibana.json create mode 100644 x-pack/test/cloud_integration/fixtures/saml/saml_provider/metadata.xml create mode 100644 x-pack/test/cloud_integration/fixtures/saml/saml_provider/server/index.ts create mode 100644 x-pack/test/cloud_integration/fixtures/saml/saml_provider/server/init_routes.ts create mode 100644 x-pack/test/cloud_integration/fixtures/saml/saml_provider/server/saml_tools.ts create mode 100644 x-pack/test/cloud_integration/ftr_provider_context.d.ts create mode 100644 x-pack/test/cloud_integration/tests/fullstory.ts diff --git a/NOTICE.txt b/NOTICE.txt index 4eec329b7a603..b0f7e65f46fa9 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -235,6 +235,10 @@ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +--- +Portions of this code are licensed under the following license: +For license information please see https://edge.fullstory.com/s/fs.js.LICENSE.txt + --- This product bundles bootstrap@3.3.6 which is available under a "MIT" license. diff --git a/package.json b/package.json index ecedb64c343ec..ceb178d068519 100644 --- a/package.json +++ b/package.json @@ -128,25 +128,26 @@ "@kbn/analytics": "link:bazel-bin/packages/kbn-analytics", "@kbn/apm-config-loader": "link:bazel-bin/packages/kbn-apm-config-loader", "@kbn/apm-utils": "link:bazel-bin/packages/kbn-apm-utils", + "@kbn/common-utils": "link:bazel-bin/packages/kbn-common-utils", "@kbn/config": "link:bazel-bin/packages/kbn-config", "@kbn/config-schema": "link:bazel-bin/packages/kbn-config-schema", "@kbn/crypto": "link:bazel-bin/packages/kbn-crypto", - "@kbn/mapbox-gl": "link:bazel-bin/packages/kbn-mapbox-gl", "@kbn/i18n": "link:bazel-bin/packages/kbn-i18n", "@kbn/interpreter": "link:bazel-bin/packages/kbn-interpreter", "@kbn/io-ts-utils": "link:bazel-bin/packages/kbn-io-ts-utils", "@kbn/legacy-logging": "link:bazel-bin/packages/kbn-legacy-logging", "@kbn/logging": "link:bazel-bin/packages/kbn-logging", + "@kbn/mapbox-gl": "link:bazel-bin/packages/kbn-mapbox-gl", "@kbn/monaco": "link:bazel-bin/packages/kbn-monaco", "@kbn/rule-data-utils": "link:bazel-bin/packages/kbn-rule-data-utils", - "@kbn/securitysolution-list-constants": "link:bazel-bin/packages/kbn-securitysolution-list-constants", "@kbn/securitysolution-es-utils": "link:bazel-bin/packages/kbn-securitysolution-es-utils", "@kbn/securitysolution-hook-utils": "link:bazel-bin/packages/kbn-securitysolution-hook-utils", - "@kbn/securitysolution-io-ts-types": "link:bazel-bin/packages/kbn-securitysolution-io-ts-types", "@kbn/securitysolution-io-ts-alerting-types": "link:bazel-bin/packages/kbn-securitysolution-io-ts-alerting-types", "@kbn/securitysolution-io-ts-list-types": "link:bazel-bin/packages/kbn-securitysolution-io-ts-list-types", + "@kbn/securitysolution-io-ts-types": "link:bazel-bin/packages/kbn-securitysolution-io-ts-types", "@kbn/securitysolution-io-ts-utils": "link:bazel-bin/packages/kbn-securitysolution-io-ts-utils", "@kbn/securitysolution-list-api": "link:bazel-bin/packages/kbn-securitysolution-list-api", + "@kbn/securitysolution-list-constants": "link:bazel-bin/packages/kbn-securitysolution-list-constants", "@kbn/securitysolution-list-hooks": "link:bazel-bin/packages/kbn-securitysolution-list-hooks", "@kbn/securitysolution-list-utils": "link:bazel-bin/packages/kbn-securitysolution-list-utils", "@kbn/securitysolution-t-grid": "link:bazel-bin/packages/kbn-securitysolution-t-grid", @@ -158,7 +159,6 @@ "@kbn/ui-framework": "link:bazel-bin/packages/kbn-ui-framework", "@kbn/ui-shared-deps": "link:bazel-bin/packages/kbn-ui-shared-deps", "@kbn/utility-types": "link:bazel-bin/packages/kbn-utility-types", - "@kbn/common-utils": "link:bazel-bin/packages/kbn-common-utils", "@kbn/utils": "link:bazel-bin/packages/kbn-utils", "@loaders.gl/core": "^2.3.1", "@loaders.gl/json": "^2.3.1", @@ -273,6 +273,7 @@ "jquery": "^3.5.0", "js-levenshtein": "^1.1.6", "js-search": "^1.4.3", + "js-sha256": "^0.9.0", "js-yaml": "^3.14.0", "json-stable-stringify": "^1.0.1", "json-stringify-pretty-compact": "1.2.0", diff --git a/x-pack/plugins/cloud/public/fullstory.ts b/x-pack/plugins/cloud/public/fullstory.ts new file mode 100644 index 0000000000000..31e5ec128b9a3 --- /dev/null +++ b/x-pack/plugins/cloud/public/fullstory.ts @@ -0,0 +1,100 @@ +/* + * 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 { sha256 } from 'js-sha256'; +import type { IBasePath, PackageInfo } from '../../../../src/core/public'; + +export interface FullStoryDeps { + basePath: IBasePath; + orgId: string; + packageInfo: PackageInfo; + userIdPromise: Promise; +} + +interface FullStoryApi { + identify(userId: string, userVars?: Record): void; + event(eventName: string, eventProperties: Record): void; +} + +export const initializeFullStory = async ({ + basePath, + orgId, + packageInfo, + userIdPromise, +}: FullStoryDeps) => { + // @ts-expect-error + window._fs_debug = false; + // @ts-expect-error + window._fs_host = 'fullstory.com'; + // @ts-expect-error + window._fs_script = basePath.prepend(`/internal/cloud/${packageInfo.buildNum}/fullstory.js`); + // @ts-expect-error + window._fs_org = orgId; + // @ts-expect-error + window._fs_namespace = 'FSKibana'; + + /* eslint-disable */ + (function(m,n,e,t,l,o,g,y){ + if (e in m) {if(m.console && m.console.log) { m.console.log('FullStory namespace conflict. Please set window["_fs_namespace"].');} return;} + // @ts-expect-error + g=m[e]=function(a,b,s){g.q?g.q.push([a,b,s]):g._api(a,b,s);};g.q=[]; + // @ts-expect-error + o=n.createElement(t);o.async=1;o.crossOrigin='anonymous';o.src=_fs_script; + // @ts-expect-error + y=n.getElementsByTagName(t)[0];y.parentNode.insertBefore(o,y); + // @ts-expect-error + g.identify=function(i,v,s){g(l,{uid:i},s);if(v)g(l,v,s)};g.setUserVars=function(v,s){g(l,v,s)};g.event=function(i,v,s){g('event',{n:i,p:v},s)}; + // @ts-expect-error + g.anonymize=function(){g.identify(!!0)}; + // @ts-expect-error + g.shutdown=function(){g("rec",!1)};g.restart=function(){g("rec",!0)}; + // @ts-expect-error + g.log = function(a,b){g("log",[a,b])}; + // @ts-expect-error + g.consent=function(a){g("consent",!arguments.length||a)}; + // @ts-expect-error + g.identifyAccount=function(i,v){o='account';v=v||{};v.acctId=i;g(o,v)}; + // @ts-expect-error + g.clearUserCookie=function(){}; + // @ts-expect-error + g.setVars=function(n, p){g('setVars',[n,p]);}; + // @ts-expect-error + g._w={};y='XMLHttpRequest';g._w[y]=m[y];y='fetch';g._w[y]=m[y]; + // @ts-expect-error + if(m[y])m[y]=function(){return g._w[y].apply(this,arguments)}; + // @ts-expect-error + g._v="1.3.0"; + // @ts-expect-error + })(window,document,window['_fs_namespace'],'script','user'); + /* eslint-enable */ + + // @ts-expect-error + const fullstory: FullStoryApi = window.FSKibana; + + // Record an event that Kibana was opened so we can easily search for sessions that use Kibana + // @ts-expect-error + window.FSKibana.event('Loaded Kibana', { + kibana_version_str: packageInfo.version, + }); + + // Use a promise here so we don't have to wait to retrieve the user to start recording the session + userIdPromise + .then((userId) => { + if (!userId) return; + // Do the hashing here to keep it at clear as possible in our source code that we do not send literal user IDs + const hashedId = sha256(userId.toString()); + // @ts-expect-error + window.FSKibana.identify(hashedId); + }) + .catch((e) => { + // eslint-disable-next-line no-console + console.error( + `[cloud.full_story] Could not call FS.identify due to error: ${e.toString()}`, + e + ); + }); +}; diff --git a/x-pack/plugins/cloud/public/plugin.test.mocks.ts b/x-pack/plugins/cloud/public/plugin.test.mocks.ts new file mode 100644 index 0000000000000..889b8492d5b1b --- /dev/null +++ b/x-pack/plugins/cloud/public/plugin.test.mocks.ts @@ -0,0 +1,13 @@ +/* + * 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 { FullStoryDeps } from './fullstory'; + +export const initializeFullStoryMock = jest.fn(); +jest.doMock('./fullstory', () => { + return { initializeFullStory: initializeFullStoryMock }; +}); diff --git a/x-pack/plugins/cloud/public/plugin.test.ts b/x-pack/plugins/cloud/public/plugin.test.ts index 981c5138d98d2..af4d3c4c9005d 100644 --- a/x-pack/plugins/cloud/public/plugin.test.ts +++ b/x-pack/plugins/cloud/public/plugin.test.ts @@ -9,11 +9,98 @@ import { nextTick } from '@kbn/test/jest'; import { coreMock } from 'src/core/public/mocks'; import { homePluginMock } from 'src/plugins/home/public/mocks'; import { securityMock } from '../../security/public/mocks'; -import { CloudPlugin } from './plugin'; +import { initializeFullStoryMock } from './plugin.test.mocks'; +import { CloudPlugin, CloudConfigType, loadFullStoryUserId } from './plugin'; describe('Cloud Plugin', () => { + describe('#setup', () => { + describe('setupFullstory', () => { + beforeEach(() => { + initializeFullStoryMock.mockReset(); + }); + + const setupPlugin = async ({ + config = {}, + securityEnabled = true, + currentUserProps = {}, + }: { + config?: Partial; + securityEnabled?: boolean; + currentUserProps?: Record; + }) => { + const initContext = coreMock.createPluginInitializerContext({ + id: 'cloudId', + base_url: 'https://cloud.elastic.co', + deployment_url: '/abc123', + profile_url: '/profile/alice', + organization_url: '/org/myOrg', + full_story: { + enabled: false, + }, + ...config, + }); + const plugin = new CloudPlugin(initContext); + + const coreSetup = coreMock.createSetup(); + const securitySetup = securityMock.createSetup(); + securitySetup.authc.getCurrentUser.mockResolvedValue( + securityMock.createMockAuthenticatedUser(currentUserProps) + ); + + const setup = plugin.setup(coreSetup, securityEnabled ? { security: securitySetup } : {}); + // Wait for fullstory dynamic import to resolve + await new Promise((r) => setImmediate(r)); + + return { initContext, plugin, setup }; + }; + + it('calls initializeFullStory with correct args when enabled and org_id are set', async () => { + const { initContext } = await setupPlugin({ + config: { full_story: { enabled: true, org_id: 'foo' } }, + currentUserProps: { + username: '1234', + }, + }); + + expect(initializeFullStoryMock).toHaveBeenCalled(); + const { + basePath, + orgId, + packageInfo, + userIdPromise, + } = initializeFullStoryMock.mock.calls[0][0]; + expect(basePath.prepend).toBeDefined(); + expect(orgId).toEqual('foo'); + expect(packageInfo).toEqual(initContext.env.packageInfo); + expect(await userIdPromise).toEqual('1234'); + }); + + it('passes undefined user ID when security is not available', async () => { + await setupPlugin({ + config: { full_story: { enabled: true, org_id: 'foo' } }, + securityEnabled: false, + }); + + expect(initializeFullStoryMock).toHaveBeenCalled(); + const { orgId, userIdPromise } = initializeFullStoryMock.mock.calls[0][0]; + expect(orgId).toEqual('foo'); + expect(await userIdPromise).toEqual(undefined); + }); + + it('does not call initializeFullStory when enabled=false', async () => { + await setupPlugin({ config: { full_story: { enabled: false, org_id: 'foo' } } }); + expect(initializeFullStoryMock).not.toHaveBeenCalled(); + }); + + it('does not call initializeFullStory when org_id is undefined', async () => { + await setupPlugin({ config: { full_story: { enabled: true } } }); + expect(initializeFullStoryMock).not.toHaveBeenCalled(); + }); + }); + }); + describe('#start', () => { - function setupPlugin() { + const startPlugin = () => { const plugin = new CloudPlugin( coreMock.createPluginInitializerContext({ id: 'cloudId', @@ -21,6 +108,9 @@ describe('Cloud Plugin', () => { deployment_url: '/abc123', profile_url: '/profile/alice', organization_url: '/org/myOrg', + full_story: { + enabled: false, + }, }) ); const coreSetup = coreMock.createSetup(); @@ -29,10 +119,10 @@ describe('Cloud Plugin', () => { plugin.setup(coreSetup, { home: homeSetup }); return { coreSetup, plugin }; - } + }; it('registers help support URL', async () => { - const { plugin } = setupPlugin(); + const { plugin } = startPlugin(); const coreStart = coreMock.createStart(); const securityStart = securityMock.createStart(); @@ -47,7 +137,7 @@ describe('Cloud Plugin', () => { }); it('does not register custom nav links on anonymous pages', async () => { - const { plugin } = setupPlugin(); + const { plugin } = startPlugin(); const coreStart = coreMock.createStart(); coreStart.http.anonymousPaths.isAnonymous.mockReturnValue(true); @@ -68,7 +158,7 @@ describe('Cloud Plugin', () => { }); it('registers a custom nav link for superusers', async () => { - const { plugin } = setupPlugin(); + const { plugin } = startPlugin(); const coreStart = coreMock.createStart(); const securityStart = securityMock.createStart(); @@ -94,7 +184,7 @@ describe('Cloud Plugin', () => { }); it('registers a custom nav link when there is an error retrieving the current user', async () => { - const { plugin } = setupPlugin(); + const { plugin } = startPlugin(); const coreStart = coreMock.createStart(); const securityStart = securityMock.createStart(); @@ -116,7 +206,7 @@ describe('Cloud Plugin', () => { }); it('does not register a custom nav link for non-superusers', async () => { - const { plugin } = setupPlugin(); + const { plugin } = startPlugin(); const coreStart = coreMock.createStart(); const securityStart = securityMock.createStart(); @@ -133,7 +223,7 @@ describe('Cloud Plugin', () => { }); it('registers user profile links for superusers', async () => { - const { plugin } = setupPlugin(); + const { plugin } = startPlugin(); const coreStart = coreMock.createStart(); const securityStart = securityMock.createStart(); @@ -169,7 +259,7 @@ describe('Cloud Plugin', () => { }); it('registers profile links when there is an error retrieving the current user', async () => { - const { plugin } = setupPlugin(); + const { plugin } = startPlugin(); const coreStart = coreMock.createStart(); const securityStart = securityMock.createStart(); @@ -201,7 +291,7 @@ describe('Cloud Plugin', () => { }); it('does not register profile links for non-superusers', async () => { - const { plugin } = setupPlugin(); + const { plugin } = startPlugin(); const coreStart = coreMock.createStart(); const securityStart = securityMock.createStart(); @@ -217,4 +307,56 @@ describe('Cloud Plugin', () => { expect(securityStart.navControlService.addUserMenuLinks).not.toHaveBeenCalled(); }); }); + + describe('loadFullStoryUserId', () => { + let consoleMock: jest.SpyInstance; + + beforeEach(() => { + consoleMock = jest.spyOn(console, 'debug').mockImplementation(() => {}); + }); + afterEach(() => { + consoleMock.mockRestore(); + }); + + it('returns principal ID when username specified', async () => { + expect( + await loadFullStoryUserId({ + getCurrentUser: jest.fn().mockResolvedValue({ + username: '1234', + }), + }) + ).toEqual('1234'); + expect(consoleMock).not.toHaveBeenCalled(); + }); + + it('returns undefined if getCurrentUser throws', async () => { + expect( + await loadFullStoryUserId({ + getCurrentUser: jest.fn().mockRejectedValue(new Error(`Oh no!`)), + }) + ).toBeUndefined(); + }); + + it('returns undefined if getCurrentUser returns undefined', async () => { + expect( + await loadFullStoryUserId({ + getCurrentUser: jest.fn().mockResolvedValue(undefined), + }) + ).toBeUndefined(); + }); + + it('returns undefined and logs if username undefined', async () => { + expect( + await loadFullStoryUserId({ + getCurrentUser: jest.fn().mockResolvedValue({ + username: undefined, + metadata: { foo: 'bar' }, + }), + }) + ).toBeUndefined(); + expect(consoleMock).toHaveBeenLastCalledWith( + `[cloud.full_story] username not specified. User metadata: {"foo":"bar"}` + ); + }); + }); }); diff --git a/x-pack/plugins/cloud/public/plugin.ts b/x-pack/plugins/cloud/public/plugin.ts index 5820452706539..68dece1bc5d3d 100644 --- a/x-pack/plugins/cloud/public/plugin.ts +++ b/x-pack/plugins/cloud/public/plugin.ts @@ -5,9 +5,20 @@ * 2.0. */ -import { CoreSetup, CoreStart, Plugin, PluginInitializerContext, HttpStart } from 'src/core/public'; +import { + CoreSetup, + CoreStart, + Plugin, + PluginInitializerContext, + HttpStart, + IBasePath, +} from 'src/core/public'; import { i18n } from '@kbn/i18n'; -import { SecurityPluginSetup, SecurityPluginStart } from '../../security/public'; +import type { + AuthenticatedUser, + SecurityPluginSetup, + SecurityPluginStart, +} from '../../security/public'; import { getIsCloudEnabled } from '../common/is_cloud_enabled'; import { ELASTIC_SUPPORT_LINK } from '../common/constants'; import { HomePublicPluginSetup } from '../../../../src/plugins/home/public'; @@ -21,6 +32,10 @@ export interface CloudConfigType { profile_url?: string; deployment_url?: string; organization_url?: string; + full_story: { + enabled: boolean; + org_id?: string; + }; } interface CloudSetupDependencies { @@ -51,7 +66,12 @@ export class CloudPlugin implements Plugin { this.isCloudEnabled = false; } - public setup(core: CoreSetup, { home }: CloudSetupDependencies) { + public setup(core: CoreSetup, { home, security }: CloudSetupDependencies) { + this.setupFullstory({ basePath: core.http.basePath, security }).catch((e) => + // eslint-disable-next-line no-console + console.debug(`Error setting up FullStory: ${e.toString()}`) + ); + const { id, cname, @@ -135,4 +155,57 @@ export class CloudPlugin implements Plugin { const user = await security.authc.getCurrentUser().catch(() => null); return user?.roles.includes('superuser') ?? true; } + + private async setupFullstory({ + basePath, + security, + }: CloudSetupDependencies & { basePath: IBasePath }) { + const { enabled, org_id: orgId } = this.config.full_story; + if (!enabled || !orgId) { + return; + } + + // Keep this import async so that we do not load any FullStory code into the browser when it is disabled. + const { initializeFullStory } = await import('./fullstory'); + const userIdPromise: Promise = security + ? loadFullStoryUserId({ getCurrentUser: security.authc.getCurrentUser }) + : Promise.resolve(undefined); + + initializeFullStory({ + basePath, + orgId, + packageInfo: this.initializerContext.env.packageInfo, + userIdPromise, + }); + } } + +/** @internal exported for testing */ +export const loadFullStoryUserId = async ({ + getCurrentUser, +}: { + getCurrentUser: () => Promise; +}) => { + try { + const currentUser = await getCurrentUser().catch(() => undefined); + if (!currentUser) { + return undefined; + } + + // Log very defensively here so we can debug this easily if it breaks + if (!currentUser.username) { + // eslint-disable-next-line no-console + console.debug( + `[cloud.full_story] username not specified. User metadata: ${JSON.stringify( + currentUser.metadata + )}` + ); + } + + return currentUser.username; + } catch (e) { + // eslint-disable-next-line no-console + console.error(`[cloud.full_story] Error loading the current user: ${e.toString()}`, e); + return undefined; + } +}; diff --git a/x-pack/plugins/cloud/server/assets/fullstory_library.js b/x-pack/plugins/cloud/server/assets/fullstory_library.js new file mode 100644 index 0000000000000..487d6c8e8528c --- /dev/null +++ b/x-pack/plugins/cloud/server/assets/fullstory_library.js @@ -0,0 +1,13 @@ +/* + * 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. + */ + +/* @notice + * Portions of this code are licensed under the following license: + * For license information please see https://edge.fullstory.com/s/fs.js.LICENSE.txt + */ +/* eslint-disable */ +!function(e){var t={};function n(r){if(t[r])return t[r].exports;var i=t[r]={i:r,l:!1,exports:{}};return e[r].call(i.exports,i,i.exports,n),i.l=!0,i.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var i in e)n.d(r,i,function(t){return e[t]}.bind(null,i));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e["default"]}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="/s",n(n.s=4)}([function(e,t,n){"use strict";var r=this&&this.__assign||function(){return(r=Object.assign||function(e){for(var t,n=1,r=arguments.length;n0},t.assertExhaustive=function(e,t){throw void 0===t&&(t="Reached unexpected case in exhaustive switch"),new Error(t)},t.pick=function(e){for(var t=[],n=1;na&&(i=a);var u=n.split(/[#,]/);if(u.length<3&&(u=n.split("`")).length<3)return null;var c=u[0],h=u[1],d=u[2],l=u[3],p="";void 0!==l&&(p=decodeURIComponent(l),(f.indexOf(p)>=0||v.indexOf(p)>=0)&&(o("Ignoring invalid app key \""+p+"\" from cookie."),p=""));var m=d.split(":");return{expirationAbsTimeSeconds:i,host:c,orgId:h,userId:m[0],sessionId:m[1]||"",appKeyHash:p}}function y(e){for(var t={},n=e.cookie.split(";"),r=0;r=0&&(t=t.slice(0,n)),t}(e))?e:0==e.indexOf("www.")?"app."+e.slice(4):"app."+e:e}function H(e){return e?e+"/s/fs.js":void 0}!function(e){e.MUT_INSERT=2,e.MUT_REMOVE=3,e.MUT_ATTR=4,e.MUT_TEXT=6,e.MOUSEMOVE=8,e.MOUSEMOVE_CURVE=9,e.SCROLL_LAYOUT=10,e.SCROLL_LAYOUT_CURVE=11,e.MOUSEDOWN=12,e.MOUSEUP=13,e.KEYDOWN=14,e.KEYUP=15,e.CLICK=16,e.FOCUS=17,e.VALUECHANGE=18,e.RESIZE_LAYOUT=19,e.DOMLOADED=20,e.LOAD=21,e.PLACEHOLDER_SIZE=22,e.UNLOAD=23,e.BLUR=24,e.SET_FRAME_BASE=25,e.TOUCHSTART=32,e.TOUCHEND=33,e.TOUCHCANCEL=34,e.TOUCHMOVE=35,e.TOUCHMOVE_CURVE=36,e.NAVIGATE=37,e.PLAY=38,e.PAUSE=39,e.RESIZE_VISUAL=40,e.RESIZE_VISUAL_CURVE=41,e.RESIZE_DOCUMENT=42,e.LOG=48,e.ERROR=49,e.DBL_CLICK=50,e.FORM_SUBMIT=51,e.WINDOW_FOCUS=52,e.WINDOW_BLUR=53,e.HEARTBEAT=54,e.WATCHED_ELEM=56,e.PERF_ENTRY=57,e.REC_FEAT_SUPPORTED=58,e.SELECT=59,e.CSSRULE_INSERT=60,e.CSSRULE_DELETE=61,e.FAIL_THROTTLED=62,e.AJAX_REQUEST=63,e.SCROLL_VISUAL_OFFSET=64,e.SCROLL_VISUAL_OFFSET_CURVE=65,e.MEDIA_QUERY_CHANGE=66,e.RESOURCE_TIMING_BUFFER_FULL=67,e.MUT_SHADOW=68,e.DISABLE_STYLESHEET=69,e.FULLSCREEN=70,e.FULLSCREEN_ERROR=71,e.ADOPTED_STYLESHEETS=72,e.CUSTOM_ELEMENT_DEFINED=73,e.MODAL_OPEN=74,e.MODAL_CLOSE=75,e.SLOW_INTERACTION=76,e.LONG_FRAME=77,e.TIMING=78,e.STORAGE_WRITE_FAILURE=79,e.KEEP_ELEMENT=2e3,e.KEEP_URL=2001,e.KEEP_BOUNCE=2002,e.SYS_SETVAR=8193,e.SYS_RESOURCEHASH=8195,e.SYS_SETCONSENT=8196,e.SYS_CUSTOM=8197,e.SYS_REPORTCONSENT=8198}(R||(R={})),function(e){e.Unknown=0,e.Serialization=1}(A||(A={})),function(e){e.Unknown=0,e.DomSnapshot=1,e.NodeEncoding=2,e.LzEncoding=3}(x||(x={})),function(e){e.Internal=0,e.Public=1}(O||(O={}));var j,K,V,z,Y,G,Q,X,J,$,Z,ee,te,ne=["print","alert","confirm"];function re(e){switch(e){case R.MOUSEDOWN:case R.MOUSEMOVE:case R.MOUSEMOVE_CURVE:case R.MOUSEUP:case R.KEYDOWN:case R.KEYUP:case R.TOUCHSTART:case R.TOUCHEND:case R.TOUCHMOVE:case R.TOUCHMOVE_CURVE:case R.TOUCHCANCEL:case R.CLICK:case R.SCROLL_LAYOUT:case R.SCROLL_LAYOUT_CURVE:case R.SCROLL_VISUAL_OFFSET:case R.SCROLL_VISUAL_OFFSET_CURVE:case R.NAVIGATE:return!0;}return!1}!function(e){e.GrantConsent=!0,e.RevokeConsent=!1}(j||(j={})),function(e){e.Page=0,e.Document=1}(K||(K={})),function(e){e.Unknown=0,e.Api=1,e.FsShutdownFrame=2,e.Hibernation=3,e.Reidentify=4,e.SettingsBlocked=5,e.Size=6,e.Unload=7}(V||(V={})),function(e){e.Timing=0,e.Navigation=1,e.Resource=2,e.Paint=3,e.Mark=4,e.Measure=5,e.Memory=6}(z||(z={})),function(e){e.Performance=0,e.PerformanceEntries=1,e.PerformanceMemory=2,e.Console=3,e.Ajax=4,e.PerformanceObserver=5,e.AjaxFetch=6}(Y||(Y={})),function(e){e.Node=1,e.Sheet=2}(G||(G={})),function(e){e.StyleSheetHooks=0,e.SetPropertyHooks=1}(Q||(Q={})),function(e){e.User="user",e.Account="acct",e.Event="evt"}(X||(X={})),function(e){e.Elide=0,e.Record=1,e.Whitelist=2}(J||(J={})),function(e){e.ReasonNoSuchOrg=1,e.ReasonOrgDisabled=2,e.ReasonOrgOverQuota=3,e.ReasonBlockedDomain=4,e.ReasonBlockedIp=5,e.ReasonBlockedUserAgent=6,e.ReasonBlockedGeo=7,e.ReasonBlockedTrafficRamping=8,e.ReasonInvalidURL=9,e.ReasonUserOptOut=10,e.ReasonInvalidRecScript=11,e.ReasonDeletingUser=12,e.ReasonNativeHookFailure=13}($||($={})),function(e){e.Unset=0,e.Exclude=1,e.Mask=2,e.Unmask=3,e.Watch=4,e.Keep=5}(Z||(Z={})),function(e){e.Unset=0,e.Click=1}(ee||(ee={})),function(e){e.MaxLogsPerPage=1024,e.MutationProcessingInterval=250,e.CurveSamplingInterval=142,e.DefaultBundleUploadInterval=5e3,e.HeartbeatInitial=4e3,e.HeartbeatMax=256200,e.PageInactivityTimeout=18e5,e.BackoffMax=3e5,e.ScrollSampleInterval=e.MutationProcessingInterval/5,e.InactivityThreshold=4e3,e.MaxPayloadLength=16384}(te||(te={}));function ie(e,t){return function(){try{return e.apply(this,arguments)}catch(e){try{t&&t(e)}catch(e){}}}}var oe=function(){},se=navigator.userAgent,ae=se.indexOf("MSIE ")>-1||se.indexOf("Trident/")>-1,ue=(ae&&se.indexOf("Trident/5"),ae&&se.indexOf("Trident/6"),ae&&se.indexOf("rv:11")>-1),ce=se.indexOf("Edge/")>-1;se.indexOf("CriOS");var he=/^((?!chrome|android).)*safari/i.test(window.navigator.userAgent);function de(){var e=window.navigator.userAgent.match(/Version\/(\d+)/);return e?parseInt(e[1]):-1}function le(e){if(!he)return!1;var t=de();return t>=0&&t===e}function pe(e){if(!he)return!1;var t=de();return t>=0&&tt)return!1;return n==t}function pt(e,t){var n=0;for(var r in e)if(Object.prototype.hasOwnProperty.call(e,r)&&++n>t)return!0;return!1}Ze="function"==typeof s.objectKeys?function(e){return s.objectKeys(e)}:function(e){var t=[];for(var n in e)s.objectHasOwnProp(e,n)&&t.push(n);return t},st=(ot=function(e){return e.matches||e.msMatchesSelector||e.webkitMatchesSelector})(window.Element.prototype),!(at=window.document?window.document.documentElement:void 0)||st&&at instanceof window.Element||(st=ot(at)),it=($e=[st,function(e,t){return st.call(e,t)}])[0],rt=$e[1];var ft;ut=ae?function(e){var t=e.nextSibling;return t&&e.parentNode&&t===e.parentNode.firstChild?null:t}:function(e){return e.nextSibling};var vt;ft=ae?function(e,t){if(e){var n=e.parentNode?e.parentNode.firstChild:null;do{t(e),e=e.nextSibling}while(e&&e!=n)}}:function(e,t){for(;e;e=e.nextSibling)t(e)};vt=ae?function(e){var t=e.previousSibling;return t&&e.parentNode&&t===e.parentNode.lastChild?null:t}:function(e){return e.previousSibling};function _t(e,t){if(!e)return oe;var n=function(e){try{var t=window;return t.Zone&&t.Zone.root&&"function"==typeof t.Zone.root.wrap?t.Zone.root.wrap(e):e}catch(t){return e}}(e);return t&&(n=n.bind(t)),ie(n,function(e){o("Unexpected error: "+e)})}function gt(e){var t,n=Array.prototype.toJSON,r=String.prototype.toJSON;n&&(Array.prototype.toJSON=void 0),r&&(String.prototype.toJSON=void 0);try{t=s.jsonStringify(e)}catch(e){t=mt(e)}finally{n&&(Array.prototype.toJSON=n),r&&(String.prototype.toJSON=r)}return t}function mt(e){var t="Internal error: unable to determine what JSON error was";try{t=(t=""+e).replace(/[^a-zA-Z0-9\.\:\!\, ]/g,"_")}catch(e){}return"\""+t+"\""}function yt(e){var t=e.doctype;if(!t)return"";var n=""}function wt(e){return s.jsonParse(e)}function bt(e){var t=0,n=0;return null==e.screen?[t,n]:(t=parseInt(String(e.screen.width)),n=parseInt(String(e.screen.height)),[t=isNaN(t)?0:t,n=isNaN(n)?0:n])}var St=function(){function e(e,t){this.target=e,this.propertyName=t,this._before=oe,this._afterSync=oe,this._afterAsync=oe,this.on=!1}return e.prototype.before=function(e){return this._before=_t(e),this},e.prototype.afterSync=function(e){return this._afterSync=_t(e),this},e.prototype.afterAsync=function(e){return this._afterAsync=_t(function(t){s.setWindowTimeout(window,ie(function(){e(t)}),0)}),this},e.prototype.disable=function(){if(this.on=!1,this.shim){var e=this.shim,t=e.override,n=e["native"];this.target[this.propertyName]===t&&(this.target[this.propertyName]=n,this.shim=void 0)}},e.prototype.enable=function(){if(this.on=!0,this.shim)return!0;this.shim=this.makeShim();try{this.target[this.propertyName]=this.shim.override}catch(e){return!1}return!0},e.prototype.makeShim=function(){var e=this,t=this.target[this.propertyName];return{"native":t,override:function(){var n={that:this,args:arguments,result:null};e.on&&e._before(n);var r=t.apply(this,arguments);return e.on&&(n.result=r,e._afterSync(n),e._afterAsync(n)),r}}},e}(),Et={};function Tt(e,t){if(!e||"function"!=typeof e[t])return null;var n;Et[t]=Et[t]||[];for(var r=0;r\n";var n=[];try{for(var r=arguments.callee.caller.caller;r&&n.lengthn)return!1;var r=new Error("Assertion failed: "+t);return Mt.sendToBugsnag(r,"error"),e}function Pt(e,t,n,r){void 0!==n&&("function"==typeof e.addEventListener?e.addEventListener(t,n,r):"function"==typeof e.addListener?e.addListener(n):o("Target of "+t+" doesn't seem to support listeners"))}function qt(e,t,n,r){void 0!==n&&("function"==typeof e.removeEventListener?e.removeEventListener(t,n,r):"function"==typeof e.removeListener?e.removeListener(n):o("Target of "+t+" doesn't seem to support listeners"))}var Ut=function(){function e(){var e=this;this._listeners=[],this._children=[],this._yesCapture=!0,this._noCapture=!1;try{var t=Object.defineProperty({},"passive",{get:function(){e._yesCapture={capture:!0,passive:!0},e._noCapture={capture:!1,passive:!0}}});window.addEventListener("test",oe,t)}catch(e){}}return e.prototype.add=function(e,t,n,r,i){return void 0===i&&(i=!1),this.addCustom(e,t,n,r,i)},e.prototype.addCustom=function(e,t,n,r,i){void 0===i&&(i=!1);var o={target:e,type:t,fn:Mt.wrap(function(e){(i||!1!==e.isTrusted||"message"==t||e._fs_trust_event)&&r(e)}),options:n?this._yesCapture:this._noCapture,index:this._listeners.length};return this._listeners.push(o),Pt(e,t,o.fn,o.options),o},e.prototype.remove=function(e){e.target&&(qt(e.target,e.type,e.fn,e.options),e.target=null,e.fn=void 0)},e.prototype.clear=function(){for(var e=0;e0&&t.height>0)return this.width=t.width,void(this.height=t.height);r=this.computeLayoutViewportSizeFromMediaQueries(e),this.width=r[0],this.height=r[1]}}return e.prototype.computeLayoutViewportSizeFromMediaQueries=function(e){var t=this.findMediaValue(e,"width",this.clientWidth,this.clientWidth+128);void 0===t&&(t=this.tryToGet(e,"innerWidth")),void 0===t&&(t=this.clientWidth);var n=this.findMediaValue(e,"height",this.clientHeight,this.clientHeight+128);return void 0===n&&(n=this.tryToGet(e,"innerHeight")),void 0===n&&(n=this.clientHeight),[t,n]},e.prototype.findMediaValue=function(e,t,n,r){if(s.matchMedia){var i=s.matchMedia(e,"(min-"+t+": "+n+"px)");if(null!=i){if(i.matches&&s.matchMedia(e,"(max-"+t+": "+n+"px)").matches)return n;for(;n<=r;){var o=s.mathFloor((n+r)/2);if(s.matchMedia(e,"(min-"+t+": "+o+"px)").matches){if(s.matchMedia(e,"(max-"+t+": "+o+"px)").matches)return o;n=o+1}else r=o-1}}}},e.prototype.tryToGet=function(e,t){try{return e[t]}catch(e){return}},e}();function Yt(e,t){return new zt(e,t)}var Gt=function(e,t){this.offsetLeft=0,this.offsetTop=0,this.pageLeft=0,this.pageTop=0,this.width=0,this.height=0,this.scale=0;var n=e.document;if(n.body){var r="BackCompat"==n.compatMode;"pageXOffset"in e?(this.pageLeft=e.pageXOffset,this.pageTop=e.pageYOffset):n.scrollingElement?(this.pageLeft=n.scrollingElement.scrollLeft,this.pageTop=n.scrollingElement.scrollTop):r?(this.pageLeft=n.body.scrollLeft,this.pageTop=n.body.scrollTop):n.documentElement&&(n.documentElement.scrollLeft>0||n.documentElement.scrollTop>0)?(this.pageLeft=n.documentElement.scrollLeft,this.pageTop=n.documentElement.scrollTop):(this.pageLeft=n.body.scrollLeft||0,this.pageTop=n.body.scrollTop||0),this.offsetLeft=this.pageLeft-t.pageLeft,this.offsetTop=this.pageTop-t.pageTop;try{var i=e.innerWidth,o=e.innerHeight}catch(e){return}if(0!=i&&0!=o){this.scale=t.width/i,this.scale<1&&(this.scale=1);var s=t.width-t.clientWidth,a=t.height-t.clientHeight;this.width=i-s/this.scale,this.height=o-a/this.scale}}};var Qt,Xt=(Qt=function(e,t){return(Qt=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n])})(e,t)},function(e,t){function n(){this.constructor=e}Qt(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)}),Jt=function(e,t,n,r){return new(n||(n=Promise))(function(i,o){function s(e){try{u(r.next(e))}catch(e){o(e)}}function a(e){try{u(r["throw"](e))}catch(e){o(e)}}function u(e){var t;e.done?i(e.value):(t=e.value,t instanceof n?t:new n(function(e){e(t)})).then(s,a)}u((r=r.apply(e,t||[])).next())})},$t=function(e,t){var n,r,i,o,s={label:0,sent:function(){if(1&i[0])throw i[1];return i[1]},trys:[],ops:[]};return o={next:a(0),"throw":a(1),"return":a(2)},"function"==typeof Symbol&&(o[Symbol.iterator]=function(){return this}),o;function a(o){return function(a){return function(o){if(n)throw new TypeError("Generator is already executing.");for(;s;)try{if(n=1,r&&(i=2&o[0]?r["return"]:o[0]?r["throw"]||((i=r["return"])&&i.call(r),0):r.next)&&!(i=i.call(r,o[1])).done)return i;switch(r=0,i&&(o=[2&o[0],i.value]),o[0]){case 0:case 1:i=o;break;case 4:return s.label++,{value:o[1],done:!1};case 5:s.label++,r=o[1],o=[0];continue;case 7:o=s.ops.pop(),s.trys.pop();continue;default:if(!(i=(i=s.trys).length>0&&i[i.length-1])&&(6===o[0]||2===o[0])){s=0;continue}if(3===o[0]&&(!i||o[1]>i[0]&&o[1]this._due)return et.resolve().then(this._wrappedTick)["catch"](function(){})},e.registry={},e.nextId=0,e.checkedAlready=!1,e.lastCheck=0,e}(),en=function(e){function t(t){var n=e.call(this)||this;return n._interval=t,n._handle=-1,n}return Xt(t,e),t.prototype.start=function(e){var t=this;-1==this._handle&&(this.setTick(function(){e(),t.register(t._interval)}),this._handle=s.setWindowInterval(window,this._wrappedTick,this._interval),this.register(this._interval))},t.prototype.cancel=function(){-1!=this._handle&&(s.clearWindowInterval(window,this._handle),this._handle=-1,this.setTick(function(){}))},t}(Zt),tn=function(e){function t(t,n,r){void 0===n&&(n=0);for(var i=[],o=3;ot&&(this._skew=e-t,this._reportTimeSkew("timekeeper set with future ts"))},e.prototype._reportTimeSkew=function(e){this._reported++<=2&&Mt.sendToBugsnag(e,"error",{skew:this._skew,startTime:this._startTime,wallTime:this.wallTime()})},e}();function on(e){var t=e;return t.tagName?"object"==typeof t.tagName?"form":t.tagName.toLowerCase():null}var sn,an,un=n(3),cn=n(0),hn=Object.defineProperty,dn=p()%1e9,ln=window.WeakMap||((sn=function(){this.name="__st"+(1e9*s.mathRandom()>>>0)+dn++ +"__"}).prototype={set:function(e,t){var n=e[this.name];n&&n[0]===e?n[1]=t:hn(e,this.name,{value:[e,t],writable:!0})},get:function(e){var t;return(t=e[this.name])&&t[0]===e?t[1]:void 0}},sn),pn=1,fn=4,vn=function(){for(var e=0,t=0,n=arguments.length;t0&&o(n[u],this._rules[u]),r[u].length>0&&o(r[u],this._consentRules[u])}},e.prototype._fallback=function(e){for(var t=0,n=e;t0&&t.length<1e4;){var n=t.pop();delete Tn[n.id],n.node._fs==n.id&&(n.node._fs=0),n.id=0,n.next&&t.push(n.next),n.child&&t.push(n.child)}Ft(t.length<1e4,"clearIds is fast")}var qn,Un=function(){function e(e,t){this._onchange=e,this._checkElem=t,this._fallback=!1,this._elems={},this.values={},this.radios={},qn=this}return e.prototype.hookEvents=function(){(function(){var e=Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,"value");if(!e||!e.set)return!1;Nn||(kt(HTMLInputElement,"value",jn),kt(HTMLInputElement,"checked",jn),kt(HTMLSelectElement,"value",jn),kt(HTMLTextAreaElement,"value",jn),kt(HTMLSelectElement,"selectedIndex",jn),kt(HTMLOptionElement,"selected",jn),Nn=!0);return!0})()||(this._fallback=!0)},e.prototype.addInput=function(e){var t=Mn(e);if(this._elems[t]=e,Vn(e)){var n=Hn(e);e.checked&&(this.radios[n]=t)}else this.values[t]=Kn(e);(function(e){switch(e.type){case"checkbox":case"radio":return e.checked!=e.hasAttribute("checked");default:return(e.value||"")!=function(e){if("select"!=on(e))return e.getAttribute("value")||"";var t=e,n=t.querySelector("option[selected]")||t.querySelector("option");if(!n)return"";return n.value||""}(e);}})(e)&&this._onchange(e)},e.prototype.diffValue=function(e,t){var n=Mn(e);if(Vn(e)){var r=Hn(e);return this.radios[r]==n!=("true"==t)}return this.values[n]!=t},e.prototype.onChange=function(e,t){void 0===t&&(t=Kn(e));var n=Mn(e);if((e=this._elems[n])&&this.diffValue(e,t))if(this._onchange(e),Vn(e)){var r=Hn(e);"false"==t&&this.radios[r]==n?delete this.radios[r]:this.radios[r]=n}else this.values[n]=t},e.prototype.tick=function(){for(var e in this._elems){var t=this._elems[e];if(this._checkElem(t)){if(this._fallback){var n=Kn(t);if(t&&this.diffValue(t,n))if(this._onchange(t),Vn(t)){var r=Hn(t);this.radios[r]=+e}else this.values[e]=n}}else delete this._elems[e],delete this.values[e],Vn(t)&&delete this.radios[Hn(t)]}},e.prototype.shutdown=function(){qn=null},e.prototype._usingFallback=function(){return this._fallback},e.prototype._trackingElem=function(e){return!!this._elems[e]},e}(),Nn=!1;var Wn,Dn={};function Bn(){try{if(qn)for(var e in Dn){var t=Dn[e],n=t[0],r=t[1];qn.onChange(n,r)}}finally{Wn=null,Dn={}}}function Hn(e){if(!e)return"";for(var t=e;t&&"form"!=on(t);)t=t.parentElement;return(t&&"form"==on(t)?Mn(t):0)+":"+e.name}function jn(e,t){var n=function e(t,n){if(void 0===n&&(n=2),n<=0)return t;var r=on(t);return"option"!=r&&"optgroup"!=r||!t.parentElement?t:e(t.parentElement,n-1)}(e),r=Mn(n);r&&qn&&qn.diffValue(n,""+t)&&(Dn[r]=[n,""+t],Wn||(Wn=new tn(Bn)).start())}function Kn(e){switch(e.type){case"checkbox":case"radio":return""+e.checked;default:var t=e.value;return t||(t=""),""+t;}}function Vn(e){return e&&"radio"==e.type}var zn={};var Yn="__default";function Gn(e){void 0===e&&(e=Yn);var t=zn[e];return t||(t=function(){var e=document.implementation.createHTMLDocument("");return e.head||e.documentElement.appendChild(e.createElement("head")),e.body||e.documentElement.appendChild(e.createElement("body")),e}(),e!==Yn&&(t.open(),t.write(e),t.close()),zn[e]=t),t}var Qn=new(function(){function e(){var e=Gn(),t=e.getElementById("urlresolver-base");t||((t=e.createElement("base")).id="urlresolver-base",e.head.appendChild(t));var n=e.getElementById("urlresolver-parser");n||((n=e.createElement("a")).id="urlresolver-parser",e.head.appendChild(n)),this.base=t,this.parser=n}return e.prototype.parseUrl=function(e,t){if("undefined"!=typeof URL)try{e||(e=document.baseURI);var n=e?new URL(t,e):new URL(t);if(n.href)return n}catch(e){}return this.parseUrlUsingBaseAndAnchor(e,t)},e.prototype.parseUrlUsingBaseAndAnchor=function(e,t){this.base.setAttribute("href",e),this.parser.setAttribute("href",t);var n=document.createElement("a");return n.href=this.parser.href,n},e.prototype.resolveUrl=function(e,t){return this.parseUrl(e,t).href},e.prototype.resolveToDocument=function(e,t){var n=Jn(e);return null==n?t:this.resolveUrl(n,t)},e}());function Xn(e,t){return Qn.parseUrl(e,t)}function Jn(e){var t=e.document,n=e.location.href;if("string"==typeof t.baseURI)n=t.baseURI;else{var r=t.getElementsByTagName("base")[0];r&&r.href&&(n=r.href)}return"about:blank"==n&&e.parent!=e?Jn(e.parent):n}var $n=new RegExp("[^\\s]"),Zn=new RegExp("[\\s]*$");String.prototype;function er(e){var t=$n.exec(e);if(!t)return e;for(var n=t.index,r=(t=Zn.exec(e))?e.length-t.index:0,i="\uFFFF",o=e.slice(n,e.length-r).split(/\r\n?|\n/g),s=0;sir?(Mt.sendToBugsnag("Ignoring huge text node","warning",{length:r}),""):e.parentNode&&"style"==on(e.parentNode)?n:t.mask?er(n):n}function sr(e){return tr[e]||e.toLowerCase()}function ar(e,t,n,r){var i,o=on(t);if(null===o)return null;var s=function(e){var t,r,s;i=null!==(r=null===(t=nr[e][o])||void 0===t?void 0:t[n])&&void 0!==r?r:null===(s=nr[e]["*"])||void 0===s?void 0:s[n]};if(s("Any"),void 0===i){var a=xn(t);if(!a)return null;a.watchKind==an.Exclude?s("Exclude"):a.mask&&s("Mask")}if(void 0===i)return r;switch(i){case"erase":return null;case"scrubUrl":return ur(r,e,{source:"dom",type:o});case"maskText":return er(r);default:return Object(cn.assertExhaustive)(i);}}function ur(e,t,n){switch(n.source){case"dom":switch(r=n.type){case"frame":case"iframe":return hr(e,t);default:return cr(e,t);}case"event":switch(r=n.type){case R.AJAX_REQUEST:case R.NAVIGATE:return cr(e,t);case R.SET_FRAME_BASE:return hr(e,t);default:return Object(cn.assertExhaustive)(r);}case"log":return hr(e,t);case"page":var r;switch(r=n.type){case"base":return hr(e,t);case"referrer":case"url":return cr(e,t);default:return Object(cn.assertExhaustive)(r);}case"perfEntry":switch(n.type){case"frame":case"iframe":case"navigation":case"other":return hr(e,t);default:return cr(e,t);}default:return Object(cn.assertExhaustive)(n);}}function cr(e,t){return lr(e,t,function(e){if(!(e in pr)){var t=["password","token","^jwt$"];switch("4K3FQ"!==e&&"NQ829"!==e&&"KCF98"!==e&&t.push("^code$"),e){case"2FVM4":t.push("^e$","^eref$","^fn$");break;case"35500":t.push("share_token","password-reset-key");break;case"1HWDJ":t.push("email_id","invite","join");break;case"J82WF":t=[".*"];break;case"8MM83":t=["^creditCard"];break;case"PAN8Z":t.push("code","hash","ol","aeh");break;case"BKP05":t.push("api_key","session_id","encryption_key");break;case"QKM7G":t.push("postcode","encryptedQuoteId","registrationId","productNumber","customerName","agentId","qqQuoteId");break;case"FP60X":t.push("phrase");break;case"GDWG7":t=["^(?!productType|utmSource).*$"];break;case"RV68C":t.push("drivingLicense");break;case"S3VEC":t.push("data");break;case"Q8RZE":t.push("myLowesCardNumber");}pr[e]=new RegExp(t.join("|"),"i")}return pr[e]}(t))}function hr(e,t){return lr(e,t,fr)}function dr(e,t,n,r){var i=new RegExp("(\\/"+t+"\\/).*$","i");n==r&&e.pathname.indexOf(t)>=0&&(e.pathname=e.pathname.replace(i,"$1"+rr))}function lr(e,t,n){var r=Xn("",e);return r.hash&&r.hash.indexOf("access_token")>=0&&(r.hash="#"+rr),dr(r,"visitor",t,"QS8RG"),dr(r,"account",t,"QS8RG"),dr(r,"parentAccount",t,"QS8RG"),dr(r,"reset_password",t,"AGQFM"),dr(r,"reset-password",t,"95NJ7"),dr(r,"dl",t,"RV68C"),dr(r,"retailer",t,"FP60X"),dr(r,"ocadotech",t,"FP60X"),dr(r,"serviceAccounts",t,"FP60X"),dr(r,"signup",t,"7R98D"),r.search&&r.search.length>0&&(r.search=function(e,t){return e.split("?").map(function(e){return function(e,t){return e.replace("?","").split("&").map(function(e){return e.split("=")}).map(function(e){var n=e[0],r=e[1],i=e.slice(2);return t.test(n)&&void 0!==r?[n,rr].concat(i):[n,r].concat(i)}).map(function(e){var t=e[0],n=e[1],r=e.slice(2);return void 0===n?t:[t,n].concat(r).join("=")}).join("&")}(e,t)}).join("?")}(r.search,n)),r.href.substring(0,2048)}var pr={};var fr=new RegExp(".*","i");var vr=/([a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+)/gi,_r=/(?:(http)|(ftp)|(file))[s]?:\/\/(?:[a-zA-Z]|[0-9]|[$-_@.&+#]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+/gi;function gr(e){return"function"==typeof(t=e.constructor)&&Function.prototype.toString.call(t).indexOf("[native code]")>-1;var t}var mr=function(){function e(e,t,n){this._watcher=e,this._resizer=t,this._orgId=n,Tn={},kn=1}return e.prototype.tokenizeNode=function(e,t,n,r,i,o,s){var a=this,u=xn(t),c=xn(n),h=[];return function(e){var t=kn;try{return e(),!0}catch(e){return kn=t,!1}}(function(){a.tokeNode(e,u,c,r,h,i,o,s)})||(h=[]),h},e.prototype.tokeNode=function(e,t,n,r,i,o,s,a){for(var u=[{parentMirror:t,nextMirror:n,node:r}],c=function(){var t=u.pop();if(!t)return"continue";if("string"==typeof t)return i.push(t),"continue";var n=t.parentMirror,r=t.nextMirror,c=t.node,d=h._encodeTagAndAttributes(e,n,r,c,i,o,s);if(null==d||d.watchKind===an.Exclude)return"continue";var l=c.nodeType===pn?c.shadowRoot:null;return(l||c.firstChild)&&a(d)?(u.push("]"),function(e,t){if(!e)return;var n=[];ft(e,function(e){return n.push(e)});for(;n.length>0;){var r=n.pop();r&&t(r)}}(c.firstChild,function(e){u.push({parentMirror:d,nextMirror:null,node:e})}),l&&u.push({parentMirror:d,nextMirror:null,node:l}),void u.push("[")):"continue"},h=this;u.length;)c()},e.prototype._encodeTagAndAttributes=function(e,t,n,r,i,o,s){if("script"==on(r)||8==r.nodeType)return null;var a,u,c,h,d=function(e){return e.constructor===window.ShadowRoot}(r),l=function(e){var t={id:kn++,node:e};return Tn[t.id]=t,e._fs=t.id,t}(r);if((d||(null==t?void 0:t.isInShadowDOM))&&(l.isInShadowDOM=!0),t&&(d?t.shadow=l:(a=t,u=l,c=n,h=this._resizer,Fn(u,h),u.parent=a,u.next=c,c&&(u.prev=c.prev,c.prev=u),null==u.next?(u.prev=a.lastChild,a.lastChild=u):u.next.prev=u,null==u.prev?a.child=u:u.prev.next=u)),10==r.nodeType){var p=r;return i.push("1?s.push([o.idx,a]):s.push(o.idx):s.push(i)}for(n=1;n1e3)return null;if(!n||n.nodeType!=pn)return null;var r=n;if(getComputedStyle(r).display.indexOf("inline")<0)return r;n=n.parentNode}},t}(Er),kr=function(e){function t(){return null!==e&&e.apply(this,arguments)||this}return br(t,e),t.prototype.observe=function(e){var t=this;if(e&&e.nodeType==pn){var n=e;this.growWatchedIndex(xn(e)),this._ctx.measurer.requestMeasureTask(function(){t.addEntry(n)})}},t.prototype.unobserveSubtree=function(e){var t=xn(e);t&&this.clearWatchedIndex(t)},t.prototype.nodeChanged=function(e){var t=this,n=this.relatedWatched(e);this._ctx.measurer.requestMeasureTask(function(){for(var e=0,r=n;e0){var r=zr(t[n-1],e);if(r)return void(t[n-1]=r)}else!function(e){qr.push(e),Pr||(Pr=!0,Mr(Ur))}(this.observer);t[n]=e},addListeners:function(){this.addListeners_(this.target)},addListeners_:function(e){var t=this.options;t.attributes&&e.addEventListener("DOMAttrModified",this.handleEventBound,!0),t.characterData&&e.addEventListener("DOMCharacterDataModified",this.handleEventBound,!0),t.childList&&e.addEventListener("DOMNodeInserted",this.handleEventBound,!0),(t.childList||t.subtree)&&e.addEventListener("DOMNodeRemoved",this.handleEventBound,!0)},removeListeners:function(){this.removeListeners_(this.target)},removeListeners_:function(e){var t=this.options;t.attributes&&e.removeEventListener("DOMAttrModified",this.handleEventBound,!0),t.characterData&&e.removeEventListener("DOMCharacterDataModified",this.handleEventBound,!0),t.childList&&e.removeEventListener("DOMNodeInserted",this.handleEventBound,!0),(t.childList||t.subtree)&&e.removeEventListener("DOMNodeRemoved",this.handleEventBound,!0)},addTransientObserver:function(e){if(e!==this.target){this.addListeners_(e),this.transientObservedNodes.push(e);var t=Or.get(e);t||Or.set(e,t=[]),t.push(this)}},removeTransientObservers:function(){var e=this.transientObservedNodes;this.transientObservedNodes=[],e.forEach(function(e){this.removeListeners_(e);for(var t=Or.get(e),n=0;n0||this._toRefresh.length>0){var n={},r={};for(var i in this.processRecords(e,t,r,n),r){var o=i.split("\t");t.push({Kind:R.MUT_ATTR,When:e,Args:[parseInt(o[0]),o[1],r[i]]})}for(var i in n)t.push({Kind:R.MUT_TEXT,When:e,Args:[parseInt(i),n[i]]})}var s=this._newShadowContainers;this._newShadowContainers=[];for(var a=0;a0)for(var d=0;d0){s[h]=c.target;var p=Jr(c.target);p&&(s[p.id]=p.node)}break;case"characterData":Cn(c.target)||c.oldValue!=c.target.textContent&&(r[h]=or(c.target));break;case"attributes":var f=An(w=c.target);if(_n(this._watcher.isWatched(w))>_n(f)){a(w);break}var v=Xr(c.attributeNamespace)+(c.attributeName||""),_=sr(v);if(w.hasAttribute(v)){var g=c.target.getAttribute(v);c.oldValue!=g&&(g=ar(this._ctx.options.orgId,c.target,_,g||""),this._attrVisitor(c.target,_,g||""),null!==g&&(n[h+"\t"+_]=g))}else n[h+"\t"+_]=null;}}catch(e){}for(var m=0,y=this._toRefresh;m0&&n.push({When:t,Kind:R.MUT_SHADOW,Args:[o,this._compress?this._lz.encode(s):s]})},e.prototype.genInsert=function(e,t,n,r,i,o){var s=Mn(r)||-1,a=Mn(o)||-1,u=-1===s&&-1===a,c=p(),h=this.genDocStream(e,r,i,o),d=p()-c;if(h.length>0){var l=p(),f=this._compress?this._lz.encode(h):h,v=p()-l;n.push({When:t,Kind:R.MUT_INSERT,Args:[s,a,f]},{When:t,Kind:R.TIMING,Args:[[O.Internal,A.Serialization,u?x.DomSnapshot:x.NodeEncoding,t,d,[x.LzEncoding,v]]]})}},e.prototype.genDocStream=function(e,t,n,r){var i=this;if(t&&Cn(t))return[];for(var o=[],s=this._encoder.tokenizeNode(e,t,r,n,function(e){if(e.nodeType==pn){var t=e;t.shadowRoot&&i.observe(t.shadowRoot)}i._nodeVisitor(e,o)},this._attrVisitor,this._visitChildren),a=0,u=o;a0){var i=t[t.length-1];if(i.Kind==R.MUT_REMOVE)return void i.Args.push(r)}t.push({When:e,Kind:R.MUT_REMOVE,Args:[r]})},e.prototype.setUpIEWorkarounds=function(){var t=this;if(ue){var n=Object.getOwnPropertyDescriptor(Node.prototype,"textContent"),r=n&&n.set;if(!n||!r)throw new Error("Missing textContent setter -- not safe to record mutations.");Object.defineProperty(Element.prototype,"textContent",Gr(Gr({},n),{set:function(e){try{for(var t=void 0;t=this.firstChild;)this.removeChild(t);if(null===e||""==e)return;var n=(this.ownerDocument||document).createTextNode(e);this.appendChild(n)}catch(t){r&&r.call(this,e)}}}))}this._setPropertyThrottle=new nn(e.ThrottleMax,e.ThrottleInterval,function(){return new tn(function(){t._setPropertyWasThrottled=!0,t.tearDownIEWorkarounds()}).start()});var i=this._setPropertyThrottle.guard(function(e){e.cssText=e.cssText});this._setPropertyThrottle.open(),this._setPropertyHook=Tt(CSSStyleDeclaration.prototype,"setProperty"),this._setPropertyHook&&this._setPropertyHook.afterSync(function(e){i(e.that)}),this._removePropertyHook=Tt(CSSStyleDeclaration.prototype,"removeProperty"),this._removePropertyHook&&this._removePropertyHook.afterSync(function(e){i(e.that)})},e.prototype.tearDownIEWorkarounds=function(){this._setPropertyThrottle&&this._setPropertyThrottle.close(),this._setPropertyHook&&this._setPropertyHook.disable(),this._removePropertyHook&&this._removePropertyHook.disable()},e.prototype.updateConsent=function(){var e=this,t=xn(this._root);t&&function(e,t){for(var n=[e];n.length;){var r=n.pop();if(r){t(r);for(var i=r.child,o=r.shadow;i;)n.push(i),i=i.next;o&&n.push(o)}}}(t,function(t){var n=t.node;t.matchesAnyConsentRule&&e.refreshElement(n)})},e.prototype.refreshElement=function(e){Mn(e)&&this._toRefresh.push(e)},e.ThrottleMax=1024,e.ThrottleInterval=1e4,e}();function Xr(e){return void 0===e&&(e=""),null===e?"":{"http://www.w3.org/1999/xlink":"xlink:","http://www.w3.org/XML/1998/namespace":"xml:","http://www.w3.org/2000/xmlns/":"xmlns:"}[e]||""}function Jr(e){return!(null==e?void 0:e.shadowRoot)||gr(e.shadowRoot)?null:xn(e.shadowRoot)}var $r=["navigationStart","unloadEventStart","unloadEventEnd","redirectStart","redirectEnd","fetchStart","domainLookupStart","domainLookupEnd","connectStart","connectEnd","secureConnectionStart","requestStart","responseStart","responseEnd","domLoading","domInteractive","domContentLoadedEventStart","domContentLoadedEventEnd","domComplete","loadEventStart","loadEventEnd"],Zr=["name","startTime","duration","initiatorType","redirectStart","redirectEnd","fetchStart","domainLookupStart","domainLookupEnd","connectStart","connectEnd","secureConnectionStart","requestStart","responseStart","responseEnd","unloadEventStart","unloadEventEnd","domInteractive","domContentLoadedEventStart","domContentLoadedEventEnd","domComplete","loadEventStart","loadEventEnd","type","redirectCount","decodedBodySize","encodedBodySize","transferSize"],ei=["name","startTime","duration","initiatorType","redirectStart","redirectEnd","fetchStart","domainLookupStart","domainLookupEnd","connectStart","connectEnd","secureConnectionStart","requestStart","responseStart","responseEnd","decodedBodySize","encodedBodySize","transferSize"],ti=["name","startTime","duration"],ni=["jsHeapSizeLimit","totalJSHeapSize","usedJSHeapSize"],ri=function(){function e(e,t,n){this._ctx=e,this._queue=t,this._perfSupported=!1,this._timingSupported=!1,this._getEntriesSupported=!1,this._memorySupported=!1,this._lastUsedJSHeapSize=0,this._gotLoad=!1,this._observer=null,this._observedBatches=[];var r=window.performance;r&&(this._perfSupported=!0,r.timing&&(this._timingSupported=!0),r.memory&&(this._memorySupported=!0),"function"==typeof r.getEntries&&(this._getEntriesSupported=!0),this._listeners=n.createChild())}return e.prototype.start=function(e){var t=this;this._resourceUploader=e;var n=window.performance;n&&(this._ctx.recording.inFrame||this._queue.enqueue({Kind:R.REC_FEAT_SUPPORTED,Args:[Y.Performance,this._timingSupported,Y.PerformanceEntries,this._getEntriesSupported,Y.PerformanceMemory,this._memorySupported,Y.PerformanceObserver,!!window.PerformanceObserver]}),this.observe(),!this._observer&&n.addEventListener&&n.removeEventListener&&this._listeners.add(n,"resourcetimingbufferfull",!0,function(){t._queue.enqueue({Kind:R.RESOURCE_TIMING_BUFFER_FULL,Args:[]})}),this.checkMemory())},e.prototype.onLoad=function(){this._gotLoad||(this._gotLoad=!0,this._timingSupported&&(this.recordTiming(performance.timing),this.checkForNewEntries()))},e.prototype.tick=function(e){this.checkMemory(),e&&this.checkForNewEntries()},e.prototype.shutdown=function(){this._listeners&&this._listeners.clear(),this._resourceUploader=void 0;var e=[];this._observer?(this._observer.takeRecords&&(e=this._observer.takeRecords()),this._observer.disconnect()):window.performance&&window.performance.getEntries&&(e=window.performance.getEntries()),e.length>300&&(e=e.slice(0,300),this._queue.enqueue({Kind:R.RESOURCE_TIMING_BUFFER_FULL,Args:[]})),this._observedBatches.push(e),this.tick(!0)},e.prototype.observe=function(){var e=this;if(!this._observer&&this._getEntriesSupported&&window.PerformanceObserver){this._observedBatches.push(performance.getEntries()),this._observer=new window.PerformanceObserver(function(t){var n=t.getEntries();e._observedBatches.push(n)});var t=["navigation","resource","measure","mark"];window.PerformancePaintTiming&&t.push("paint"),this._observer.observe({entryTypes:t})}},e.prototype.checkMemory=function(){if(this._memorySupported&&!this._ctx.recording.inFrame){var e=performance.memory;if(e){var t=e.usedJSHeapSize-this._lastUsedJSHeapSize;(0==this._lastUsedJSHeapSize||s.mathAbs(t/this._lastUsedJSHeapSize)>.2)&&(this.addPerfEvent(z.Memory,e,ni),this._lastUsedJSHeapSize=e.usedJSHeapSize)}}},e.prototype.recordEntry=function(e){switch(e.entryType){case"navigation":this.recordNavigation(e);break;case"resource":this.recordResource(e);break;case"paint":this.recordPaint(e);break;case"measure":this.recordMeasure(e);break;case"mark":this.recordMark(e);}},e.prototype.checkForNewEntries=function(){if(this._perfSupported&&this._getEntriesSupported){var e=this._observedBatches;this._observedBatches=[];for(var t=0,n=e;t=t&&(n=void 0),o[o.length-1]--,n&&n!==si&&r&&(o.push(s.objectKeys(n).length),a.push(u));o[o.length-1]<=0;)o.pop(),a.pop();return n})}catch(e){}return"[error serializing "+e.constructor.name+"]"}}var ui=function(){function e(e){this._requestTracker=e}return e.prototype.disable=function(){this._hook&&(this._hook.disable(),this._hook=null)},e.prototype.enable=function(e){var t,n=this,r=I(e),i=null===(t=null==r?void 0:r._w)||void 0===t?void 0:t.fetch;(i||e.fetch)&&(this._hook=Tt(i?r._w:e,"fetch"),this._hook&&this._hook.afterSync(function(e){return n.recordFetch(e.that,e.result,e.args[0],e.args[1])}))},e.prototype.recordFetch=function(e,t,n,r){var i,o="GET",s="",a={};if("string"==typeof n?s=n:"url"in n?(s=n.url,o=n.method,i=n.body,n.headers&&n.headers.forEach(function(e,t){a[e]=t})):s=""+n,r){o=r.method||o;var u=r.headers;if(u)if(nt(u))for(var c=0,h=u;c-1;n&&o?t.clone().text().then(Mt.wrap(function(i){var o=mi(i,n),s=o[0],a=o[1];r.onComplete(e,t,s,a)}))["catch"](Mt.wrap(function(n){r.onComplete(e,t,-1,void 0)})):r.onComplete(e,t,-1,void 0)}))["catch"](Mt.wrap(function(t){r.onComplete(e,t,-1,void 0)}))},e.prototype.onComplete=function(e,t,n,r){var i=this,o=-1,s="";if("headers"in t){o=t.status;s=this.serializeFetchHeaders(t.headers,function(e){return i._requestTracker.isHeaderInResponseHeaderWhitelist(e[0])})}return this._requestTracker.onComplete(e,s,o,n,r)},e.prototype.serializeFetchHeaders=function(e,t){var n="";return e.forEach(function(e,r){r=r.toLowerCase();var i=t([r,e]);n+=r+(i?": "+e:"")+di}),n},e}(),ci=function(){function e(e){this._requestTracker=e}return e.prototype.disable=function(){this._xhrOpenHook&&(this._xhrOpenHook.disable(),this._xhrOpenHook=null),this._xhrSetHeaderHook&&(this._xhrSetHeaderHook.disable(),this._xhrSetHeaderHook=null)},e.prototype.enable=function(e){var t,n=this,r=I(e),i=(null===(t=null==r?void 0:r._w)||void 0===t?void 0:t.XMLHttpRequest)||e.XMLHttpRequest;if(i){var o=i.prototype;this._xhrOpenHook=Tt(o,"open"),this._xhrOpenHook&&this._xhrOpenHook.before(function(e){var t=e.args[0],r=e.args[1];n._requestTracker.addPendingReq(e.that,t,r),e.that.addEventListener("load",Mt.wrap(function(t){n.onComplete(e.that)})),e.that.addEventListener("error",Mt.wrap(function(t){n.onComplete(e.that)}))}),this._xhrSendHook=Tt(o,"send"),this._xhrSendHook&&this._xhrSendHook.before(function(e){var t=e.args[0];n._requestTracker.addRequestBody(e.that,t)}),this._xhrSetHeaderHook=Tt(o,"setRequestHeader"),this._xhrSetHeaderHook&&this._xhrSetHeaderHook.before(function(e){var t=e.args[0],r=e.args[1];n._requestTracker.addHeader(e.that,t,r)})}},e.prototype.onComplete=function(e){var t=this,n=this.responseBody(e),r=n[0],i=n[1],o=Ei(function(e){var t=[];return e.split(di).forEach(function(e){var n=e.indexOf(":");-1!=n?t.push([e.slice(0,n).trim(),e.slice(n+1,e.length).trim()]):t.push([e.trim(),null])}),t}(e.getAllResponseHeaders()),function(e){return t._requestTracker.isHeaderInResponseHeaderWhitelist(e[0])});return this._requestTracker.onComplete(e,o,e.status,r,i)},e.prototype.responseBody=function(e){var t=this._requestTracker.pendingReq(e);if(!t)return[-1,void 0];var n=this._requestTracker.getRspWhitelist(t.url);if(e.responseType){var r=e.response;switch(r||o("Maybe response type was different that expected."),e.responseType){case"text":return mi(e.responseText,n);case"json":return function(e,t){if(!e)return[-1,void 0];return[yi(e),ai(e,te.MaxPayloadLength,t)]}(r,n);case"arraybuffer":return function(e,t){return[e?e.byteLength:-1,t?"[ArrayBuffer]":void 0]}(r,n);case"blob":return function(e,t){return[e?e.size:-1,t?"[Blob]":void 0]}(r,n);case"document":return function(e,t){return[-1,t?"[Document]":void 0]}(0,n);}}return mi(e.responseText,n)},e}();var hi,di="\r\n",li=["a-im","accept","accept-charset","accept-encoding","accept-language","accept-datetime","access-control-request-method,","access-control-request-headers","cache-control","connection","content-length","content-md5","content-type","date","expect","forwarded","from","host","if-match","if-modified-since","if-none-match","if-range","if-unmodified-since","max-forwards","origin","pragma","range","referer","te","user-agent","upgrade","via","warning"],pi=["access-control-allow-origin","access-control-allow-credentials","access-control-expose-headers","access-control-max-age","access-control-allow-methods","access-control-allow-headers","accept-patch","accept-ranges","age","allow","alt-svc","cache-control","connection","content-disposition","content-encoding","content-language","content-length","content-location","content-md5","content-range","content-type","date","delta-base","etag","expires","im","last-modified","link","location","permanent","p3p","pragma","proxy-authenticate","public-key-pins","retry-after","permanent","server","status","strict-transport-security","trailer","transfer-encoding","tk","upgrade","vary","via","warning","www-authenticate","x-frame-options"],fi={BM7A6:["x-b3-traceid"],KD87S:["transactionid"],NHYJM:["x-att-conversationid"],GBNRN:["x-trace-id"],R16RC:["x-request-id"],DE9CX:["x-client","x-client-id","ot-baggage-original-client","x-req-id","x-datadog-trace-id","x-datadog-parent-id","x-datadog-sampling-priority"]},vi={"thefullstory.com":["x-cloud-trace-context"],TN1:["x-cloud-trace-context"],KD87S:["transactionid"],PPE96:["x-b3-traceid"],HWT6H:["x-b3-traceid"],PPEY7:["x-b3-traceid"],PPK3W:["x-b3-traceid"],NHYJM:["x-att-conversationid"],GBNRN:["x-trace-id"],NK5T9:["traceid","requestid"]},_i=function(){function e(e,t){this._ctx=e,this._queue=t,this._enabled=!1,this._tracker=new gi(e,t),this._xhr=new ci(this._tracker),this._fetch=new ui(this._tracker)}return e.prototype.isEnabled=function(){return this._enabled},e.prototype.enable=function(e){this._enabled||(this._enabled=!0,this._queue.enqueue({Kind:R.REC_FEAT_SUPPORTED,Args:[Y.Ajax,!0,Y.AjaxFetch,!!e]}),this._xhr.enable(this._ctx.window),e&&this._fetch.enable(this._ctx.window))},e.prototype.disable=function(){this._enabled&&(this._enabled=!1,this._xhr.disable(),this._fetch.disable())},e.prototype.tick=function(e){this._tracker.tick(e)},e.prototype.setWatches=function(e){this._tracker.setWatches(e)},e}(),gi=function(){function e(e,t){this._ctx=e,this._queue=t,this._reqHeaderWhitelist={},this._rspHeaderWhitelist={},this._pendingReqs={},this._events=[],this._curId=1,this.addHeaderWhitelist(li,pi),this.addHeaderWhitelist(fi[e.options.orgId],vi[e.options.orgId])}return e.prototype.getReqWhitelist=function(e){var t=this.findWhitelistIndexFor(e);return t>=0&&this._reqWhitelist[t]},e.prototype.getRspWhitelist=function(e){var t=this.findWhitelistIndexFor(e);return t>=0&&this._rspWhitelist[t]},e.prototype.isHeaderInRequestHeaderWhitelist=function(e){return e in this._reqHeaderWhitelist},e.prototype.isHeaderInResponseHeaderWhitelist=function(e){return e in this._rspHeaderWhitelist},e.prototype.pushEvent=function(e){this._events.push(e)},e.prototype.setWatches=function(e){var t=this,n=[];this._reqWhitelist=[],this._rspWhitelist=[],e.forEach(function(e){n.push(e.URLRegex),t._reqWhitelist.push(Si(e.RecordReq,e.ReqWhitelist)),t._rspWhitelist.push(Si(e.RecordRsp,e.RspWhitelist))}),this._reqBodyRegex=new RegExp("("+n.join(")|(")+")")},e.prototype.addHeaderWhitelist=function(e,t){var n=this;e&&e.forEach(function(e){return n._reqHeaderWhitelist[e]=!0}),t&&t.forEach(function(e){return n._rspHeaderWhitelist[e]=!0})},e.prototype.tick=function(e){if(e){for(var t=0;te.MaxRuleBytes&&(o("CSSRule too large, inserting dummy instead: "+n.length),n="dummy {}"),this.withEventQueueForSheet(t,function(e){return e.enqueue({Kind:R.CSSRULE_INSERT,Args:"number"==typeof r?[i,[n],r]:[i,[n]]})}))},e.prototype.addDelete=function(e,t){var n=Fi(e,G.Node);n&&this.withEventQueueForSheet(e,function(e){return e.enqueue({Kind:R.CSSRULE_DELETE,Args:[n,t]})})},e.prototype.onDisableSheet=function(e,t){var n=Fi(e,G.Node);n&&this.withEventQueueForSheet(e,function(e){return e.enqueue({Kind:R.DISABLE_STYLESHEET,Args:[n,!!t]})})},e.prototype.withEventQueueForSheet=function(e,t){if(e.ownerNode)return n=this.ctx,r=e.ownerNode,i=t,void((o=I(Ai(r)||n.window))&&"function"==typeof o._withEventQueue&&o._withEventQueue(n.recording.pageSignature(),function(e){i({enqueue:function(t){Ft(null!=e,Ri)&&e.enqueue(t)},enqueueFirst:function(t){Ft(null!=e,Ri)&&e.enqueueFirst(t)}}),e=null}));var n,r,i,o;t(this.queue)},e.prototype.stop=function(){this.throttle.close();for(var e=0,t=this.hooks;e0&&i[i.length-1])&&(6===o[0]||2===o[0])){s=0;continue}if(3===o[0]&&(!i||o[1]>i[0]&&o[1]-1)return[n];if("srcset"==t&&("img"==i||"source"==i)){return null!=n.match(/^\s*$/)?[]:n.split(",").map(function(e){return e.trim().split(/\s+/)[0]})}var o=e;if("style"==t&&o.style){var s=o.style.backgroundImage;if(!s)return[];if(s.length>300)return[];var a=[],u=void 0;for(jt.lastIndex=0;u=jt.exec(s);){var c=u[1];c&&a.push(c.trim())}return a}return[]}(e,t,n);r5e5)){var n=ki(Ti(e));if(n){if(n.length>0&&Li.test(t))return 0;var r,i=Gn();ae?(r=i.createElement("style")).textContent=e.textContent:r=i.importNode(e,!0),i.head.appendChild(r);var o=ki(Ti(r));if(i.head.removeChild(r),o)return n.length>o.length?o.length:void 0}}}(o);void 0!==s&&t.push(function(){n._styleSheetWatcher.snapshotEl(o,s)});break;default:"#"!==e.nodeName[0]&&e.nodeName.indexOf("-")>-1&&this._customElementWatcher.onCustomNodeVisited(e);}if("scrollLeft"in e&&"scrollTop"in e){var a=e;this._ctx.measurer.requestMeasureTask(function(){0==a.scrollLeft&&0==a.scrollTop||n.addScroll(a)})}},e.prototype.isSafePointerEvent=function(e){var t=Ki(e);return!!Mn(t)&&!Cn(t)},e.prototype.addMouseMove=function(e){var t=Mn(Ki(e));this._queue.enqueue({Kind:R.MOUSEMOVE,Args:t?[e.clientX,e.clientY,t]:[e.clientX,e.clientY]})},e.prototype.addMouseDown=function(e){this._queue.enqueue({Kind:R.MOUSEDOWN,Args:[e.clientX,e.clientY]})},e.prototype.addMouseUp=function(e){this._queue.enqueue({Kind:R.MOUSEUP,Args:[e.clientX,e.clientY]})},e.prototype.addTouchEvent=function(e,t){if(void 0!==e.changedTouches)for(var n=0;n0)return n[0]}}return e.target}var Vi=/^\s*at .*(\S+\:\d+|native|())/m,zi=/^(eval@)?(\[native code\])?$/;function Yi(e){if(!e||"string"!=typeof e.stack)return[];var t=e;return t.stack.match(Vi)?t.stack.split("\n").filter(function(e){return!!e.match(Vi)}).map(function(e){e.indexOf("(eval ")>-1&&(e=e.replace(/eval code/g,"eval").replace(/(\(eval at [^\()]*)|(\)\,.*$)/g,""));var t=e.replace(/^\s+/,"").replace(/\(eval code/g,"(").replace(/\(native code\)/,"").split(/\s+/).slice(1),n=Qi(t.pop());return Gi(t.join(" "),["eval",""].indexOf(n[0])>-1?"":n[0],n[1],n[2])}):function(e){return e.split("\n").filter(function(e){return!e.match(zi)}).map(function(e){if(e.indexOf(" > eval")>-1&&(e=e.replace(/ line (\d+)(?: > eval line \d+)* > eval\:\d+\:\d+/g,":$1")),-1===e.indexOf("@")&&-1===e.indexOf(":"))return[e,"",-1,-1];var t=e.split("@"),n=Qi(t.pop());return Gi(t.join("@"),n[0],n[1],n[2])})}(t.stack)}function Gi(e,t,n,r){return[e||"",t||"",parseInt(n||"-1"),parseInt(r||"-1")]}function Qi(e){if(!e||-1===e.indexOf(":"))return["","",""];var t=/(.+?)(?:\:(\d+))?(?:\:(\d+))?$/.exec(e.replace(/[\(\)]/g,""));return t?[t[1]||"",t[2]||"",t[3]||""]:["","",""]}var Xi=function(){for(var e=0,t=0,n=arguments.length;t0&&"string"==typeof e.nodeName}(t)?function(e){return e.toString()}(t):void 0===t?"undefined":"object"!=typeof t||null==t?t:t instanceof Error?t.stack||t.name+": "+t.message:void 0;if(void 0!==o)return void 0===(o=s.jsonStringify(o))?0:("\""==o[0]&&(o=to(o,n,"...\"")),o.length<=n?(i.tokens.push(o),o.length):0);if(i.cyclic){i.opath.splice(r);var a=i.opath.lastIndexOf(t);if(a>-1){var u="";return u="\""+to(u,n-2)+"\"",i.tokens.push(u),u.length}i.opath.push(t)}var c=n,h=function(e){return c>=e.length&&(c-=e.length,i.tokens.push(e),!0)},d=function(e){","==i.tokens[i.tokens.length-1]?i.tokens[i.tokens.length-1]=e:h(e)};if(c<2)return 0;if(nt(t)){h("[");for(var l=0;l0;l++){var p=e(t[l],c-1,r+1,i);if(c-=p,0==p&&!h("null"))break;h(",")}d("]")}else{h("{");var f=Ze(t);for(l=0;l0;l++){var v=f[l],_=t[v];if(!h("\""+v+"\":"))break;if(0==(p=e(_,c-1,r+1,i))){i.tokens.pop();break}c-=p,h(",")}d("}")}return n==1/0?1:n-c}(e,t,0,i);var o=i.tokens.join("");return r?function(e,t){var n=t.replace(vr,"");return n=n.replace(_r,function(t){return ur(t,e,{source:"log",type:"debug"})})}(n,o):o}catch(e){return mt(e)}}function Zi(e,t){var n=0;try{s.jsonStringify(e,function(e,r){if(n++>t)throw"break";if("object"==typeof r)return r})}catch(e){return"break"!=e}return!1}var eo=function(e){return isNaN(e)?"Invalid Date":e.toUTCString()},to=function(e,t,n){return void 0===n&&(n="..."),e.length<=t?e:e.length<=n.length||t<=n.length?e.substring(0,t):e.substring(0,t-n.length)+n};var no=function(){for(var e=0,t=0,n=arguments.length;tthis._curveEndMs&&(this._curveEndMs=e.When),this._evts.push(e)},e.prototype.finish=function(e,t){void 0===t&&(t=[]);var n=this._evts.length;if(n<=1)return!1;for(var r=no([this._curveEndMs],t),i=this._evts[0].When,o=this._evts[n-1].When,s=0;s0?this._lastWhen:this._ctx.time.now();this.enqueueAt(t,e),Zt.checkForBrokenSchedulers()},e.prototype.enqueueAt=function(e,t){if(!this._recordingDisabled){e0){var t=e;t.When=this._eventQueue[0].When,this._eventQueue.unshift(t)}else this.enqueue(e)},e.prototype.addUnload=function(e){this._gotUnload||(this._gotUnload=!0,this.enqueue({Kind:R.UNLOAD,Args:[e]}),this.singSwanSong())},e.prototype.shutdown=function(e){this._flush(),this.addUnload(e),this._flush(),this._recordingDisabled=!0,this.stopPipeline()},e.prototype._flush=function(){this.processEvents(),this._transport.flush()},e.prototype.singSwanSong=function(){this._recordingDisabled||(this.processEvents(),this._transport.singSwanSong())},e.prototype.rebaseIframe=function(e){for(var t=0,n=this._eventQueue.length;t0){var d=h[h.length-1].Args[2];if(d)h[0].Args[9]=d}}for(var l in o){o[p=parseInt(l)].finish(R.SCROLL_LAYOUT_CURVE,[p])}for(var l in s){s[p=parseInt(l)].finish(R.SCROLL_VISUAL_OFFSET_CURVE,[p])}for(var l in i){var p;i[p=parseInt(l)].finish(R.TOUCHMOVE_CURVE,[p])}return t&&t.finish(R.RESIZE_VISUAL_CURVE),n}(t);e||(n=n.concat(this._gatherExternalEvents(0!=n.length))),this.ensureFrameIds(n),0!=n.length&&this._transport.enqueueEvents(this._ctx.recording.pageSignature(),n)}},e.prototype.ensureFrameIds=function(e){if(this._frameId)for(var t=this._parentIds,n=t&&t.length>0,r=0;r>>0).toString(16)).slice(-8);return e},e}();function uo(e){var t=new ao(1);return t.writeAscii(e),t.sumAsHex()}function co(e){var t=new Uint8Array(e);return lo(String.fromCharCode.apply(null,t))}var ho="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";function lo(e){var t;return(null!==(t=window.btoa)&&void 0!==t?t:po)(e).replace(/\+/g,"-").replace(/\//g,"_")}function po(e){for(var t=String(e),n=[],r=0,i=0,o=0,s=ho;t.charAt(0|o)||(s="=",o%1);n.push(s.charAt(63&r>>8-o%1*8))){if((i=t.charCodeAt(o+=.75))>255)throw new Error("'btoa' failed: The string to be encoded contains characters outside of the Latin1 range.");r=r<<8|i}return n.join("")}var fo=function(e,t,n,r){return new(n||(n=Promise))(function(i,o){function s(e){try{u(r.next(e))}catch(e){o(e)}}function a(e){try{u(r["throw"](e))}catch(e){o(e)}}function u(e){var t;e.done?i(e.value):(t=e.value,t instanceof n?t:new n(function(e){e(t)})).then(s,a)}u((r=r.apply(e,t||[])).next())})},vo=function(e,t){var n,r,i,o,s={label:0,sent:function(){if(1&i[0])throw i[1];return i[1]},trys:[],ops:[]};return o={next:a(0),"throw":a(1),"return":a(2)},"function"==typeof Symbol&&(o[Symbol.iterator]=function(){return this}),o;function a(o){return function(a){return function(o){if(n)throw new TypeError("Generator is already executing.");for(;s;)try{if(n=1,r&&(i=2&o[0]?r["return"]:o[0]?r["throw"]||((i=r["return"])&&i.call(r),0):r.next)&&!(i=i.call(r,o[1])).done)return i;switch(r=0,i&&(o=[2&o[0],i.value]),o[0]){case 0:case 1:i=o;break;case 4:return s.label++,{value:o[1],done:!1};case 5:s.label++,r=o[1],o=[0];continue;case 7:o=s.ops.pop(),s.trys.pop();continue;default:if(!(i=(i=s.trys).length>0&&i[i.length-1])&&(6===o[0]||2===o[0])){s=0;continue}if(3===o[0]&&(!i||o[1]>i[0]&&o[1]go?[4,t(mo)]:[3,3]:[3,5];case 2:u.sent(),i=e.now(),u.label=3;case 3:a=new Uint8Array(n,s,Math.min(o-s,_o)),r.write(a),u.label=4;case 4:return s+=_o,[3,1];case 5:return[2,{hash:r.sum(),hasher:r}];}})})}var wo=function(e,t,n,r){return new(n||(n=Promise))(function(i,o){function s(e){try{u(r.next(e))}catch(e){o(e)}}function a(e){try{u(r["throw"](e))}catch(e){o(e)}}function u(e){var t;e.done?i(e.value):(t=e.value,t instanceof n?t:new n(function(e){e(t)})).then(s,a)}u((r=r.apply(e,t||[])).next())})},bo=function(e,t){var n,r,i,o,s={label:0,sent:function(){if(1&i[0])throw i[1];return i[1]},trys:[],ops:[]};return o={next:a(0),"throw":a(1),"return":a(2)},"function"==typeof Symbol&&(o[Symbol.iterator]=function(){return this}),o;function a(o){return function(a){return function(o){if(n)throw new TypeError("Generator is already executing.");for(;s;)try{if(n=1,r&&(i=2&o[0]?r["return"]:o[0]?r["throw"]||((i=r["return"])&&i.call(r),0):r.next)&&!(i=i.call(r,o[1])).done)return i;switch(r=0,i&&(o=[2&o[0],i.value]),o[0]){case 0:case 1:i=o;break;case 4:return s.label++,{value:o[1],done:!1};case 5:s.label++,r=o[1],o=[0];continue;case 7:o=s.ops.pop(),s.trys.pop();continue;default:if(!(i=(i=s.trys).length>0&&i[i.length-1])&&(6===o[0]||2===o[0])){s=0;continue}if(3===o[0]&&(!i||o[1]>i[0]&&o[1]So){var i=ur(e,t,{source:"log",type:"bugsnag"});return Mt.sendToBugsnag("Size of blob resource exceeds limit","warning",{url:i,MaxResourceSizeBytes:So}),void r(null)}(function(e){var t=oo(),n=t.resolve,r=t.promise,i=new FileReader;return i.readAsArrayBuffer(e),i.onload=function(){n(i.result)},i.onerror=function(e){Mt.sendToBugsnag(e,"error"),n(null)},r})(n).then(function(e){r(e?{buffer:e,blob:n,contentType:n.type}:null)})},s.send(),i)}function ko(e,t){var n,r;return wo(this,void 0,et,function(){var i;return bo(this,function(o){switch(o.label){case 0:return i=e.window,(null===(r=null===(n=i.crypto)||void 0===n?void 0:n.subtle)||void 0===r?void 0:r.digest)?[4,i.crypto.subtle.digest({name:"sha-1"},t)]:[3,2];case 1:return[2,{hash:co(o.sent()),algorithm:"sha1"}];case 2:return[4,yo(e.time,so,t)];case 3:return[2,{hash:o.sent().hash,algorithm:"fsnv"}];}})})}var Io=function(e,t,n,r){return new(n||(n=Promise))(function(i,o){function s(e){try{u(r.next(e))}catch(e){o(e)}}function a(e){try{u(r["throw"](e))}catch(e){o(e)}}function u(e){var t;e.done?i(e.value):(t=e.value,t instanceof n?t:new n(function(e){e(t)})).then(s,a)}u((r=r.apply(e,t||[])).next())})},Co=function(e,t){var n,r,i,o,s={label:0,sent:function(){if(1&i[0])throw i[1];return i[1]},trys:[],ops:[]};return o={next:a(0),"throw":a(1),"return":a(2)},"function"==typeof Symbol&&(o[Symbol.iterator]=function(){return this}),o;function a(o){return function(a){return function(o){if(n)throw new TypeError("Generator is already executing.");for(;s;)try{if(n=1,r&&(i=2&o[0]?r["return"]:o[0]?r["throw"]||((i=r["return"])&&i.call(r),0):r.next)&&!(i=i.call(r,o[1])).done)return i;switch(r=0,i&&(o=[2&o[0],i.value]),o[0]){case 0:case 1:i=o;break;case 4:return s.label++,{value:o[1],done:!1};case 5:s.label++,r=o[1],o=[0];continue;case 7:o=s.ops.pop(),s.trys.pop();continue;default:if(!(i=(i=s.trys).length>0&&i[i.length-1])&&(6===o[0]||2===o[0])){s=0;continue}if(3===o[0]&&(!i||o[1]>i[0]&&o[1]0&&(this._regexes=this._joinRegexes(t))},e.prototype.matches=function(e){return!!this._regexes&&this._regexes.test(e)},e.prototype._isValidRegex=function(e){try{return new RegExp(e),!0}catch(t){return Mt.sendToBugsnag("Browser rejected UrlKeep.Regex","error",{expr:e,error:t.toString()}),!1}},e.prototype._joinRegexes=function(e){try{return new RegExp("("+e.join(")|(")+")","i")}catch(t){return Mt.sendToBugsnag("Browser rejected joining UrlKeep.Regexs","error",{exprs:e,error:t.toString()}),null}},e}();function Po(e,t){var n=Mn(e)+" ";return e.id&&(n+="#"+e.id),n+="[src="+ur(e.src,t,{source:"log",type:"debug"})+"]"}var qo,Uo=function(e){var t=e.transport,n=e.frame,r=e.orgId,s=e.scheme,a=e.script,u=e.recHost,c=e.appHost,h=Po(n,r);o("Injecting into Frame "+h);try{if(function(e){return e.id==e.name&&No.test(e.id)}(n))return void o("Blacklisted iframe: "+h);if(function(e){if(!e.contentDocument||!e.contentWindow||!e.contentWindow.location)return!0;return function(e){return!!e.src&&"about:blank"!=e.src&&e.src.indexOf("javascript:")<0}(e)&&e.src!=e.contentWindow.location.href&&"loading"==e.contentDocument.readyState}(n))return void o("Frame not yet loaded: "+h);var d=n.contentWindow,l=n.contentDocument;if(!d||!l)return void o("Missing contentWindow or contentDocument: "+h);if(!l.head)return void o("Missing contentDocument.head: "+h);if(I(d))return void o("FS already defined in Frame contentWindow: "+h+". Ignoring.");d._fs_org=r,d._fs_script=a,d._fs_rec_host=u,d._fs_app_host=c,d._fs_debug=i(),d._fs_run_in_iframe=!0,t&&(d._fs_transport=t);var p=l.createElement("script");p.async=!0,p.crossOrigin="anonymous",p.src=s+"//"+a,"testdrive"==r&&(p.src+="?allowMoo=true"),l.head.appendChild(p)}catch(e){o("iFrame no injecty. Probably not same origin.")}},No=/^fb\d{18}$/;!function(e){e[e.NoInfoYet=1]="NoInfoYet",e[e.Enabled=2]="Enabled",e[e.Disabled=3]="Disabled"}(qo||(qo={}));var Wo=function(){function e(e,t,n,r){var i=this;this._ctx=e,this._transport=n,this._injector=r,this._bundleUploadInterval=te.DefaultBundleUploadInterval,this._iFrames=[],this._pendingChildFrameIdInits=[],this._listeners=new Ut,this._getCurrentSessionEnabled=qo.NoInfoYet,this._resourceUploadingEnabled=!1,this._tickerTasks=[],this._pendingIframes={},this._watcher=new yn,this._queue=new io(e,this._transport,function(e){for(var t=i._eventWatcher.bundleEvents(e),n=void 0;n=i._tickerTasks.pop();)n();return t},t),this._keep=new Lo(e,this._queue),this._eventWatcher=new Di(e,this._queue,this._keep,this._watcher,this._listeners,function(e){i.onFrameCreated(e)},function(e){i.beforeFrameRemoved(e)},new Eo(e,this._queue,new Ao(e))),this._consoleWatcher=new Ji(e,this._queue,this._listeners),this._scheme=e.options.scheme,this._script=e.options.script,this._recHost=e.options.recHost,this._appHost=e.options.appHost,this._orgId=e.options.orgId,this._wnd=e.window}return e.prototype.bundleUploadInterval=function(){return this._bundleUploadInterval},e.prototype.start=function(e,t){var n=this;this._onFullyStarted=t,"onpagehide"in this._wnd?this._listeners.add(this._wnd,"pagehide",!1,function(e){n.onUnload()}):this._listeners.add(this._wnd,"unload",!1,function(e){n.onUnload()}),this._listeners.add(this._wnd,"message",!1,function(e){if("string"==typeof e.data){var t=e.source;n.postMessageReceived(t,Bo(e.data))}});var r=this._wnd.Document?this._wnd.Document.prototype:this._wnd.document;this._docCloseHook=Tt(r,"close"),this._docCloseHook&&this._docCloseHook.afterAsync(function(){n._listeners.refresh()})},e.prototype.queue=function(){return this._queue},e.prototype.eventWatcher=function(){return this._eventWatcher},e.prototype.console=function(){return this._consoleWatcher},e.prototype.onDomLoad=function(){this._eventWatcher.onDomLoad()},e.prototype.onLoad=function(){this._eventWatcher.onLoad()},e.prototype.shutdown=function(e){this._eventWatcher.shutdown(e),this._consoleWatcher.disable(),this._listeners&&this._listeners.clear(),this._docCloseHook&&this._docCloseHook.disable(),this.tellAllFramesTo(["ShutdownFrame"])},e.prototype.tellAllFramesTo=function(e){for(var t=0;t0){for(var e=0;e0&&this._transport.enqueueEvents(r,n);break;case"RequestFrameId":if(!e)return void o("No MessageEvent.source, iframe may have unloaded.");var s=this.iFrameWndToFsId(e);s?(o("Responding to FID request for frame "+s),this._pendingIframes[s]=!1,this.sendFrameIdToInnerFrame(e,s)):o("No FrameId found. Hoping to send one later.");}},e.prototype.sendFrameIdToInnerFrame=function(e,t){var n=this,r=function(){var r=[];0!=n._frameId&&(r=n._parentIds?n._parentIds.concat(n._frameId):[n._frameId]);var i=n._ctx.time.startTime();Do(e,["SetFrameId",t,r,i,n._scheme,n._script,n._appHost,n._orgId,n._pageRsp])};null==this._frameId?this._pendingChildFrameIdInits.push(r):r()},e.prototype.iFrameWndToFsId=function(e){for(var t=0;t=400&&502!==e||202==e||206==e}var jo=function(){return(jo=Object.assign||function(e){for(var t,n=1,r=arguments.length;n2e6))try{localStorage._fs_swan_song=t}catch(e){}},e.prototype._recover=function(){try{if("_fs_swan_song"in localStorage){var e=localStorage._fs_swan_song||localStorage.singSwanSong;delete localStorage._fs_swan_song,delete localStorage.singSwanSong;var t=wt(e);if(!(t.Bundles&&t.UserId&&t.SessionId&&t.PageId))return void o("Malformed swan song found. Ignoring it.");t.OrgId||(t.OrgId=this._identity.orgId()),t.Bundles.length>0&&(o("Sending "+t.Bundles.length+" bundles as prior page swan song"),this.sendSwanSongBundles(t))}}catch(e){o("Error recovering swan-song: "+e)}},e.prototype.sendSwanSongBundles=function(e,t){var n=this;void 0===t&&(t=0);var r=null;if(nt(e.Bundles)&&0!==e.Bundles.length&&void 0!==e.Bundles[0]){1==e.Bundles.length&&(r=this._ctx.time.wallTime()-(e.LastBundleTime||0));this._protocol.bundle({bundle:e.Bundles[0],deltaT:r,orgId:e.OrgId,pageId:e.PageId,serverBundleTime:e.ServerBundleTime,serverPageStart:e.ServerPageStart,sessionId:e.SessionId,userId:e.UserId,isNewSession:e.IsNewSession,win:function(t){o("Sent "+e.Bundles[0].Evts.length+" trailing events from last session as Seq "+e.Bundles[0].Seq),e.Bundles.shift(),e.Bundles.length>0?n.sendSwanSongBundles(jo(jo({},e),{ServerBundleTime:t.BundleTime})):o("Done with prior page swan song")},lose:function(r){Ho(r)?o("Fatal error while sending events, giving up"):(o("Failed to send events from last session, will retry while on this page"),n._lastSwanSongRetryTimeout=new n._timeoutFactory(n.sendSwanSongBundles,n._protocol.exponentialBackoffMs(t,!0),n,e,t+1).start())}})}},e}(),Vo=function(){function e(e,t,n,r){var i=this;void 0===t&&(t=new Ro(e)),void 0===n&&(n=en),void 0===r&&(r=tn),this._ctx=e,this._protocol=t,this._tickerFactory=n,this._backoffRetries=0,this._backoffTime=0,this._bundleSeq=1,this._lastPostTime=0,this._serverBundleTime=0,this._isNewSession=!1,this._largePageSize=16e6,this._outgoingEventQueue=[],this._bundleQueue=[],this._hibernating=!1,this._heartbeatInterval=0,this._lastUserActivity=this._ctx.time.wallTime(),this._finished=!1,this._scheme=e.options.scheme,this._identity=e.recording.identity,this._lastBundleTime=e.time.wallTime(),this._swanSong=new Ko(e,this._protocol,this._identity,r),this._heartbeatTimeout=new r(function(){i.onHeartbeat()}),this._hibernationTimeout=new r(function(){i.onHibernate()},te.PageInactivityTimeout)}return e.prototype.onShutdown=function(e){this._onShutdown=e},e.prototype.scheme=function(){return this._scheme},e.prototype.enqueueEvents=function(e,t){if(this.maybeHibernate(),this._hibernating){if(this._finished)return;for(var n=0,r=t;n0&&this.enqueueNextBundle(!0),this._bundleQueue.length>0||this._pendingBundle)){var e=this._bundleQueue.concat();this._pendingBundle&&e.unshift(this._pendingBundle),this._swanSong.sing({pageId:this._pageId,bundles:e,lastBundleTime:this._lastBundleTime,serverPageStart:this._serverPageStart,serverBundleTime:this._serverBundleTime,isNewSession:this._isNewSession})}},e.prototype.maybeHibernate=function(){this._hibernating||this.calcLastUserActivityDuration()>=te.PageInactivityTimeout+5e3&&this.onHibernate()},e.prototype.calcLastUserActivityDuration=function(){return s.mathFloor(this._ctx.time.wallTime()-this._lastUserActivity)},e.prototype.onHeartbeat=function(){var e=this.calcLastUserActivityDuration();this._outgoingEventQueue.push({When:this._ctx.time.now(),Kind:R.HEARTBEAT,Args:[e]}),this._heartbeatInterval*=2,this._heartbeatInterval>te.HeartbeatMax&&(this._heartbeatInterval=te.HeartbeatMax),this._heartbeatTimeout.start(this._heartbeatInterval)},e.prototype.onHibernate=function(){this._hibernating||(this.calcLastUserActivityDuration()<=2*te.PageInactivityTimeout&&(this._outgoingEventQueue.push({When:this._ctx.time.now(),Kind:R.UNLOAD,Args:[V.Hibernation]}),this.singSwanSong()),this.stopPipeline(),this._hibernating=!0)},e.prototype.enqueueAndSendBundle=function(){this._pendingBundle?this._pendingBundleFailed&&this._sendPendingBundle():0!=this._outgoingEventQueue.length?this.enqueueNextBundle():this.maybeSendNextBundle()},e.prototype.enqueueNextBundle=function(e){void 0===e&&(e=!1);var t={When:this._outgoingEventQueue[0].When,Seq:this._bundleSeq++,Evts:this._outgoingEventQueue};this._outgoingEventQueue=[],this._bundleQueue.push(t),e?this._protocol.bundleBeacon({bundle:t,deltaT:null,orgId:this._identity.orgId(),pageId:this._pageId,serverBundleTime:this._serverBundleTime,serverPageStart:this._serverPageStart,isNewSession:this._isNewSession,sessionId:this._identity.sessionId(),userId:this._identity.userId(),win:function(){},lose:function(){}}):this.maybeSendNextBundle()},e.prototype.maybeSendNextBundle=function(){this._pageId&&this._serverPageStart&&!this._pendingBundle&&0!=this._bundleQueue.length&&(this._pendingBundle=this._bundleQueue.shift(),this._sendPendingBundle())},e.prototype._sendPendingBundle=function(){var e=this,t=this._ctx.time.wallTime();if(!(te._ctx.recording.bundleUploadInterval()&&e.maybeSendNextBundle()},function(t){if(o("Failed to send events."),Ho(t))return 206==t?Mt.sendToBugsnag("Failed to send bundle, probably because of its large size","error"):t>=500&&Mt.sendToBugsnag("Failed to send bundle, recording outage likely","error"),void(e._onShutdown&&e._onShutdown());e._pendingBundleFailed=!0,e._backoffTime=e._lastPostTime+e._protocol.exponentialBackoffMs(e._backoffRetries++,!1)}))}},e.prototype.sendBundle=function(e,t,n){var r=s.mathFloor(this._ctx.time.wallTime()-this._lastUserActivity),i=this._protocol.bundle({bundle:e,deltaT:null,lastUserActivity:r,orgId:this._identity.orgId(),pageId:this._pageId,serverBundleTime:this._serverBundleTime,serverPageStart:this._serverPageStart,isNewSession:this._isNewSession,sessionId:this._identity.sessionId(),userId:this._identity.userId(),win:t,lose:n});i>this._largePageSize&&this._bundleSeq>16&&(o("splitting large page: "+i),this._ctx.recording.splitPage(V.Size))},e}(),zo=function(){var e=function(t,n){return(e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n])})(t,n)};return function(t,n){function r(){this.constructor=t}e(t,n),t.prototype=null===n?Object.create(n):(r.prototype=n.prototype,new r)}}(),Yo=function(e){function t(t,n,r,i,o){void 0===r&&(r=new Vo(t,n)),void 0===i&&(i=en),void 0===o&&(o=Uo);var s,a,u=e.call(this,t,i,r,o)||this;return u._protocol=n,u._domLoaded=!1,u._recordingDisabled=!1,u._integrationScriptFetched=!1,r.onShutdown(function(){return u.shutdown(V.SettingsBlocked)}),u._doc=u._wnd.document,u._frameId=0,u._identity=t.recording.identity,u._getCurrentSessionEnabled=qo.NoInfoYet,s=u._wnd,a=function(e){if(u._eventWatcher.shutdown(V.Api),e){var t=u._doc.getElementById(e);t&&t.setAttribute("_fs_embed_token",u._embedToken)}},s._fs_shutdown=a,u}return zo(t,e),t.prototype.onDomLoad=function(){var t=this;e.prototype.onDomLoad.call(this),this._domLoaded=!0,this.injectIntegrationScript(function(){t.fireFsReady(t._recordingDisabled)})},t.prototype.getReplayFlags=function(){var e=U(this._wnd);if(/[?&]_fs_force_session=true(&|#|$)/.test(location.search)&&(e+=",forceSession",this._wnd.history)){var t=location.search.replace(/(^\?|&)_fs_force_session=true(&|$)/,function(e,t,n){return n?t:""});this._wnd.history.replaceState({},"",this._wnd.location.href.replace(location.search,t))}return e},t.prototype.start=function(t,n){var r,i,o,s=this;e.prototype.start.call(this,t,n);var a=this.getReplayFlags(),u=Vt(this._doc),c=u[0],h=u[1],d=bt(this._wnd),l=d[0],p=d[1],f="";t||(f=this._identity.userId());var v=null!==(o=null===(i=null===(r=this._ctx)||void 0===r?void 0:r.recording)||void 0===i?void 0:i.preroll)&&void 0!==o?o:-1,_=ur(Jn(this._wnd),this._orgId,{source:"page",type:"base"}),g=ur(this._doc.referrer,this._orgId,{source:"page",type:"referrer"}),m=ur(this._wnd.location.href,this._orgId,{source:"page",type:"url"}),y={OrgId:this._orgId,UserId:f,Url:m,Base:_,Width:c,Height:h,ScreenWidth:l,ScreenHeight:p,Referrer:g,Preroll:v,Doctype:yt(this._doc),CompiledTimestamp:1591209308,AppId:this._identity.appId()};a&&(y.ReplayFlags=a),this._protocol.page(y,function(e){s.handleResponse(e),s.handleIdentity(e.CookieDomain,e.UserIntId,e.SessionIntId,e.PageIntId,e.EmbedToken),s.handleIntegrationScript(e.IntegrationScript),e.PreviewMode&&s.maybeInjectPreviewScript();var t=s._wnd._fs_pagestart;t&&t();var n=!!e.Consented;s._queue.enqueueFirst({Kind:R.SYS_REPORTCONSENT,Args:[n,K.Document]}),s._queue.enqueueFirst({Kind:R.SET_FRAME_BASE,Args:[ur(Jn(s._wnd),s._orgId,{source:"event",type:R.SET_FRAME_BASE}),yt(s._doc)]}),s._queue.startPipeline({pageId:e.PageIntId,serverPageStart:e.PageStart,isNewSession:!!e.IsNewSession}),s.fullyStarted()},function(e,t){t&&t.user_id&&t.cookie_domain&&t.reason_code==$.ReasonBlockedTrafficRamping&&f!=t.user_id&&s.handleIdentity(t.cookie_domain,t.user_id,"","",""),s.disableBecauseRecPageSaidSo()})},t.prototype.handleIntegrationScript=function(e){var t=this;this._integrationScriptFetched=!0,this._integrationScript=e,this.injectIntegrationScript(function(){t.fireFsReady(t._recordingDisabled)})},t.prototype.handleIdentity=function(e,t,n,r,i){var s=this._identity;s.setIds(this._wnd,e,t,n),this._embedToken=i,o("/User,"+s.userId()+"/Session,"+s.sessionId()+"/Page,"+r)},t.prototype.injectIntegrationScript=function(e){if(this._domLoaded&&this._integrationScriptFetched)if(this._integrationScript){var t=this._doc.createElement("script");this._wnd._fs_csp?(t.addEventListener("load",e),t.addEventListener("error",e),t.async=!0,t.src=this._scheme+"//"+this._recHost+"/rec/integrations?OrgId="+this._orgId,this._doc.head.appendChild(t)):(t.text=this._integrationScript,this._doc.head.appendChild(t),e())}else e()},t.prototype.maybeInjectPreviewScript=function(){if(!this._doc.getElementById("FullStory-preview-script")){var e=this._doc.createElement("script");e.id="FullStory-preview-script",e.async=!0,e.src=this._scheme+"//"+this._appHost+"/s/fspreview.js",this._doc.head.appendChild(e)}},t.prototype.disableBecauseRecPageSaidSo=function(){this.shutdown(V.SettingsBlocked),o("Disabling FS."),this._recordingDisabled=!0,this.fireFsReady(this._recordingDisabled)},t}(Wo),Go=function(){function e(e,t){void 0===t&&(t=new Qo(e)),this._wnd=e,this._messagePoster=t}return e.prototype.enqueueEvents=function(e,t){this._messagePoster.postMessage(this._wnd.parent,"EvtBundle",t,e)},e.prototype.startPipeline=function(){},e.prototype.stopPipeline=function(){},e.prototype.flush=function(){},e.prototype.singSwanSong=function(){},e.prototype.onShutdown=function(e){},e}(),Qo=function(){function e(e){this.wnd=e}return e.prototype.postMessage=function(e,t,n,r){var i=N(this.wnd);if(i)try{i.send(t,gt(n),r)}catch(e){i.send(t,gt(n))}else e.postMessage(function(e,t,n){var r=[e,t];n&&r.push(n);return gt({__fs:r})}(t,n,r),"*")},e}();var Xo,Jo=function(){var e=function(t,n){return(e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n])})(t,n)};return function(t,n){function r(){this.constructor=t}e(t,n),t.prototype=null===n?Object.create(n):(r.prototype=n.prototype,new r)}}(),$o=function(e){function t(t,n,r,i,o){void 0===n&&(n=new Qo(t.window)),void 0===r&&(r=new Go(t.window)),void 0===i&&(i=en),void 0===o&&(o=Uo);var s=e.call(this,t,i,r,o)||this;return s._messagePoster=n,s}return Jo(t,e),t.prototype.start=function(t,n){var r=this;e.prototype.start.call(this,t,n),this.sendRequestForFrameId(),this._listeners.add(this._wnd,"load",!1,function(){r._eventWatcher.recordingIsDetached()&&(o("Recording wrong document. Restarting recording in iframe."),r._ctx.recording.splitPage(V.FsShutdownFrame))})},t.prototype.postMessageReceived=function(t,n){if(e.prototype.postMessageReceived.call(this,t,n),t==this._wnd.parent||t==this._wnd)switch(n[0]){case"GreetFrame":this.sendRequestForFrameId();break;case"SetFrameId":try{var r=n[1],i=n[2],s=n[3],a=n[4],u=n[5],c=n[6],h=n[7],d=n[8];if(!r)return void o("Outer page gave us a bogus frame Id! Iframe: "+ur(location.href,h,{source:"log",type:"debug"}));this.setFrameIdFromOutside(r,i,s,a,u,c,h,d)}catch(e){o("Failed to parse frameId from message: "+gt(n))}break;case"SetConsent":var l=n[1];this.setConsent(l);break;case"InitFrameMobile":try{var p=JSON.parse(n[1]),f=p.StartTime;if(n.length>2){var v=n[2];if(v.hasOwnProperty("ProtocolVersion"))v.ProtocolVersion>=20180723&&v.hasOwnProperty("OuterStartTime")&&(f=v.OuterStartTime)}var _=p.Host;this.setFrameIdFromOutside(0,[],f,"https:",H(_),B(_),p.OrgId,p.PageResponse)}catch(e){o("Failed to initialize mobile web recording from message: "+gt(n))}}},t.prototype.sendRequestForFrameId=function(){this._frameId||(0!=this._frameId?this._wnd.parent?(o("Asking for a frame ID."),this._messagePoster.postMessage(this._wnd.parent,"RequestFrameId",[])):o("Orphaned window."):o("For some reason the outer window attempted to request a frameId"))},t.prototype.setFrameIdFromOutside=function(e,t,n,r,i,s,a,u){if(this._frameId)this._frameId!=e?(o("Updating frame id from "+this._frameId+" to "+e),this._ctx.recording.splitPage(V.FsShutdownFrame)):o("frame Id is already set to "+this._frameId);else{o("FrameId received within frame "+ur(location.href,a,{source:"log",type:"debug"})+": "+e),this._scheme=r,this._script=i,this._appHost=s,this._orgId=a,this._frameId=e,this._parentIds=t,this.handleResponse(u),this.fireFsReady();var c=!!u.Consented;this._queue.enqueueFirst({Kind:R.SYS_REPORTCONSENT,Args:[c,K.Document]}),this._queue.enqueueFirst({Kind:R.SET_FRAME_BASE,Args:[ur(Jn(this._wnd),a,{source:"event",type:R.SET_FRAME_BASE}),yt(this._wnd.document)]}),this._queue.rebaseIframe(n),this._ctx.time.setStartTime(n),this._queue.startPipeline({pageId:this._pageId,serverPageStart:u.PageStart,isNewSession:!!u.IsNewSession,frameId:e,parentIds:t}),this.flushPendingChildFrameInits(),this.fullyStarted()}},t}(Wo),Zo="fsidentity",es="newuid",ts=function(){function e(e,t){void 0===e&&(e=document),void 0===t&&(t=function(){}),this._doc=e,this._onWriteFailure=t,this._cookies={},this._appId=void 0}return e.prototype.initFromCookies=function(e,t){this._cookies=y(this._doc);var n=this._cookies.fs_uid;if(!n)try{n=localStorage._fs_uid}catch(e){}var r=m(n);r&&r.host.replace(/^www\./,"")==e.replace(/^www\./,"")&&r.orgId==t?this._cookie=r:this._cookie={expirationAbsTimeSeconds:g(),host:e,orgId:t,userId:"",sessionId:"",appKeyHash:""}},e.prototype.initFromParsedCookie=function(e){this._cookie=e},e.prototype.clear=function(){this._cookie.userId=this._cookie.sessionId=this._cookie.appKeyHash=this._appId="",this._cookie.expirationAbsTimeSeconds=g(),this._write()},e.prototype.host=function(){return this._cookie.host},e.prototype.orgId=function(){return this._cookie.orgId},e.prototype.userId=function(){return this._cookie.userId},e.prototype.sessionId=function(){return this._cookie.sessionId},e.prototype.appKeyHash=function(){return this._cookie.appKeyHash},e.prototype.cookieData=function(){return this._cookie},e.prototype.cookies=function(){return this._cookies},e.prototype.setCookie=function(e,t,n){void 0===n&&(n=new Date(p()+6048e5).toUTCString());var r=e+"="+t;this._domain?r+="; domain=."+encodeURIComponent(this._domain):r+="; domain=",r+="; Expires="+n+"; path=/; SameSite=Strict","https:"===location.protocol&&(r+="; Secure"),this._doc.cookie=r},e.prototype.setIds=function(e,t,n,r){(C(t)||t.match(/^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/g))&&(t="");var i=function(e){return e._fs_cookie_domain}(e);"string"==typeof i&&(t=i),this._domain=t,this._cookie.userId=n,this._cookie.sessionId=r,this._write()},e.prototype.clearAppId=function(){return!!this._cookie.appKeyHash&&(this._appId="",this._cookie.appKeyHash="",this._write(),!0)},e.prototype.setAppId=function(e){this._appId=e,this._cookie.appKeyHash=uo(e),this._write()},e.prototype.appId=function(){return this._appId},e.prototype.encode=function(){var e=this._cookie.host+"#"+this._cookie.orgId+"#"+this._cookie.userId+":"+this._cookie.sessionId;return this._cookie.appKeyHash&&(e+="#"+encodeURIComponent(this._cookie.appKeyHash)+"#"),e+="/"+this._cookie.expirationAbsTimeSeconds},e.prototype._write=function(){if(null!=this._domain){var e=this.encode(),t=new Date(1e3*this._cookie.expirationAbsTimeSeconds).toUTCString();this.setCookie("fs_uid",e,t);var n=[];-1===this._doc.cookie.indexOf(e)&&n.push(["fs_uid","cookie"]);try{localStorage._fs_uid=e,localStorage._fs_uid!==e&&n.push(["fs_uid","localStorage"])}catch(e){n.push(["fs_uid","localStorage",String(e)])}n.length>0&&this._onWriteFailure(n)}},e}();!function(e){e.rec="rec",e.user="user",e.account="account",e.consent="consent",e.customEvent="event",e.log="log"}(Xo||(Xo={}));var ns={acctId:"str",displayName:"str",website:"str"},rs={uid:"str",displayName:"str",email:"str"},is={str:os,bool:ss,real:as,"int":us,date:cs,strs:hs(os),bools:hs(ss),reals:hs(as),ints:hs(us),dates:hs(cs),objs:hs(ds),obj:ds};function os(e){return"string"==typeof e}function ss(e){return"boolean"==typeof e}function as(e){return"number"==typeof e}function us(e){return"number"==typeof e&&e-s.mathFloor(e)==0}function cs(e){return!!e&&(e.constructor===Date?!isNaN(e):("number"==typeof e||"string"==typeof e)&&!isNaN(new Date(e)))}function hs(e){return function(t){if(!(t instanceof Array))return!1;for(var n=0;n=0)return o("blocking FS.identify API call; uid value ("+n+") is illegal"),[void 0,Zo];var r=uo(n),i=void 0;t&&t._cookie.appKeyHash&&t._cookie.appKeyHash!==r&&t._cookie.appKeyHash!==n&&(o("user re-identified; existing uid hash ("+t._cookie.appKeyHash+") does not match provided uid ("+n+")"),i=es);return[n,i]}(a,this._identity),c=u[0],h=u[1];if(!c){switch(h){case Zo:case void 0:break;default:o("unexpected failReason returned from setAppId: "+h);}return{events:i}}t.uid=c,this._identity.setAppId(t.uid),h==es&&(r=!0)}}i.push.apply(i,this.rawEventsFromApi(X.User,rs,t,n));break;case Xo.customEvent:var d=t.n,l=t.p;i.push.apply(i,this.rawEventsFromApi(X.Event,{},l,n,d));break;default:o("invalid operation \""+e+"\"; only \"rec\", \"account\", and \"user\" are supported at present");}}catch(t){o("unexpected exception handling "+e+" API call: "+t.message)}return{events:i,reidentify:r}},e.prototype.rawEventsFromApi=function(e,t,n,r,i){var a=function e(t,n,r){var i={PayloadToSend:{},ValidationErrors:[]},a=function(r){var o=e(t,n,r);return i.ValidationErrors=i.ValidationErrors.concat(o.ValidationErrors),o.PayloadToSend};return ht(r,function(e,r){var u=function(e,t,n,r){var i=t,a=typeof n;if("undefined"===a)return o("Cannot infer type of "+a+" "+n),r.push({Type:"vartype",FieldName:t,ValueType:a+" (unsupported)"}),null;if(s.objectHasOwnProp(e,t))return{name:t,type:e[t]};var u=t.lastIndexOf("_");if(-1==u||!vs(t.substring(u+1))){var c=function(e){for(var t in is)if(is[t](e))return t;return null}(n);if(null==c)return o("Cannot infer type of "+a+" "+n),n?r.push({Type:"vartype",FieldName:t}):r.push({Type:"vartype",FieldName:t,ValueType:"null (unsupported)"}),null;u=t.length,o("Warning: Inferring user variable \""+t+"\" to be of type \""+c+"\""),t=t+"_"+c}var h=[t.substring(0,u),t.substring(u+1)],d=h[0],l=h[1];if("object"===a&&!n)return o("null is not a valid object type"),r.push({Type:"vartype",FieldName:i,ValueType:"null (unsupported)"}),null;if(!ls.test(d)){d=d.replace(/[^a-zA-Z0-9_]/g,"").replace(/^[0-9]+/,""),/[0-9]/.test(d[0])&&(d=d.substring(1)),r.push({Type:"varname",FieldName:i});var p=d+"_"+l;if(o("Warning: variable \""+i+"\" has invalid characters. It should match /"+ls.source+"/. Converted name to \""+p+"\"."),""==d)return null;t=p}if(!vs(l))return o("Variable \""+i+"\" has invalid type \""+l+"\""),r.push({Type:"varname",FieldName:i}),null;if(!function(e,t){return is[e](t)}(l,n))return o("illegal value "+gt(n)+" for type "+l),"number"===a?a=n%1==0?"integer":"real":"object"==a&&null!=n&&n.constructor==Date&&(a=isNaN(n)?"invalid date":"date"),r.push({Type:"vartype",FieldName:i,ValueType:a}),null;return{name:t,type:l}}(n,r,e,i.ValidationErrors);if(u){var c=u.name,h=u.type;if("obj"!=h){if("objs"!=h){var d,l;i.PayloadToSend[c]=fs(h,e)}else{t!=X.Event&&i.ValidationErrors.push({Type:"vartype",FieldName:c,ValueType:"Array (unsupported)"});for(var p=e,f=[],v=0;v0&&(i.PayloadToSend[c]=f)}}else{var _=a(e),g=(l="_obj").length>(d=r).length||d.substring(d.length-l.length)!=l?c.substring(0,c.length-"_obj".length):c;i.PayloadToSend[g]=_}}else i.PayloadToSend[r]=fs("",e)}),i}(e,t,n),u=[],c=e==X.Event,h=gt(a.PayloadToSend),d=!!r&&"fs"!==r;return c?u.push({When:0,Kind:R.SYS_CUSTOM,Args:d?[i,h,r]:[i,h]}):u.push({When:0,Kind:R.SYS_SETVAR,Args:d?[e,h,r]:[e,h]}),u},e}();function fs(e,t){return"str"==e&&null!=t&&(t=t.trim()),null==t||"date"!=e&&t.constructor!=Date||(t=function(e){var t,n=e.constructor===Date?e:new Date(e);try{t=n.toISOString()}catch(e){t=null}return t}(t)),t}function vs(e){return!!is[e]}var _s=function(){return(_s=Object.assign||function(e){for(var t,n=1,r=arguments.length;n0&&i[i.length-1])&&(6===o[0]||2===o[0])){s=0;continue}if(3===o[0]&&(!i||o[1]>i[0]&&o[1]u.length?u.length:a.length,d=1,l=d;l=0}var Es=["__zone_symbol__OriginalDelegate","nr@original"];function Ts(e,t){if(t){for(var n=0,r=Es;n16)Mt.sendToBugsnag("Too much synchronous recursion in requestMeasureTask","error");else{var n=this.performingMeasurements?this.recursionDepth:0,r=Mt.wrap(function(){var r=t.recursionDepth;t.recursionDepth=n+1;try{e()}finally{t.recursionDepth=r}});this.measurementTasks?this.measurementTasks.push(r):(this.measurementTasks=[r],this.schedule())}},e.prototype.performMeasurementsNow=function(){this.performMeasurements()},e}(),As=function(e){function t(t,n){var r=e.call(this)||this;return r.wnd=t,r.ResizeObserver=n,r}return Cs(t,e),t.prototype.schedule=function(){var e=this,t=this.ResizeObserver,n=this.wnd.document,r=n.body||n.documentElement||n.head,i=new t(function(){i.unobserve(r),e.performMeasurements()});i.observe(r)},t}(Rs),xs=function(e){function t(t,n,r){var i=e.call(this)||this;return i.wnd=t,i.requestWindowAnimationFrame=n,i.onRAF=Mt.wrap(function(){i.ch.port2.postMessage(void 0)}),i.ch=new r,i}return Cs(t,e),t.prototype.schedule=function(){this.ch.port1.onmessage=this.performMeasurements,this.requestWindowAnimationFrame(this.wnd,this.onRAF)},t}(Rs),Os=function(e){function t(t){var n=e.call(this)||this;return n.wnd=t,n}return Cs(t,e),t.prototype.schedule=function(){s.setWindowTimeout(this.wnd,this.performMeasurements,0)},t}(Rs),Ms=function(e,t,n,r){return new(n||(n=Promise))(function(i,o){function s(e){try{u(r.next(e))}catch(e){o(e)}}function a(e){try{u(r["throw"](e))}catch(e){o(e)}}function u(e){var t;e.done?i(e.value):(t=e.value,t instanceof n?t:new n(function(e){e(t)})).then(s,a)}u((r=r.apply(e,t||[])).next())})},Ls=function(e,t){var n,r,i,o,s={label:0,sent:function(){if(1&i[0])throw i[1];return i[1]},trys:[],ops:[]};return o={next:a(0),"throw":a(1),"return":a(2)},"function"==typeof Symbol&&(o[Symbol.iterator]=function(){return this}),o;function a(o){return function(a){return function(o){if(n)throw new TypeError("Generator is already executing.");for(;s;)try{if(n=1,r&&(i=2&o[0]?r["return"]:o[0]?r["throw"]||((i=r["return"])&&i.call(r),0):r.next)&&!(i=i.call(r,o[1])).done)return i;switch(r=0,i&&(o=[2&o[0],i.value]),o[0]){case 0:case 1:i=o;break;case 4:return s.label++,{value:o[1],done:!1};case 5:s.label++,r=o[1],o=[0];continue;case 7:o=s.ops.pop(),s.trys.pop();continue;default:if(!(i=(i=s.trys).length>0&&i[i.length-1])&&(6===o[0]||2===o[0])){s=0;continue}if(3===o[0]&&(!i||o[1]>i[0]&&o[1]=8)return void o("reidentified too many times; giving up");this.reidentifyCount++,W(this.wnd,[e,t]),this.splitPage(V.Reidentify,!0)}else u();void 0!==a&&(a?this.restart():this.shutdown(V.Api))}else W(this.wnd,[e,t])},e.prototype._cookies=function(){return this.identity?this.identity.cookies():(o("Error in FS integration: Can't get cookies from inside an iframe"),null)},e.prototype._setCookie=function(e,t){this.identity?this.identity.setCookie(e,t):o("Error in FS integration: Can't set cookies from inside an iframe")},e.prototype._withEventQueue=function(e,t){if(this.recorder){var n=this.recorder.queue(),r=this.recorder.pageSignature();null!=n&&null!=r?e===r?t(n):Mt.sendToBugsnag("Error in _withEventQueue: Page Signature mismatch","error",{pageSignature:r,callerSignature:e}):o("Error getting event queue or page signature: Recorder not initialized")}else o("Error in FS integration: Recorder not initialized")},e.prototype.initApi=function(){var e=I(this.wnd);e?(e.getCurrentSessionURL=_t(this.getCurrentSessionURL,this),e.getCurrentSession=_t(this.getCurrentSession,this),e.enableConsole=_t(this.enableConsole,this),e.disableConsole=_t(this.disableConsole,this),e.log=_t(this.log,this),e.shutdown=_t(this.shutdownApi,this),e.restart=_t(this.restart,this),e._api=_t(this._api,this),e._cookies=_t(this._cookies,this),e._setCookie=_t(this._setCookie,this),e._withEventQueue=_t(this._withEventQueue,this)):o("Missing browser API namespace; couldn't initialize API.")},e.prototype.start=function(){var e,t=this;e=L(this.wnd),r=e,o("script version UNSET (compiled at 1591209308)");var n=P(this.wnd);if(n){this.orgId=n;var i,s=(i=this.wnd)._fs_script||H(D(i));if(s){this.script=s;var a=F(this.wnd);if(a){this.recHost=a;var u=function(e){return e._fs_app_host||B(D(e))}(this.wnd);u?(this.appHost=u,o("script: "+this.script),o("recording host: "+this.recHost),o("orgid: "+this.orgId),"localhost:8080"==this.recHost&&(this.scheme="http:"),this.inFrame()||(this.identity=new ts(this.wnd.document,function(e){for(var n,r=0,i=e;r { describe('full_story', () => { - it('allows orgId when enabled: false', () => { + it('allows org_id when enabled: false', () => { expect(() => config.schema.validate({ full_story: { enabled: false, org_id: 'asdf' } }) ).not.toThrow(); }); - it('rejects undefined or empty orgId when enabled: true', () => { + it('rejects undefined or empty org_id when enabled: true', () => { expect(() => config.schema.validate({ full_story: { enabled: true } }) ).toThrowErrorMatchingInlineSnapshot( @@ -28,7 +28,7 @@ describe('xpack.cloud config', () => { ); }); - it('accepts orgId when enabled: true', () => { + it('accepts org_id when enabled: true', () => { expect(() => config.schema.validate({ full_story: { enabled: true, org_id: 'asdf' } }) ).not.toThrow(); diff --git a/x-pack/plugins/cloud/server/plugin.ts b/x-pack/plugins/cloud/server/plugin.ts index fea8be9f934e1..4c0b7b7f7eca6 100644 --- a/x-pack/plugins/cloud/server/plugin.ts +++ b/x-pack/plugins/cloud/server/plugin.ts @@ -11,6 +11,7 @@ import { CloudConfigType } from './config'; import { registerCloudUsageCollector } from './collectors'; import { getIsCloudEnabled } from '../common/is_cloud_enabled'; import { parseDeploymentIdFromDeploymentUrl } from './utils'; +import { registerFullstoryRoute } from './routes/fullstory'; interface PluginsSetup { usageCollection?: UsageCollectionSetup; @@ -40,6 +41,13 @@ export class CloudPlugin implements Plugin { const isCloudEnabled = getIsCloudEnabled(this.config.id); registerCloudUsageCollector(usageCollection, { isCloudEnabled }); + if (this.config.full_story.enabled) { + registerFullstoryRoute({ + httpResources: core.http.resources, + packageInfo: this.context.env.packageInfo, + }); + } + return { cloudId: this.config.id, deploymentId: parseDeploymentIdFromDeploymentUrl(this.config.deployment_url), diff --git a/x-pack/plugins/cloud/server/routes/fullstory.test.ts b/x-pack/plugins/cloud/server/routes/fullstory.test.ts new file mode 100644 index 0000000000000..dae541a8c033c --- /dev/null +++ b/x-pack/plugins/cloud/server/routes/fullstory.test.ts @@ -0,0 +1,62 @@ +/* + * 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. + */ + +jest.mock('fs/promises'); +import { renderFullStoryLibraryFactory, FULLSTORY_LIBRARY_PATH } from './fullstory'; +import fs from 'fs/promises'; + +const fsMock = fs as jest.Mocked; + +describe('renderFullStoryLibraryFactory', () => { + beforeEach(() => { + jest.resetAllMocks(); + fsMock.readFile.mockResolvedValue(Buffer.from('fake fs src')); + }); + afterAll(() => jest.restoreAllMocks()); + + it('successfully returns file contents', async () => { + const render = renderFullStoryLibraryFactory(); + + const { body } = await render(); + expect(fsMock.readFile).toHaveBeenCalledWith(FULLSTORY_LIBRARY_PATH); + expect(body.toString()).toEqual('fake fs src'); + }); + + it('only reads from file system once callback is invoked', async () => { + const render = renderFullStoryLibraryFactory(); + expect(fsMock.readFile).not.toHaveBeenCalled(); + await render(); + expect(fsMock.readFile).toHaveBeenCalledTimes(1); + }); + + it('does not read from filesystem on subsequent calls', async () => { + const render = renderFullStoryLibraryFactory(); + await render(); + expect(fsMock.readFile).toHaveBeenCalledTimes(1); + await render(); + expect(fsMock.readFile).toHaveBeenCalledTimes(1); + await render(); + expect(fsMock.readFile).toHaveBeenCalledTimes(1); + }); + + it('returns max-age cache-control in dist', async () => { + const render = renderFullStoryLibraryFactory(true); + const { headers } = await render(); + expect(headers).toEqual({ + 'cache-control': 'max-age=31536000', + }); + }); + + it('returns must-revalidate cache-control and sha1 etag in dev', async () => { + const render = renderFullStoryLibraryFactory(false); + const { headers } = await render(); + expect(headers).toEqual({ + 'cache-control': 'must-revalidate', + etag: '1e02f94b45750ba9284c111d31ae7e59c13b8e6e', + }); + }); +}); diff --git a/x-pack/plugins/cloud/server/routes/fullstory.ts b/x-pack/plugins/cloud/server/routes/fullstory.ts new file mode 100644 index 0000000000000..c614d803eed9f --- /dev/null +++ b/x-pack/plugins/cloud/server/routes/fullstory.ts @@ -0,0 +1,75 @@ +/* + * 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 path from 'path'; +import fs from 'fs/promises'; +import { createHash } from 'crypto'; +import { once } from 'lodash'; +import { HttpResources, HttpResponseOptions, PackageInfo } from '../../../../../src/core/server'; + +const MINUTE = 60; +const HOUR = 60 * MINUTE; +const DAY = 24 * HOUR; + +/** @internal exported for testing */ +export const FULLSTORY_LIBRARY_PATH = path.join(__dirname, '..', 'assets', 'fullstory_library.js'); + +/** @internal exported for testing */ +export const renderFullStoryLibraryFactory = (dist = true) => + once( + async (): Promise<{ + body: Buffer; + headers: HttpResponseOptions['headers']; + }> => { + const srcBuffer = await fs.readFile(FULLSTORY_LIBRARY_PATH); + const hash = createHash('sha1'); + hash.update(srcBuffer); + const hashDigest = hash.digest('hex'); + + return { + body: srcBuffer, + // In dist mode, return a long max-age, otherwise use etag + must-revalidate + headers: dist + ? { 'cache-control': `max-age=${DAY * 365}` } + : { 'cache-control': 'must-revalidate', etag: hashDigest }, + }; + } + ); + +export const registerFullstoryRoute = ({ + httpResources, + packageInfo, +}: { + httpResources: HttpResources; + packageInfo: Readonly; +}) => { + const renderFullStoryLibrary = renderFullStoryLibraryFactory(packageInfo.dist); + + /** + * Register a custom JS endpoint in order to acheive best caching possible with `max-age` similar to plugin bundles. + */ + httpResources.register( + { + // Use the build number in the URL path to leverage max-age caching on production builds + path: `/internal/cloud/${packageInfo.buildNum}/fullstory.js`, + validate: false, + options: { + authRequired: false, + }, + }, + async (context, req, res) => { + try { + return res.renderJs(await renderFullStoryLibrary()); + } catch (e) { + return res.customError({ + body: `Could not load FullStory library from disk due to error: ${e.toString()}`, + statusCode: 500, + }); + } + } + ); +}; diff --git a/x-pack/test/cloud_integration/config.ts b/x-pack/test/cloud_integration/config.ts new file mode 100644 index 0000000000000..a012dfd1ad34b --- /dev/null +++ b/x-pack/test/cloud_integration/config.ts @@ -0,0 +1,87 @@ +/* + * 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 { resolve } from 'path'; +import { FtrConfigProviderContext } from '@kbn/test'; +import { services } from '../functional/services'; +import { pageObjects } from '../functional/page_objects'; + +const FULLSTORY_ORG_ID = process.env.FULLSTORY_ORG_ID; +const FULLSTORY_API_KEY = process.env.FULLSTORY_API_KEY; +const RUN_FULLSTORY_TESTS = Boolean(FULLSTORY_ORG_ID && FULLSTORY_API_KEY); + +// the default export of config files must be a config provider +// that returns an object with the projects config values +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const kibanaCommonConfig = await readConfigFile( + require.resolve('../../../test/common/config.js') + ); + const kibanaFunctionalConfig = await readConfigFile( + require.resolve('../../../test/functional/config.js') + ); + + const kibanaPort = kibanaFunctionalConfig.get('servers.kibana.port'); + const idpPath = resolve(__dirname, './fixtures/saml/saml_provider/metadata.xml'); + const samlIdPPlugin = resolve(__dirname, './fixtures/saml/saml_provider'); + + return { + testFiles: [...(RUN_FULLSTORY_TESTS ? [resolve(__dirname, './tests/fullstory')] : [])], + + services, + pageObjects, + + servers: kibanaFunctionalConfig.get('servers'), + + esTestCluster: { + license: 'trial', + from: 'snapshot', + serverArgs: [ + 'xpack.security.authc.token.enabled=true', + 'xpack.security.authc.realms.saml.saml1.order=0', + `xpack.security.authc.realms.saml.saml1.idp.metadata.path=${idpPath}`, + 'xpack.security.authc.realms.saml.saml1.idp.entity_id=http://www.elastic.co/saml1', + `xpack.security.authc.realms.saml.saml1.sp.entity_id=http://localhost:${kibanaPort}`, + `xpack.security.authc.realms.saml.saml1.sp.logout=http://localhost:${kibanaPort}/logout`, + `xpack.security.authc.realms.saml.saml1.sp.acs=http://localhost:${kibanaPort}/api/security/saml/callback`, + 'xpack.security.authc.realms.saml.saml1.attributes.principal=http://saml.elastic-cloud.com/attributes/principal', + 'xpack.security.authc.realms.saml.saml1.attributes.groups=http://saml.elastic-cloud.com/attributes/roles', + ], + }, + + kbnTestServer: { + ...kibanaCommonConfig.get('kbnTestServer'), + serverArgs: [ + ...kibanaCommonConfig.get('kbnTestServer.serverArgs'), + `--plugin-path=${samlIdPPlugin}`, + '--server.uuid=5b2de169-2785-441b-ae8c-186a1936b17d', + '--xpack.security.encryptionKey="wuGNaIhoMpk5sO4UBxgr3NyW1sFcLgIf"', + '--xpack.security.authc.selector.enabled=false', + '--xpack.security.authc.providers.saml.saml1.order=0', + '--xpack.security.authc.providers.saml.saml1.realm=saml1', + '--xpack.security.authc.providers.basic.basic1.order=1', + ...(RUN_FULLSTORY_TESTS + ? [ + '--xpack.cloud.full_story.enabled=true', + `--xpack.cloud.full_story.org_id=${FULLSTORY_ORG_ID}`, + ] + : []), + ], + }, + uiSettings: { + defaults: { + 'accessibility:disableAnimations': true, + 'dateFormat:tz': 'UTC', + }, + }, + apps: kibanaFunctionalConfig.get('apps'), + screenshots: { directory: resolve(__dirname, 'screenshots') }, + + junit: { + reportName: 'Chrome X-Pack Cloud Integration Functional Tests (SAML)', + }, + }; +} diff --git a/x-pack/test/cloud_integration/constants.ts b/x-pack/test/cloud_integration/constants.ts new file mode 100644 index 0000000000000..95c5477fbe97b --- /dev/null +++ b/x-pack/test/cloud_integration/constants.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const CLOUD_USER_ID = `1112244`; diff --git a/x-pack/test/cloud_integration/fixtures/saml/saml_provider/kibana.json b/x-pack/test/cloud_integration/fixtures/saml/saml_provider/kibana.json new file mode 100644 index 0000000000000..81ec23fc3d2f3 --- /dev/null +++ b/x-pack/test/cloud_integration/fixtures/saml/saml_provider/kibana.json @@ -0,0 +1,7 @@ +{ + "id": "samlProviderPlugin", + "version": "8.0.0", + "kibanaVersion": "kibana", + "server": true, + "ui": false +} diff --git a/x-pack/test/cloud_integration/fixtures/saml/saml_provider/metadata.xml b/x-pack/test/cloud_integration/fixtures/saml/saml_provider/metadata.xml new file mode 100644 index 0000000000000..19a6c13264144 --- /dev/null +++ b/x-pack/test/cloud_integration/fixtures/saml/saml_provider/metadata.xml @@ -0,0 +1,41 @@ + + + + + + + + MIIDOTCCAiGgAwIBAgIVANNWkg9lzNiLqNkMFhFKHcXyaZmqMA0GCSqGSIb3DQEB +CwUAMDQxMjAwBgNVBAMTKUVsYXN0aWMgQ2VydGlmaWNhdGUgVG9vbCBBdXRvZ2Vu +ZXJhdGVkIENBMCAXDTE5MTIyNzE3MDM0MloYDzIwNjkxMjE0MTcwMzQyWjARMQ8w +DQYDVQQDEwZraWJhbmEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCQ +wYYbQtbRBKJ4uNZc2+IgRU+7NNL21ZebQlEIMgK7jAqOMrsW2b5DATz41Fd+GQFU +FUYYjwo+PQj6sJHshOJo/gNb32HrydvMI7YPvevkszkuEGCfXxQ3Dw2RTACLgD0Q +OCkwHvn3TMf0loloV/ePGWaZDYZaXi3a5DdWi/HFFoJysgF0JV2f6XyKhJkGaEfJ +s9pWX269zH/XQvGNx4BEimJpYB8h4JnDYPFIiQdqj+sl2b+kS1hH9kL5gBAMXjFU +vcNnX+PmyTjyJrGo75k0ku+spBf1bMwuQt3uSmM+TQIXkvFDmS0DOVESrpA5EC1T +BUGRz6o/I88Xx4Mud771AgMBAAGjYzBhMB0GA1UdDgQWBBQLB1Eo23M3Ss8MsFaz +V+Twcb3PmDAfBgNVHSMEGDAWgBQa7SYOe8NGcF00EbwPHA91YCsHSTAUBgNVHREE +DTALgglsb2NhbGhvc3QwCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOCAQEAnEl/ +z5IElIjvkK4AgMPrNcRlvIGDt2orEik7b6Jsq6/RiJQ7cSsYTZf7xbqyxNsUOTxv ++frj47MEN448H2nRvUxH29YR3XygV5aEwADSAhwaQWn0QfWTCZbJTmSoNEDtDOzX +TGDlAoCD9s9Xz9S1JpxY4H+WWRZrBSDM6SC1c6CzuEeZRuScNAjYD5mh2v6fOlSy +b8xJWSg0AFlJPCa3ZsA2SKbNqI0uNfJTnkXRm88Z2NHcgtlADbOLKauWfCrpgsCk +cZgo6yAYkOM148h/8wGla1eX+iE1R72NUABGydu8MSQKvc0emWJkGsC1/KqPlf/O +eOUsdwn1yDKHRxDHyA== + + + + + + + + + + diff --git a/x-pack/test/cloud_integration/fixtures/saml/saml_provider/server/index.ts b/x-pack/test/cloud_integration/fixtures/saml/saml_provider/server/index.ts new file mode 100644 index 0000000000000..4ae669256d3a8 --- /dev/null +++ b/x-pack/test/cloud_integration/fixtures/saml/saml_provider/server/index.ts @@ -0,0 +1,15 @@ +/* + * 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 { PluginInitializer } from '../../../../../../../src/core/server'; +import { initRoutes } from './init_routes'; + +export const plugin: PluginInitializer = () => ({ + setup: (core) => initRoutes(core), + start: () => {}, + stop: () => {}, +}); diff --git a/x-pack/test/cloud_integration/fixtures/saml/saml_provider/server/init_routes.ts b/x-pack/test/cloud_integration/fixtures/saml/saml_provider/server/init_routes.ts new file mode 100644 index 0000000000000..6bea9af2ed310 --- /dev/null +++ b/x-pack/test/cloud_integration/fixtures/saml/saml_provider/server/init_routes.ts @@ -0,0 +1,58 @@ +/* + * 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 { CoreSetup } from '../../../../../../../src/core/server'; +import { getSAMLResponse, getSAMLRequestId } from './saml_tools'; + +export function initRoutes(core: CoreSetup) { + const serverInfo = core.http.getServerInfo(); + core.http.resources.register( + { + path: '/saml_provider/login', + validate: false, + options: { authRequired: false }, + }, + async (context, request, response) => { + const samlResponse = await getSAMLResponse({ + inResponseTo: await getSAMLRequestId(request.url.href!), + destination: `${serverInfo.protocol}://${serverInfo.hostname}:${serverInfo.port}/api/security/saml/callback`, + }); + + return response.renderHtml({ + body: ` + + Kibana SAML Login + + + +
+ +
+ + `, + }); + } + ); + + core.http.resources.register( + { path: '/saml_provider/login/submit.js', validate: false, options: { authRequired: false } }, + (context, request, response) => { + return response.renderJs({ body: 'document.getElementById("loginForm").submit();' }); + } + ); + + core.http.resources.register( + { + path: '/saml_provider/logout', + validate: false, + options: { authRequired: false }, + }, + async (context, request, response) => { + return response.redirected({ headers: { location: '/logout?SAMLResponse=something' } }); + } + ); +} diff --git a/x-pack/test/cloud_integration/fixtures/saml/saml_provider/server/saml_tools.ts b/x-pack/test/cloud_integration/fixtures/saml/saml_provider/server/saml_tools.ts new file mode 100644 index 0000000000000..da902d332e6e7 --- /dev/null +++ b/x-pack/test/cloud_integration/fixtures/saml/saml_provider/server/saml_tools.ts @@ -0,0 +1,161 @@ +/* + * 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 crypto from 'crypto'; +import fs from 'fs'; +import { stringify } from 'query-string'; +import url from 'url'; +import zlib from 'zlib'; +import { promisify } from 'util'; +import { parseString } from 'xml2js'; +import { SignedXml } from 'xml-crypto'; +import { KBN_KEY_PATH } from '@kbn/dev-utils'; +import { CLOUD_USER_ID } from '../../../../constants'; + +/** + * @file Defines a set of tools that allow us to parse and generate various SAML XML messages. + * The format of these XML messages is a minimum accepted by Elasticsearch and based on the format + * used by `Auth0` identity provider, `auth0/node-samlp` package and SAML 2.0 Specification: + * http://docs.oasis-open.org/security/saml/v2.0/saml-bindings-2.0-os.pdf. + */ + +const inflateRawAsync = promisify(zlib.inflateRaw); +const deflateRawAsync = promisify(zlib.deflateRaw); +const parseStringAsync = promisify(parseString); + +const signingKey = fs.readFileSync(KBN_KEY_PATH); +const signatureAlgorithm = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256'; + +export async function getSAMLRequestId(urlWithSAMLRequestId: string) { + const inflatedSAMLRequest = (await inflateRawAsync( + Buffer.from( + url.parse(urlWithSAMLRequestId, true /* parseQueryString */).query.SAMLRequest as string, + 'base64' + ) + )) as Buffer; + + const parsedSAMLRequest = (await parseStringAsync(inflatedSAMLRequest.toString())) as any; + return parsedSAMLRequest['saml2p:AuthnRequest'].$.ID; +} + +export async function getSAMLResponse({ + destination, + inResponseTo, + sessionIndex, + username = 'a@b.c', + issuer = 'http://www.elastic.co/saml1', +}: { + destination?: string; + inResponseTo?: string; + sessionIndex?: string; + username?: string; + issuer?: string; +} = {}) { + const issueInstant = new Date().toISOString(); + const notOnOrAfter = new Date(Date.now() + 3600 * 1000).toISOString(); + + const samlAssertionTemplateXML = ` + + ${issuer} + + a@b.c + + + + + + + urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified + + + + + ${CLOUD_USER_ID} + + + superuser + + + + `; + + const signature = new SignedXml(); + signature.signatureAlgorithm = signatureAlgorithm; + signature.signingKey = signingKey; + + // Adds a reference to a `Assertion` xml element and an array of transform algorithms to be used during signing. + signature.addReference( + `//*[local-name(.)='Assertion']`, + [ + 'http://www.w3.org/2000/09/xmldsig#enveloped-signature', + 'http://www.w3.org/2001/10/xml-exc-c14n#', + ], + 'http://www.w3.org/2001/04/xmlenc#sha256' + ); + + signature.computeSignature(samlAssertionTemplateXML, { + location: { reference: `//*[local-name(.)='Issuer']`, action: 'after' }, + }); + + return Buffer.from( + ` + + ${issuer} + + + ${signature.getSignedXml()} + + ` + ).toString('base64'); +} + +export async function getLogoutRequest({ + destination, + sessionIndex, + issuer = 'http://www.elastic.co/saml1', +}: { + destination: string; + sessionIndex: string; + issuer?: string; +}) { + const issueInstant = new Date().toISOString(); + const logoutRequestTemplateXML = ` + + ${issuer} + a@b.c + ${sessionIndex} + + `; + + // HTTP-Redirect with deflate encoding: + // http://docs.oasis-open.org/security/saml/v2.0/saml-bindings-2.0-os.pdf - section 3.4.4.1 + const deflatedLogoutRequest = (await deflateRawAsync( + Buffer.from(logoutRequestTemplateXML) + )) as Buffer; + + const queryStringParameters: Record = { + SAMLRequest: deflatedLogoutRequest.toString('base64'), + SigAlg: signatureAlgorithm, + }; + + const signer = crypto.createSign('RSA-SHA256'); + signer.update(stringify(queryStringParameters, { sort: false })); + queryStringParameters.Signature = signer.sign(signingKey.toString(), 'base64'); + + return queryStringParameters; +} diff --git a/x-pack/test/cloud_integration/ftr_provider_context.d.ts b/x-pack/test/cloud_integration/ftr_provider_context.d.ts new file mode 100644 index 0000000000000..66d4e37b795ca --- /dev/null +++ b/x-pack/test/cloud_integration/ftr_provider_context.d.ts @@ -0,0 +1,13 @@ +/* + * 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 { GenericFtrProviderContext } from '@kbn/test'; + +import { pageObjects } from '../functional/page_objects'; +import { services } from '../functional/services'; + +export type FtrProviderContext = GenericFtrProviderContext; diff --git a/x-pack/test/cloud_integration/tests/fullstory.ts b/x-pack/test/cloud_integration/tests/fullstory.ts new file mode 100644 index 0000000000000..1cdad719e94e5 --- /dev/null +++ b/x-pack/test/cloud_integration/tests/fullstory.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 expect from '@kbn/expect'; +import fetch from 'node-fetch'; +import { sha256 } from 'js-sha256'; +import { FtrProviderContext } from '../ftr_provider_context'; +import { CLOUD_USER_ID } from '../constants'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const browser = getService('browser'); + const find = getService('find'); + const PageObjects = getPageObjects(['common']); + + const delay = (ms: number) => new Promise((r) => setTimeout(r, ms)); + + describe('Cloud FullStory integration', function () { + before(async () => { + // Create role mapping so user gets superuser access + await getService('esSupertest') + .post('/_security/role_mapping/saml1') + .send({ roles: ['superuser'], enabled: true, rules: { field: { 'realm.name': 'saml1' } } }) + .expect(200); + }); + + it('initializes FullStory', async () => { + await PageObjects.common.navigateToApp('home'); + await find.byCssSelector('[data-test-subj="userMenuButton"]', 20000); + + // Check FullStory library loaded + // @ts-expect-error + expect(await browser.execute(() => typeof window.FSKibana === 'function')).to.eql(true); + }); + + it('records a FullStory session with the associated SAML user', async () => { + // Get session ID once fullstory has initialized + let sessionUrl: string | null = null; + let attempts = 0; + while (sessionUrl === null && attempts < 30) { + // @ts-expect-error + sessionUrl = await browser.execute(() => window.FSKibana.getCurrentSessionURL()); + attempts++; + await delay(1000); + } + expect(typeof sessionUrl).to.eql('string'); + sessionUrl = sessionUrl!.replace('%3A', ':'); // undo encoding so comparisons work with API response + + // Check that the session was recorded in the FS API for the given user based on their hashed ID + const hashedUserId = sha256(CLOUD_USER_ID); + const fsSessions = await fetch( + `https://www.fullstory.com/api/v1/sessions?uid=${hashedUserId}&limit=100`, + { + headers: { + 'content-type': 'application/json', + Authorization: `Basic ${process.env.FULLSTORY_API_KEY}`, + }, + } + ).then((r) => r.json()); + expect(fsSessions.find((s: any) => s.FsUrl === sessionUrl)).not.to.be(undefined); + }); + }); +} diff --git a/yarn.lock b/yarn.lock index 448c97ff82469..9d7569b6ab4f2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17600,6 +17600,11 @@ js-search@^1.4.3: resolved "https://registry.yarnpkg.com/js-search/-/js-search-1.4.3.tgz#23a86d7e064ca53a473930edc48615b6b1c1954a" integrity sha512-Sny5pf00kX1sM1KzvUC9nGYWXOvBfy30rmvZWeRktpg+esQKedIXrXNee/I2CAnsouCyaTjitZpRflDACx4toA== +js-sha256@^0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/js-sha256/-/js-sha256-0.9.0.tgz#0b89ac166583e91ef9123644bd3c5334ce9d0966" + integrity sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA== + js-sha3@0.8.0: version "0.8.0" resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840" From 67d4c3184efc3c909ae0e9ca819f3a7429327195 Mon Sep 17 00:00:00 2001 From: Kuldeep M Date: Thu, 24 Jun 2021 20:10:22 +0100 Subject: [PATCH 48/69] [Workplace Search] source connection panel content vertical alignment (#103225) * fix 1786 source connection panel vertical alignment * Update x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configured_sources_list.tsx Co-authored-by: Constance Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Constance --- .../components/add_source/configured_sources_list.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configured_sources_list.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configured_sources_list.tsx index ac19043a30ca6..6da09acf45cbe 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configured_sources_list.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configured_sources_list.tsx @@ -70,7 +70,12 @@ export const ConfiguredSourcesList: React.FC = ({ - + Date: Thu, 24 Jun 2021 20:12:52 +0100 Subject: [PATCH 49/69] [Logs UI] Log threshold rule performance improvements (#102650) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add optimisations for executor / chart previews Co-authored-by: Felix Stürmer --- .../alerting/logs/log_threshold/types.ts | 108 +++-- .../http_api/log_alerts/chart_preview_data.ts | 9 + .../criterion_preview_chart.tsx | 13 +- .../components/expression_editor/editor.tsx | 25 + .../log_threshold_chart_preview.ts | 54 ++- .../log_threshold_executor.test.ts | 455 ++++++++---------- .../log_threshold/log_threshold_executor.ts | 154 ++++-- 7 files changed, 483 insertions(+), 335 deletions(-) diff --git a/x-pack/plugins/infra/common/alerting/logs/log_threshold/types.ts b/x-pack/plugins/infra/common/alerting/logs/log_threshold/types.ts index f1e983fc34df8..6da0bb58e4e85 100644 --- a/x-pack/plugins/infra/common/alerting/logs/log_threshold/types.ts +++ b/x-pack/plugins/infra/common/alerting/logs/log_threshold/types.ts @@ -100,7 +100,7 @@ export enum AlertStates { ERROR, } -const ThresholdRT = rt.type({ +export const ThresholdRT = rt.type({ comparator: ComparatorRT, value: rt.number, }); @@ -240,31 +240,43 @@ const chartPreviewHistogramBucket = rt.type({ doc_count: rt.number, }); +const ChartPreviewBucketsRT = rt.partial({ + histogramBuckets: rt.type({ + buckets: rt.array(chartPreviewHistogramBucket), + }), +}); + // ES query responses // +const hitsRT = rt.type({ + total: rt.type({ + value: rt.number, + }), +}); + +const bucketFieldsRT = rt.type({ + key: rt.record(rt.string, rt.string), + doc_count: rt.number, +}); + +const afterKeyRT = rt.partial({ + after_key: rt.record(rt.string, rt.string), +}); + export const UngroupedSearchQueryResponseRT = rt.intersection([ commonSearchSuccessResponseFieldsRT, rt.intersection([ rt.type({ - hits: rt.type({ - total: rt.type({ - value: rt.number, - }), - }), + hits: hitsRT, }), - // Chart preview buckets rt.partial({ - aggregations: rt.type({ - histogramBuckets: rt.type({ - buckets: rt.array(chartPreviewHistogramBucket), - }), - }), + aggregations: ChartPreviewBucketsRT, }), ]), ]); export type UngroupedSearchQueryResponse = rt.TypeOf; -export const GroupedSearchQueryResponseRT = rt.intersection([ +export const UnoptimizedGroupedSearchQueryResponseRT = rt.intersection([ commonSearchSuccessResponseFieldsRT, rt.type({ aggregations: rt.type({ @@ -272,33 +284,73 @@ export const GroupedSearchQueryResponseRT = rt.intersection([ rt.type({ buckets: rt.array( rt.type({ - key: rt.record(rt.string, rt.string), - doc_count: rt.number, + ...bucketFieldsRT.props, filtered_results: rt.intersection([ rt.type({ doc_count: rt.number, }), - // Chart preview buckets - rt.partial({ - histogramBuckets: rt.type({ - buckets: rt.array(chartPreviewHistogramBucket), - }), - }), + ChartPreviewBucketsRT, ]), }) ), }), - rt.partial({ - after_key: rt.record(rt.string, rt.string), - }), + afterKeyRT, ]), }), - hits: rt.type({ - total: rt.type({ - value: rt.number, - }), + hits: hitsRT, + }), +]); + +export type UnoptimizedGroupedSearchQueryResponse = rt.TypeOf< + typeof UnoptimizedGroupedSearchQueryResponseRT +>; + +export const OptimizedGroupedSearchQueryResponseRT = rt.intersection([ + commonSearchSuccessResponseFieldsRT, + rt.type({ + aggregations: rt.type({ + groups: rt.intersection([ + rt.type({ + buckets: rt.array(rt.intersection([bucketFieldsRT, ChartPreviewBucketsRT])), + }), + afterKeyRT, + ]), }), + hits: hitsRT, }), ]); +export type OptimizedGroupedSearchQueryResponse = rt.TypeOf< + typeof OptimizedGroupedSearchQueryResponseRT +>; + +export const GroupedSearchQueryResponseRT = rt.union([ + UnoptimizedGroupedSearchQueryResponseRT, + OptimizedGroupedSearchQueryResponseRT, +]); + export type GroupedSearchQueryResponse = rt.TypeOf; + +export const isOptimizedGroupedSearchQueryResponse = ( + response: GroupedSearchQueryResponse['aggregations']['groups']['buckets'] +): response is OptimizedGroupedSearchQueryResponse['aggregations']['groups']['buckets'] => { + const result = response[0]; + return result && !result.hasOwnProperty('filtered_results'); +}; + +export const isOptimizableGroupedThreshold = ( + selectedComparator: AlertParams['count']['comparator'], + selectedValue?: AlertParams['count']['value'] +) => { + if (selectedComparator === Comparator.GT) { + return true; + } else if ( + typeof selectedValue === 'number' && + selectedComparator === Comparator.GT_OR_EQ && + selectedValue > 0 + ) { + return true; + } else { + return false; + } +}; diff --git a/x-pack/plugins/infra/common/http_api/log_alerts/chart_preview_data.ts b/x-pack/plugins/infra/common/http_api/log_alerts/chart_preview_data.ts index e6baca305508e..5f488dd532285 100644 --- a/x-pack/plugins/infra/common/http_api/log_alerts/chart_preview_data.ts +++ b/x-pack/plugins/infra/common/http_api/log_alerts/chart_preview_data.ts @@ -7,6 +7,7 @@ import * as rt from 'io-ts'; import { + ThresholdRT, countCriteriaRT, timeUnitRT, timeSizeRT, @@ -58,6 +59,14 @@ export type GetLogAlertsChartPreviewDataSuccessResponsePayload = rt.TypeOf< export const getLogAlertsChartPreviewDataAlertParamsSubsetRT: any = rt.intersection([ rt.type({ criteria: countCriteriaRT, + count: rt.intersection([ + rt.type({ + comparator: ThresholdRT.props.comparator, + }), + rt.partial({ + value: ThresholdRT.props.value, + }), + ]), timeUnit: timeUnitRT, timeSize: timeSizeRT, }), diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criterion_preview_chart.tsx b/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criterion_preview_chart.tsx index 4e84cf0f9127c..4fa96ea6828d4 100644 --- a/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criterion_preview_chart.tsx +++ b/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criterion_preview_chart.tsx @@ -68,6 +68,10 @@ export const CriterionPreview: React.FC = ({ const criteria = field && comparator && value ? [{ field, comparator, value }] : []; const params = { criteria, + count: { + comparator: alertParams.count.comparator, + value: alertParams.count.value, + }, timeSize: alertParams.timeSize, timeUnit: alertParams.timeUnit, groupBy: alertParams.groupBy, @@ -78,7 +82,14 @@ export const CriterionPreview: React.FC = ({ } catch (error) { return null; } - }, [alertParams.timeSize, alertParams.timeUnit, alertParams.groupBy, chartCriterion]); + }, [ + alertParams.timeSize, + alertParams.timeUnit, + alertParams.groupBy, + alertParams.count.comparator, + alertParams.count.value, + chartCriterion, + ]); // Check for the existence of properties that are necessary for a meaningful chart. if (chartAlertParams === null || chartAlertParams.criteria.length === 0) return null; diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/editor.tsx b/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/editor.tsx index ef533f63dc175..4eb0f3e8645cf 100644 --- a/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/editor.tsx +++ b/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/editor.tsx @@ -23,6 +23,7 @@ import { PartialRatioAlertParams, ThresholdType, timeUnitRT, + isOptimizableGroupedThreshold, } from '../../../../../common/alerting/logs/log_threshold/types'; import { decodeOrThrow } from '../../../../../common/runtime_types'; import { ObjectEntries } from '../../../../../common/utility_types'; @@ -255,6 +256,15 @@ export const Editor: React.FC< setHasSetDefaults(true); }); + const shouldShowGroupByOptimizationWarning = useMemo(() => { + const hasSetGroupBy = alertParams.groupBy && alertParams.groupBy.length > 0; + return ( + hasSetGroupBy && + alertParams.count && + !isOptimizableGroupedThreshold(alertParams.count.comparator, alertParams.count.value) + ); + }, [alertParams]); + // Wait until the alert param defaults have been set if (!hasSetDefaults) return null; @@ -299,6 +309,21 @@ export const Editor: React.FC< {alertParams.criteria && isRatioAlert(alertParams.criteria) && criteriaComponent} + {shouldShowGroupByOptimizationWarning && ( + <> + + + {i18n.translate('xpack.infra.logs.alertFlyout.groupByOptimizationWarning', { + defaultMessage: + 'When setting a "group by" we highly recommend using the "{comparator}" comparator for your threshold. This can lead to significant performance improvements.', + values: { + comparator: Comparator.GT, + }, + })} + + + )} + ); diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_chart_preview.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_chart_preview.ts index 321273c656216..7bf2cb5ea3394 100644 --- a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_chart_preview.ts +++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_chart_preview.ts @@ -23,6 +23,7 @@ import { UngroupedSearchQueryResponse, GroupedSearchQueryResponse, GroupedSearchQueryResponseRT, + isOptimizedGroupedSearchQueryResponse, } from '../../../../common/alerting/logs/log_threshold/types'; import { decodeOrThrow } from '../../../../common/runtime_types'; import { ResolvedLogSourceConfiguration } from '../../../../common/log_sources'; @@ -97,10 +98,19 @@ const addHistogramAggregationToQuery = ( }; if (isGrouped) { - query.body.aggregations.groups.aggregations.filtered_results = { - ...query.body.aggregations.groups.aggregations.filtered_results, - aggregations: histogramAggregation, - }; + const isOptimizedQuery = !query.body.aggregations.groups.aggregations?.filtered_results; + + if (isOptimizedQuery) { + query.body.aggregations.groups.aggregations = { + ...query.body.aggregations.groups.aggregations, + ...histogramAggregation, + }; + } else { + query.body.aggregations.groups.aggregations.filtered_results = { + ...query.body.aggregations.groups.aggregations.filtered_results, + aggregations: histogramAggregation, + }; + } } else { query.body = { ...query.body, @@ -151,18 +161,34 @@ const getGroupedResults = async ( const processGroupedResults = ( results: GroupedSearchQueryResponse['aggregations']['groups']['buckets'] ): Series => { - return results.reduce((series, group) => { - if (!group.filtered_results.histogramBuckets) return series; - const groupName = Object.values(group.key).join(', '); - const points = group.filtered_results.histogramBuckets.buckets.reduce( - (pointsAcc, bucket) => { + const getGroupName = ( + key: GroupedSearchQueryResponse['aggregations']['groups']['buckets'][0]['key'] + ) => Object.values(key).join(', '); + + if (isOptimizedGroupedSearchQueryResponse(results)) { + return results.reduce((series, group) => { + if (!group.histogramBuckets) return series; + const groupName = getGroupName(group.key); + const points = group.histogramBuckets.buckets.reduce((pointsAcc, bucket) => { const { key, doc_count: count } = bucket; return [...pointsAcc, { timestamp: key, value: count }]; - }, - [] - ); - return [...series, { id: groupName, points }]; - }, []); + }, []); + return [...series, { id: groupName, points }]; + }, []); + } else { + return results.reduce((series, group) => { + if (!group.filtered_results.histogramBuckets) return series; + const groupName = getGroupName(group.key); + const points = group.filtered_results.histogramBuckets.buckets.reduce( + (pointsAcc, bucket) => { + const { key, doc_count: count } = bucket; + return [...pointsAcc, { timestamp: key, value: count }]; + }, + [] + ); + return [...series, { id: groupName, points }]; + }, []); + } }; const processUngroupedResults = (results: UngroupedSearchQueryResponse): Series => { diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.test.ts index ffabd7ba65f03..55c66f0aabbfb 100644 --- a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.test.ts +++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.test.ts @@ -58,6 +58,74 @@ const negativeCriteria: Criterion[] = [ { ...textField, comparator: Comparator.NOT_MATCH_PHRASE }, ]; +const expectedPositiveFilterClauses = [ + { + range: { + numericField: { + gt: 10, + }, + }, + }, + { + range: { + numericField: { + gte: 10, + }, + }, + }, + { + range: { + numericField: { + lt: 10, + }, + }, + }, + { + range: { + numericField: { + lte: 10, + }, + }, + }, + { + term: { + keywordField: { + value: 'error', + }, + }, + }, + { + match: { + textField: 'Something went wrong', + }, + }, + { + match_phrase: { + textField: 'Something went wrong', + }, + }, +]; + +const expectedNegativeFilterClauses = [ + { + term: { + keywordField: { + value: 'error', + }, + }, + }, + { + match: { + textField: 'Something went wrong', + }, + }, + { + match_phrase: { + textField: 'Something went wrong', + }, + }, +]; + const baseAlertParams: Pick = { count: { comparator: Comparator.GT, @@ -102,53 +170,7 @@ describe('Log threshold executor', () => { criteria: positiveCriteria, }; const filters = buildFiltersFromCriteria(alertParams, TIMESTAMP_FIELD); - expect(filters.mustFilters).toEqual([ - { - range: { - numericField: { - gt: 10, - }, - }, - }, - { - range: { - numericField: { - gte: 10, - }, - }, - }, - { - range: { - numericField: { - lt: 10, - }, - }, - }, - { - range: { - numericField: { - lte: 10, - }, - }, - }, - { - term: { - keywordField: { - value: 'error', - }, - }, - }, - { - match: { - textField: 'Something went wrong', - }, - }, - { - match_phrase: { - textField: 'Something went wrong', - }, - }, - ]); + expect(filters.mustFilters).toEqual(expectedPositiveFilterClauses); }); test('Handles negative criteria', () => { @@ -158,25 +180,7 @@ describe('Log threshold executor', () => { }; const filters = buildFiltersFromCriteria(alertParams, TIMESTAMP_FIELD); - expect(filters.mustNotFilters).toEqual([ - { - term: { - keywordField: { - value: 'error', - }, - }, - }, - { - match: { - textField: 'Something went wrong', - }, - }, - { - match_phrase: { - textField: 'Something went wrong', - }, - }, - ]); + expect(filters.mustNotFilters).toEqual(expectedNegativeFilterClauses); }); test('Handles time range', () => { @@ -194,7 +198,7 @@ describe('Log threshold executor', () => { describe('ES queries', () => { describe('Query generation', () => { - test('Correctly generates ungrouped queries', () => { + it('Correctly generates ungrouped queries', () => { const alertParams: AlertParams = { ...baseAlertParams, criteria: [...positiveCriteria, ...negativeCriteria], @@ -223,71 +227,9 @@ describe('Log threshold executor', () => { }, }, }, - { - range: { - numericField: { - gt: 10, - }, - }, - }, - { - range: { - numericField: { - gte: 10, - }, - }, - }, - { - range: { - numericField: { - lt: 10, - }, - }, - }, - { - range: { - numericField: { - lte: 10, - }, - }, - }, - { - term: { - keywordField: { - value: 'error', - }, - }, - }, - { - match: { - textField: 'Something went wrong', - }, - }, - { - match_phrase: { - textField: 'Something went wrong', - }, - }, - ], - must_not: [ - { - term: { - keywordField: { - value: 'error', - }, - }, - }, - { - match: { - textField: 'Something went wrong', - }, - }, - { - match_phrase: { - textField: 'Something went wrong', - }, - }, + ...expectedPositiveFilterClauses, ], + must_not: [...expectedNegativeFilterClauses], }, }, runtime_mappings: { @@ -304,148 +246,159 @@ describe('Log threshold executor', () => { }); }); - test('Correctly generates grouped queries', () => { - const alertParams: AlertParams = { - ...baseAlertParams, - groupBy: ['host.name'], - criteria: [...positiveCriteria, ...negativeCriteria], - }; - const query = getGroupedESQuery( - alertParams, - TIMESTAMP_FIELD, - FILEBEAT_INDEX, - runtimeMappings - ); - expect(query).toEqual({ - index: 'filebeat-*', - allow_no_indices: true, - ignore_unavailable: true, - body: { - query: { - bool: { - filter: [ - { - range: { - '@timestamp': { - gte: expect.any(Number), - lte: expect.any(Number), - format: 'epoch_millis', + describe('Correctly generates grouped queries', () => { + it('When using an optimizable threshold comparator', () => { + const alertParams: AlertParams = { + ...baseAlertParams, + groupBy: ['host.name'], + criteria: [...positiveCriteria, ...negativeCriteria], + }; + const query = getGroupedESQuery( + alertParams, + TIMESTAMP_FIELD, + FILEBEAT_INDEX, + runtimeMappings + ); + + expect(query).toEqual({ + index: 'filebeat-*', + allow_no_indices: true, + ignore_unavailable: true, + body: { + query: { + bool: { + filter: [ + { + range: { + '@timestamp': { + gte: expect.any(Number), + lte: expect.any(Number), + format: 'epoch_millis', + }, }, }, + ...expectedPositiveFilterClauses, + ], + must_not: [...expectedNegativeFilterClauses], + }, + }, + aggregations: { + groups: { + composite: { + size: 2000, + sources: [ + { + 'group-0-host.name': { + terms: { + field: 'host.name', + }, + }, + }, + ], }, - ], + }, + }, + runtime_mappings: { + runtime_field: { + type: 'keyword', + script: { + lang: 'painless', + source: 'emit("a runtime value")', + }, + }, }, + size: 0, }, - aggregations: { - groups: { - composite: { - size: 40, - sources: [ + }); + }); + + it('When not using an optimizable threshold comparator', () => { + const alertParams: AlertParams = { + ...baseAlertParams, + count: { + ...baseAlertParams.count, + comparator: Comparator.LT, + }, + groupBy: ['host.name'], + criteria: [...positiveCriteria, ...negativeCriteria], + }; + + const query = getGroupedESQuery( + alertParams, + TIMESTAMP_FIELD, + FILEBEAT_INDEX, + runtimeMappings + ); + + expect(query).toEqual({ + index: 'filebeat-*', + allow_no_indices: true, + ignore_unavailable: true, + body: { + query: { + bool: { + filter: [ { - 'group-0-host.name': { - terms: { - field: 'host.name', + range: { + '@timestamp': { + gte: expect.any(Number), + lte: expect.any(Number), + format: 'epoch_millis', }, }, }, ], }, - aggregations: { - filtered_results: { - filter: { - bool: { - filter: [ - { - range: { - '@timestamp': { - gte: expect.any(Number), - lte: expect.any(Number), - format: 'epoch_millis', - }, - }, - }, - { - range: { - numericField: { - gt: 10, - }, - }, - }, - { - range: { - numericField: { - gte: 10, - }, - }, - }, - { - range: { - numericField: { - lt: 10, - }, - }, - }, - { - range: { - numericField: { - lte: 10, - }, - }, - }, - { - term: { - keywordField: { - value: 'error', - }, - }, - }, - { - match: { - textField: 'Something went wrong', - }, - }, - { - match_phrase: { - textField: 'Something went wrong', - }, + }, + aggregations: { + groups: { + composite: { + size: 2000, + sources: [ + { + 'group-0-host.name': { + terms: { + field: 'host.name', }, - ], - must_not: [ - { - term: { - keywordField: { - value: 'error', + }, + }, + ], + }, + aggregations: { + filtered_results: { + filter: { + bool: { + filter: [ + { + range: { + '@timestamp': { + gte: expect.any(Number), + lte: expect.any(Number), + format: 'epoch_millis', + }, }, }, - }, - { - match: { - textField: 'Something went wrong', - }, - }, - { - match_phrase: { - textField: 'Something went wrong', - }, - }, - ], + ...expectedPositiveFilterClauses, + ], + must_not: [...expectedNegativeFilterClauses], + }, }, }, }, }, }, - }, - runtime_mappings: { - runtime_field: { - type: 'keyword', - script: { - lang: 'painless', - source: 'emit("a runtime value")', + runtime_mappings: { + runtime_field: { + type: 'keyword', + script: { + lang: 'painless', + source: 'emit("a runtime value")', + }, }, }, + size: 0, }, - size: 0, - }, + }); }); }); }); diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts index a537801202217..f9d0b5575abfc 100644 --- a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts @@ -36,6 +36,8 @@ import { CountCriteria, CountAlertParams, RatioAlertParams, + isOptimizedGroupedSearchQueryResponse, + isOptimizableGroupedThreshold, } from '../../../../common/alerting/logs/log_threshold/types'; import { InfraBackendLibs } from '../../infra_types'; import { getIntervalInSeconds } from '../../../utils/get_interval_in_seconds'; @@ -57,7 +59,7 @@ type LogThresholdAlertExecutorOptions = AlertExecutorOptions< LogThresholdActionGroups >; -const COMPOSITE_GROUP_SIZE = 40; +const COMPOSITE_GROUP_SIZE = 2000; const checkValueAgainstComparatorMap: { [key: string]: (a: number, b: number) => boolean; @@ -68,6 +70,10 @@ const checkValueAgainstComparatorMap: { [Comparator.LT_OR_EQ]: (a: number, b: number) => a <= b, }; +// The executor execution roughly follows a pattern of: +// ES Query generation -> fetching of results -> processing of results. +// With forks for group_by vs ungrouped, and ratio vs non-ratio. + export const createLogThresholdExecutor = (libs: InfraBackendLibs) => async function ({ services, params }: LogThresholdAlertExecutorOptions) { const { alertInstanceFactory, savedObjectsClient, scopedClusterClient } = services; @@ -277,11 +283,26 @@ type ReducedGroupByResults = ReducedGroupByResult[]; const getReducedGroupByResults = ( results: GroupedSearchQueryResponse['aggregations']['groups']['buckets'] ): ReducedGroupByResults => { - return results.reduce((acc, groupBucket) => { - const groupName = Object.values(groupBucket.key).join(', '); - const groupResult = { name: groupName, documentCount: groupBucket.filtered_results.doc_count }; - return [...acc, groupResult]; - }, []); + const getGroupName = ( + key: GroupedSearchQueryResponse['aggregations']['groups']['buckets'][0]['key'] + ) => Object.values(key).join(', '); + + if (isOptimizedGroupedSearchQueryResponse(results)) { + return results.reduce((acc, groupBucket) => { + const groupName = getGroupName(groupBucket.key); + const groupResult = { name: groupName, documentCount: groupBucket.doc_count }; + return [...acc, groupResult]; + }, []); + } else { + return results.reduce((acc, groupBucket) => { + const groupName = getGroupName(groupBucket.key); + const groupResult = { + name: groupName, + documentCount: groupBucket.filtered_results.doc_count, + }; + return [...acc, groupResult]; + }, []); + } }; export const processGroupByResults = ( @@ -430,12 +451,29 @@ export const buildFiltersFromCriteria = ( }; export const getGroupedESQuery = ( - params: Pick & { criteria: CountCriteria }, + params: Pick & { + criteria: CountCriteria; + count: { + comparator: AlertParams['count']['comparator']; + value?: AlertParams['count']['value']; + }; + }, timestampField: string, index: string, runtimeMappings: estypes.MappingRuntimeFields ): estypes.SearchRequest | undefined => { - const { groupBy } = params; + // IMPORTANT: + // For the group by scenario we need to account for users utilizing "less than" configurations + // to attempt to match on "0", e.g. something has stopped reporting. We need to cast a wider net for these + // configurations to try and capture more documents, so that the filtering doesn't make the group "disappear". + // Due to this there are two forks in the group by code, one where we can optimize the filtering early, and one where + // it is an inner aggregation. "Less than" configurations with high cardinality group by fields can cause severe performance + // problems. + + const { + groupBy, + count: { comparator, value }, + } = params; if (!groupBy || !groupBy.length) { return; @@ -446,47 +484,81 @@ export const getGroupedESQuery = ( timestampField ); - const aggregations = { - groups: { - composite: { - size: COMPOSITE_GROUP_SIZE, - sources: groupBy.map((field, groupIndex) => ({ - [`group-${groupIndex}-${field}`]: { - terms: { field }, - }, - })), + if (isOptimizableGroupedThreshold(comparator, value)) { + const aggregations = { + groups: { + composite: { + size: COMPOSITE_GROUP_SIZE, + sources: groupBy.map((field, groupIndex) => ({ + [`group-${groupIndex}-${field}`]: { + terms: { field }, + }, + })), + }, }, - aggregations: { - filtered_results: { - filter: { - bool: { - // Scope the inner filtering back to the unpadded range - filter: [rangeFilter, ...mustFilters], - ...(mustNotFilters.length > 0 && { must_not: mustNotFilters }), + }; + + const body: estypes.SearchRequest['body'] = { + query: { + bool: { + filter: [rangeFilter, ...mustFilters], + ...(mustNotFilters.length > 0 && { must_not: mustNotFilters }), + }, + }, + aggregations, + runtime_mappings: runtimeMappings, + size: 0, + }; + + return { + index, + allow_no_indices: true, + ignore_unavailable: true, + body, + }; + } else { + const aggregations = { + groups: { + composite: { + size: COMPOSITE_GROUP_SIZE, + sources: groupBy.map((field, groupIndex) => ({ + [`group-${groupIndex}-${field}`]: { + terms: { field }, + }, + })), + }, + aggregations: { + filtered_results: { + filter: { + bool: { + // Scope the inner filtering back to the unpadded range + filter: [rangeFilter, ...mustFilters], + ...(mustNotFilters.length > 0 && { must_not: mustNotFilters }), + }, }, }, }, }, - }, - }; + }; - const body: estypes.SearchRequest['body'] = { - query: { - bool: { - filter: [groupedRangeFilter], + const body: estypes.SearchRequest['body'] = { + query: { + bool: { + filter: [groupedRangeFilter], + }, }, - }, - aggregations, - runtime_mappings: runtimeMappings, - size: 0, - }; + aggregations, + runtime_mappings: runtimeMappings, + size: 0, + }; - return { - index, - allow_no_indices: true, - ignore_unavailable: true, - body, - }; + return { + index, + allow_no_indices: true, + ignore_unavailable: true, + body, + }; + } }; export const getUngroupedESQuery = ( From fb3e8f4498680d1d3bc52990572e6d0f0c00c7c2 Mon Sep 17 00:00:00 2001 From: Constance Date: Thu, 24 Jun 2021 12:43:26 -0700 Subject: [PATCH 50/69] [Enterprise Search] Product 404 polish pass (#103198) * Refactor NotFound component - shared NotFound becomes NotFoundPrompt - returns only an EuiEmptyPrompt, and individual products/plugins are in charge of their own layout, rather than NotFound doing a bunch of arduous switch handling (also closer to how errorConnecting is a component set per-plugin) - This is both due to the recent page template refactor and the fact that WS has extra complex logic of needing to switch between its kibana layout and personal dashboard layout - logos are still hosted in shared/ since they need extra custom CSS to work correctly sizing wise and in dark mode. I renamed its folder from `assets`->`logos` for extra clarity * [AS] Update current AS routers using NotFound + update EngineRouter to use NotFound * [WS] Update app router - Handle errorConnecting at the topmost level, instead of in WorkplaceSearchConfigured (to simplify various logic/expectations & match App Search) - Simplify isOrganization check to use `useRouteMatch` instead of a regex - Use new NotFound component - Add NotFound component for the personal dashboard router * [WS] Improve Source 404 UX - Add NotFound to SourceRouter + add breadcrumbs for organization views - When an actual source ID 404s, fix blanket redirect to a dashboard aware redirect - personal dashboard 404s should send the user back to personal sources, not organization sources + add a flash message error (similar to how App Search behaves for engine 404s) + harden error status checks (gracefully allow for non-http errors to fall back flashAPIErrors * [WS] Improve Settings 404 UX - This was the only remaining WS route I found that either did not have a 404 or a fallback to some overview page, so I tweaked the redirect order for a graceful redirect (vs a blank page) * Fix settings router test * Move away from custom product logos to OOTB Enterprise Search logo Keeping it simple, etc. RIP in peace fancy logos * [PR feedback] toContain over stringContaining --- .../components/analytics/analytics_router.tsx | 8 +- .../components/engine/engine_router.tsx | 6 +- .../components/not_found/index.ts} | 16 +-- .../components/not_found/not_found.test.tsx | 38 ++++++ .../components/not_found/not_found.tsx | 23 ++++ .../applications/app_search/index.test.tsx | 17 +-- .../public/applications/app_search/index.tsx | 13 +- .../not_found/assets/app_search_logo.tsx | 33 ----- .../assets/workplace_search_logo.tsx | 39 ------ .../applications/shared/not_found/index.ts | 2 +- .../shared/not_found/not_found.test.tsx | 70 ----------- .../shared/not_found/not_found.tsx | 117 ------------------ .../not_found/not_found_prompt.test.tsx | 51 ++++++++ .../shared/not_found/not_found_prompt.tsx | 65 ++++++++++ .../workplace_search/index.test.tsx | 32 ++--- .../applications/workplace_search/index.tsx | 38 +++--- .../content_sources/source_logic.test.ts | 80 ++++++------ .../views/content_sources/source_logic.ts | 12 +- .../content_sources/source_router.test.tsx | 4 +- .../views/content_sources/source_router.tsx | 6 +- .../workplace_search/views/not_found/index.ts | 8 ++ .../views/not_found/not_found.test.tsx | 52 ++++++++ .../views/not_found/not_found.tsx | 33 +++++ .../views/settings/settings_router.test.tsx | 4 +- .../views/settings/settings_router.tsx | 5 +- 25 files changed, 378 insertions(+), 394 deletions(-) rename x-pack/plugins/enterprise_search/public/applications/{shared/not_found/assets/logo.scss => app_search/components/not_found/index.ts} (53%) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/not_found/not_found.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/not_found/not_found.tsx delete mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/not_found/assets/app_search_logo.tsx delete mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/not_found/assets/workplace_search_logo.tsx delete mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.test.tsx delete mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found_prompt.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found_prompt.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/not_found/index.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/not_found/not_found.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/not_found/not_found.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_router.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_router.tsx index d56fe949431c3..2ed06d68301c9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_router.tsx @@ -8,8 +8,6 @@ import React from 'react'; import { Route, Switch, Redirect } from 'react-router-dom'; -import { APP_SEARCH_PLUGIN } from '../../../../../common/constants'; -import { NotFound } from '../../../shared/not_found'; import { ENGINE_ANALYTICS_PATH, ENGINE_ANALYTICS_TOP_QUERIES_PATH, @@ -21,6 +19,7 @@ import { ENGINE_ANALYTICS_QUERY_DETAIL_PATH, } from '../../routes'; import { generateEnginePath, getEngineBreadcrumbs } from '../engine'; +import { NotFound } from '../not_found'; import { ANALYTICS_TITLE } from './constants'; import { @@ -61,10 +60,7 @@ export const AnalyticsRouter: React.FC = () => { - + ); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx index da8dd8467bb61..2d1bd32a0fff5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx @@ -38,6 +38,7 @@ import { CurationsRouter } from '../curations'; import { DocumentDetail, Documents } from '../documents'; import { EngineOverview } from '../engine_overview'; import { AppSearchPageTemplate } from '../layout'; +import { NotFound } from '../not_found'; import { RelevanceTuning } from '../relevance_tuning'; import { ResultSettings } from '../result_settings'; import { SchemaRouter } from '../schema'; @@ -45,7 +46,7 @@ import { SearchUI } from '../search_ui'; import { SourceEngines } from '../source_engines'; import { Synonyms } from '../synonyms'; -import { EngineLogic } from './'; +import { EngineLogic, getEngineBreadcrumbs } from './'; export const EngineRouter: React.FC = () => { const { @@ -159,6 +160,9 @@ export const EngineRouter: React.FC = () => { )} + + + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/assets/logo.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/not_found/index.ts similarity index 53% rename from x-pack/plugins/enterprise_search/public/applications/shared/not_found/assets/logo.scss rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/not_found/index.ts index b157f55cbba68..482c1a58faa9c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/assets/logo.scss +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/not_found/index.ts @@ -5,18 +5,4 @@ * 2.0. */ -.logo404 { - width: $euiSize * 8; - height: $euiSize * 8; - - fill: $euiColorEmptyShade; - stroke: $euiColorLightShade; - - &__light { - fill: $euiColorLightShade; - } - - &__dark { - fill: $euiColorMediumShade; - } -} +export { NotFound } from './not_found'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/not_found/not_found.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/not_found/not_found.test.tsx new file mode 100644 index 0000000000000..6fed726eb5e0b --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/not_found/not_found.test.tsx @@ -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 React from 'react'; + +import { shallow } from 'enzyme'; + +import { NotFoundPrompt } from '../../../shared/not_found'; +import { SendAppSearchTelemetry } from '../../../shared/telemetry'; +import { AppSearchPageTemplate } from '../layout'; + +import { NotFound } from './'; + +describe('NotFound', () => { + const wrapper = shallow(); + + it('renders the shared not found prompt', () => { + expect(wrapper.find(NotFoundPrompt)).toHaveLength(1); + }); + + it('renders a telemetry error event', () => { + expect(wrapper.find(SendAppSearchTelemetry).prop('action')).toEqual('error'); + }); + + it('passes optional preceding page chrome', () => { + wrapper.setProps({ pageChrome: ['Engines', 'some-engine'] }); + + expect(wrapper.find(AppSearchPageTemplate).prop('pageChrome')).toEqual([ + 'Engines', + 'some-engine', + '404', + ]); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/not_found/not_found.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/not_found/not_found.tsx new file mode 100644 index 0000000000000..f6165fa192d57 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/not_found/not_found.tsx @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { APP_SEARCH_PLUGIN } from '../../../../../common/constants'; +import { PageTemplateProps } from '../../../shared/layout'; +import { NotFoundPrompt } from '../../../shared/not_found'; +import { SendAppSearchTelemetry } from '../../../shared/telemetry'; +import { AppSearchPageTemplate } from '../layout'; + +export const NotFound: React.FC = ({ pageChrome = [] }) => { + return ( + + + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx index 00acea945177a..46596cc5d6765 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx @@ -17,7 +17,7 @@ import { Redirect } from 'react-router-dom'; import { shallow, ShallowWrapper } from 'enzyme'; -import { Layout, SideNav, SideNavLink } from '../shared/layout'; +import { SideNav, SideNavLink } from '../shared/layout'; import { rerender } from '../test_helpers'; @@ -83,13 +83,6 @@ describe('AppSearchConfigured', () => { wrapper = shallow(); }); - it('renders with layout', () => { - expect(wrapper.find(Layout)).toHaveLength(1); - expect(wrapper.find(Layout).prop('readOnlyMode')).toBeFalsy(); - expect(wrapper.find(EnginesOverview)).toHaveLength(1); - expect(wrapper.find(EngineRouter)).toHaveLength(1); - }); - it('renders header actions', () => { expect(renderHeaderActions).toHaveBeenCalled(); }); @@ -98,11 +91,9 @@ describe('AppSearchConfigured', () => { expect(AppLogic).toHaveBeenCalledWith(DEFAULT_INITIAL_APP_DATA); }); - it('passes readOnlyMode state', () => { - setMockValues({ myRole: {}, readOnlyMode: true }); - rerender(wrapper); - - expect(wrapper.find(Layout).first().prop('readOnlyMode')).toEqual(true); + it('renders engine routes', () => { + expect(wrapper.find(EnginesOverview)).toHaveLength(1); + expect(wrapper.find(EngineRouter)).toHaveLength(1); }); describe('routes with ability checks', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx index d7ddad5683f38..6d049b2015487 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx @@ -14,8 +14,7 @@ import { APP_SEARCH_PLUGIN } from '../../../common/constants'; import { InitialAppData } from '../../../common/types'; import { HttpLogic } from '../shared/http'; import { KibanaLogic } from '../shared/kibana'; -import { Layout, SideNav, SideNavLink } from '../shared/layout'; -import { NotFound } from '../shared/not_found'; +import { SideNav, SideNavLink } from '../shared/layout'; import { ROLE_MAPPINGS_TITLE } from '../shared/role_mapping/constants'; @@ -28,6 +27,7 @@ import { ErrorConnecting } from './components/error_connecting'; import { KibanaHeaderActions } from './components/layout'; import { Library } from './components/library'; import { MetaEngineCreation } from './components/meta_engine_creation'; +import { NotFound } from './components/not_found'; import { RoleMappings } from './components/role_mappings'; import { Settings, SETTINGS_TITLE } from './components/settings'; import { SetupGuide } from './components/setup_guide'; @@ -85,7 +85,6 @@ export const AppSearchConfigured: React.FC> = (props) = }, } = useValues(AppLogic(props)); const { renderHeaderActions } = useValues(KibanaLogic); - const { readOnlyMode } = useValues(HttpLogic); useEffect(() => { renderHeaderActions(KibanaHeaderActions); @@ -133,13 +132,7 @@ export const AppSearchConfigured: React.FC> = (props) = )} - } readOnlyMode={readOnlyMode}> - - - - - - + ); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/assets/app_search_logo.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/assets/app_search_logo.tsx deleted file mode 100644 index 8eb2059afd3ed..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/assets/app_search_logo.tsx +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; - -export const AppSearchLogo: React.FC = () => ( - -); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/assets/workplace_search_logo.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/assets/workplace_search_logo.tsx deleted file mode 100644 index df5b1a1118c41..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/assets/workplace_search_logo.tsx +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; - -export const WorkplaceSearchLogo: React.FC = () => ( - -); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/index.ts index 482c1a58faa9c..8be374d549952 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export { NotFound } from './not_found'; +export { NotFoundPrompt } from './not_found_prompt'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.test.tsx deleted file mode 100644 index 1561224a26e42..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.test.tsx +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { setMockValues } from '../../__mocks__/kea_logic'; - -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { EuiButton as EuiButtonExternal, EuiEmptyPrompt } from '@elastic/eui'; - -import { APP_SEARCH_PLUGIN, WORKPLACE_SEARCH_PLUGIN } from '../../../../common/constants'; -import { SetAppSearchChrome } from '../kibana_chrome'; - -import { AppSearchLogo } from './assets/app_search_logo'; -import { WorkplaceSearchLogo } from './assets/workplace_search_logo'; - -import { NotFound } from './'; - -describe('NotFound', () => { - it('renders an App Search 404 view', () => { - const wrapper = shallow(); - const prompt = wrapper.find(EuiEmptyPrompt).dive().shallow(); - - expect(prompt.find('h2').text()).toEqual('404 error'); - expect(prompt.find(EuiButtonExternal).prop('href')).toEqual(APP_SEARCH_PLUGIN.SUPPORT_URL); - - const logo = prompt.find(AppSearchLogo).dive().shallow(); - expect(logo.type()).toEqual('svg'); - }); - - it('renders a Workplace Search 404 view', () => { - const wrapper = shallow(); - const prompt = wrapper.find(EuiEmptyPrompt).dive().shallow(); - - expect(prompt.find('h2').text()).toEqual('404 error'); - expect(prompt.find(EuiButtonExternal).prop('href')).toEqual( - WORKPLACE_SEARCH_PLUGIN.SUPPORT_URL - ); - - const logo = prompt.find(WorkplaceSearchLogo).dive().shallow(); - expect(logo.type()).toEqual('svg'); - }); - - it('changes the support URL if the user has a gold+ license', () => { - setMockValues({ hasGoldLicense: true }); - const wrapper = shallow(); - const prompt = wrapper.find(EuiEmptyPrompt).dive().shallow(); - - expect(prompt.find(EuiButtonExternal).prop('href')).toEqual('https://support.elastic.co'); - }); - - it('passes down optional custom breadcrumbs', () => { - const wrapper = shallow( - - ); - - expect(wrapper.find(SetAppSearchChrome).prop('trail')).toEqual(['Hello', 'World']); - }); - - it('does not render anything without a valid product', () => { - const wrapper = shallow(); - - expect(wrapper.isEmptyRender()).toBe(true); - }); -}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.tsx deleted file mode 100644 index f288961b72de4..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.tsx +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; - -import { useValues } from 'kea'; - -import { - EuiPageContent, - EuiEmptyPrompt, - EuiTitle, - EuiFlexGroup, - EuiFlexItem, - EuiButton, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; - -import { - APP_SEARCH_PLUGIN, - WORKPLACE_SEARCH_PLUGIN, - LICENSED_SUPPORT_URL, -} from '../../../../common/constants'; - -import { SetAppSearchChrome, SetWorkplaceSearchChrome } from '../kibana_chrome'; -import { BreadcrumbTrail } from '../kibana_chrome/generate_breadcrumbs'; -import { LicensingLogic } from '../licensing'; -import { EuiButtonTo } from '../react_router_helpers'; -import { SendAppSearchTelemetry, SendWorkplaceSearchTelemetry } from '../telemetry'; - -import { AppSearchLogo } from './assets/app_search_logo'; -import { WorkplaceSearchLogo } from './assets/workplace_search_logo'; -import './assets/logo.scss'; - -interface NotFoundProps { - // Expects product plugin constants (@see common/constants.ts) - product: { - ID: string; - SUPPORT_URL: string; - }; - // Optional breadcrumbs - breadcrumbs?: BreadcrumbTrail; -} - -export const NotFound: React.FC = ({ product = {}, breadcrumbs }) => { - const { hasGoldLicense } = useValues(LicensingLogic); - const supportUrl = hasGoldLicense ? LICENSED_SUPPORT_URL : product.SUPPORT_URL; - - let Logo; - let SetPageChrome; - let SendTelemetry; - - switch (product.ID) { - case APP_SEARCH_PLUGIN.ID: - Logo = AppSearchLogo; - SetPageChrome = SetAppSearchChrome; - SendTelemetry = SendAppSearchTelemetry; - break; - case WORKPLACE_SEARCH_PLUGIN.ID: - Logo = WorkplaceSearchLogo; - SetPageChrome = SetWorkplaceSearchChrome; - SendTelemetry = SendWorkplaceSearchTelemetry; - break; - default: - return null; - } - - return ( - <> - - - - - } - body={ - <> - -

- {i18n.translate('xpack.enterpriseSearch.notFound.title', { - defaultMessage: '404 error', - })} -

-
-

- {i18n.translate('xpack.enterpriseSearch.notFound.description', { - defaultMessage: 'The page you’re looking for was not found.', - })} -

- - } - actions={ - - - - {i18n.translate('xpack.enterpriseSearch.notFound.action1', { - defaultMessage: 'Back to your dashboard', - })} - - - - - {i18n.translate('xpack.enterpriseSearch.notFound.action2', { - defaultMessage: 'Contact support', - })} - - - - } - /> -
- - ); -}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found_prompt.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found_prompt.test.tsx new file mode 100644 index 0000000000000..c21aeff2780b6 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found_prompt.test.tsx @@ -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 { setMockValues } from '../../__mocks__/kea_logic'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiEmptyPrompt, EuiButton } from '@elastic/eui'; + +import { EuiButtonTo } from '../react_router_helpers'; + +import { NotFoundPrompt } from './'; + +describe('NotFoundPrompt', () => { + const subject = (props?: object) => + shallow() + .find(EuiEmptyPrompt) + .dive(); + + it('renders', () => { + const wrapper = subject({ + productSupportUrl: 'https://discuss.elastic.co/c/enterprise-search/app-search/', + }); + + expect(wrapper.find('h1').text()).toEqual('404 error'); + expect(wrapper.find(EuiButtonTo).prop('to')).toEqual('/'); + expect(wrapper.find(EuiButton).prop('href')).toContain('https://discuss.elastic.co'); + }); + + it('renders with a custom "Back to dashboard" link if passed', () => { + const wrapper = subject({ + productSupportUrl: 'https://discuss.elastic.co/c/enterprise-search/workplace-search/', + backToLink: '/workplace_search/p/sources', + }); + + expect(wrapper.find(EuiButtonTo).prop('to')).toEqual('/workplace_search/p/sources'); + }); + + it('renders with a link to our licensed support URL for gold+ licenses', () => { + setMockValues({ hasGoldLicense: true }); + const wrapper = subject(); + + expect(wrapper.find(EuiButton).prop('href')).toEqual('https://support.elastic.co'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found_prompt.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found_prompt.tsx new file mode 100644 index 0000000000000..97debd21ec16c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found_prompt.tsx @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useValues } from 'kea'; + +import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { LICENSED_SUPPORT_URL } from '../../../../common/constants'; +import { LicensingLogic } from '../licensing'; +import { EuiButtonTo } from '../react_router_helpers'; + +interface Props { + productSupportUrl: string; + backToLink?: string; +} + +export const NotFoundPrompt: React.FC = ({ productSupportUrl, backToLink = '/' }) => { + const { hasGoldLicense } = useValues(LicensingLogic); + const supportUrl = hasGoldLicense ? LICENSED_SUPPORT_URL : productSupportUrl; + + return ( + + {i18n.translate('xpack.enterpriseSearch.notFound.title', { + defaultMessage: '404 error', + })} + + } + body={ +

+ {i18n.translate('xpack.enterpriseSearch.notFound.description', { + defaultMessage: 'The page you’re looking for was not found.', + })} +

+ } + actions={ + + + + {i18n.translate('xpack.enterpriseSearch.notFound.action1', { + defaultMessage: 'Back to your dashboard', + })} + + + + + {i18n.translate('xpack.enterpriseSearch.notFound.action2', { + defaultMessage: 'Contact support', + })} + + + + } + /> + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx index 28169afd4bdeb..2743dfc794ec6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx @@ -5,17 +5,15 @@ * 2.0. */ -import '../__mocks__/react_router'; import '../__mocks__/shallow_useeffect.mock'; import { setMockValues, setMockActions, mockKibanaValues } from '../__mocks__/kea_logic'; +import { mockUseRouteMatch } from '../__mocks__/react_router'; import React from 'react'; import { Redirect } from 'react-router-dom'; import { shallow } from 'enzyme'; -import { Layout } from '../shared/layout'; - import { WorkplaceSearchHeaderActions } from './components/layout'; import { SourceAdded } from './views/content_sources/components/source_added'; import { ErrorState } from './views/error_state'; @@ -38,6 +36,14 @@ describe('WorkplaceSearch', () => { expect(wrapper.find(WorkplaceSearchConfigured)).toHaveLength(1); }); + + it('renders ErrorState', () => { + setMockValues({ errorConnecting: true }); + + const wrapper = shallow(); + + expect(wrapper.find(ErrorState)).toHaveLength(1); + }); }); describe('WorkplaceSearchUnconfigured', () => { @@ -56,12 +62,12 @@ describe('WorkplaceSearchConfigured', () => { beforeEach(() => { jest.clearAllMocks(); setMockActions({ initializeAppData, setContext }); + mockUseRouteMatch.mockReturnValue(false); }); - it('renders layout, chrome, and header actions', () => { + it('renders chrome and header actions', () => { const wrapper = shallow(); - expect(wrapper.find(Layout).first().prop('readOnlyMode')).toBeFalsy(); expect(wrapper.find(Overview)).toHaveLength(1); expect(mockKibanaValues.setChromeIsVisible).toHaveBeenCalledWith(true); @@ -83,22 +89,6 @@ describe('WorkplaceSearchConfigured', () => { expect(mockKibanaValues.renderHeaderActions).not.toHaveBeenCalled(); }); - it('renders ErrorState', () => { - setMockValues({ errorConnecting: true }); - - const wrapper = shallow(); - - expect(wrapper.find(ErrorState)).toHaveLength(1); - }); - - it('passes readOnlyMode state', () => { - setMockValues({ readOnlyMode: true }); - - const wrapper = shallow(); - - expect(wrapper.find(Layout).first().prop('readOnlyMode')).toEqual(true); - }); - it('renders SourceAdded', () => { const wrapper = shallow(); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx index 05018be2934b4..2daf677962163 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx @@ -6,19 +6,16 @@ */ import React, { useEffect } from 'react'; -import { Route, Redirect, Switch, useLocation } from 'react-router-dom'; +import { Route, Redirect, Switch, useRouteMatch } from 'react-router-dom'; import { useActions, useValues } from 'kea'; -import { WORKPLACE_SEARCH_PLUGIN } from '../../../common/constants'; import { InitialAppData } from '../../../common/types'; import { HttpLogic } from '../shared/http'; import { KibanaLogic } from '../shared/kibana'; -import { Layout } from '../shared/layout'; -import { NotFound } from '../shared/not_found'; import { AppLogic } from './app_logic'; -import { WorkplaceSearchNav, WorkplaceSearchHeaderActions } from './components/layout'; +import { WorkplaceSearchHeaderActions } from './components/layout'; import { GROUPS_PATH, SETUP_GUIDE_PATH, @@ -36,6 +33,7 @@ import { SourcesRouter } from './views/content_sources'; import { SourceAdded } from './views/content_sources/components/source_added'; import { ErrorState } from './views/error_state'; import { GroupsRouter } from './views/groups'; +import { NotFound } from './views/not_found'; import { Overview } from './views/overview'; import { RoleMappings } from './views/role_mappings'; import { Security } from './views/security'; @@ -44,30 +42,33 @@ import { SetupGuide } from './views/setup_guide'; export const WorkplaceSearch: React.FC = (props) => { const { config } = useValues(KibanaLogic); - return !config.host ? : ; + const { errorConnecting } = useValues(HttpLogic); + return !config.host ? ( + + ) : errorConnecting ? ( + + ) : ( + + ); }; export const WorkplaceSearchConfigured: React.FC = (props) => { const { hasInitialized } = useValues(AppLogic); const { initializeAppData, setContext } = useActions(AppLogic); const { renderHeaderActions, setChromeIsVisible } = useValues(KibanaLogic); - const { errorConnecting, readOnlyMode } = useValues(HttpLogic); - - const { pathname } = useLocation(); /** * Personal dashboard urls begin with /p/ * EX: http://localhost:5601/app/enterprise_search/workplace_search/p/sources */ - const personalSourceUrlRegex = /^\/p\//g; // matches '/p/*' - const isOrganization = !pathname.match(personalSourceUrlRegex); // TODO: Once auth is figured out, we need to have a check for the equivilent of `isAdmin`. + const isOrganization = !useRouteMatch(PERSONAL_PATH); // TODO: Once auth is figured out, we need to have a check for the equivalent of `isAdmin`. setContext(isOrganization); useEffect(() => { setChromeIsVisible(isOrganization); - }, [pathname]); + }, [isOrganization]); useEffect(() => { if (!hasInitialized) { @@ -95,6 +96,9 @@ export const WorkplaceSearchConfigured: React.FC = (props) => { + + + @@ -113,15 +117,7 @@ export const WorkplaceSearchConfigured: React.FC = (props) => { - } restrictWidth readOnlyMode={readOnlyMode}> - {errorConnecting ? ( - - ) : ( - - - - )} - + ); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.test.ts index 03f46830fafc3..2aed64af53f16 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.test.ts @@ -22,8 +22,6 @@ jest.mock('../../app_logic', () => ({ })); import { AppLogic } from '../../app_logic'; -import { NOT_FOUND_PATH } from '../../routes'; - import { SourceLogic } from './source_logic'; describe('SourceLogic', () => { @@ -176,47 +174,55 @@ describe('SourceLogic', () => { expect(initializeFederatedSummarySpy).toHaveBeenCalledWith(contentSource.id); }); - it('handles error', async () => { - const error = { - response: { - error: 'this is an error', - status: 400, - }, - }; - const promise = Promise.reject(error); - http.get.mockReturnValue(promise); - SourceLogic.actions.initializeSource(contentSource.id); - await expectedAsyncError(promise); + describe('errors', () => { + it('handles generic errors', async () => { + const mockError = Promise.reject('error'); + http.get.mockReturnValue(mockError); - expect(flashAPIErrors).toHaveBeenCalledWith(error); - }); + SourceLogic.actions.initializeSource(contentSource.id); + await expectedAsyncError(mockError); - it('handles not found state', async () => { - const error = { - response: { - error: 'this is an error', - status: 404, - }, - }; - const promise = Promise.reject(error); - http.get.mockReturnValue(promise); - SourceLogic.actions.initializeSource(contentSource.id); - await expectedAsyncError(promise); + expect(flashAPIErrors).toHaveBeenCalledWith('error'); + }); - expect(navigateToUrl).toHaveBeenCalledWith(NOT_FOUND_PATH); - }); + describe('404s', () => { + const mock404 = Promise.reject({ response: { status: 404 } }); - it('renders error messages passed in success response from server', async () => { - const errors = ['ERROR']; - const promise = Promise.resolve({ - ...contentSource, - errors, + it('redirects to the organization sources page on organization views', async () => { + AppLogic.values.isOrganization = true; + http.get.mockReturnValue(mock404); + + SourceLogic.actions.initializeSource('404ing_org_source'); + await expectedAsyncError(mock404); + + expect(navigateToUrl).toHaveBeenCalledWith('/sources'); + expect(setErrorMessage).toHaveBeenCalledWith('Source not found.'); + }); + + it('redirects to the personal dashboard sources page on personal views', async () => { + AppLogic.values.isOrganization = false; + http.get.mockReturnValue(mock404); + + SourceLogic.actions.initializeSource('404ing_personal_source'); + await expectedAsyncError(mock404); + + expect(navigateToUrl).toHaveBeenCalledWith('/p/sources'); + expect(setErrorMessage).toHaveBeenCalledWith('Source not found.'); + }); }); - http.get.mockReturnValue(promise); - SourceLogic.actions.initializeSource(contentSource.id); - await promise; - expect(setErrorMessage).toHaveBeenCalledWith(errors); + it('renders error messages passed in success response from server', async () => { + const errors = ['ERROR']; + const promise = Promise.resolve({ + ...contentSource, + errors, + }); + http.get.mockReturnValue(promise); + SourceLogic.actions.initializeSource(contentSource.id); + await promise; + + expect(setErrorMessage).toHaveBeenCalledWith(errors); + }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts index 2e6a3c65597ea..0fd44e01ae495 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts @@ -20,7 +20,7 @@ import { import { HttpLogic } from '../../../shared/http'; import { KibanaLogic } from '../../../shared/kibana'; import { AppLogic } from '../../app_logic'; -import { NOT_FOUND_PATH, SOURCES_PATH, getSourcesPath } from '../../routes'; +import { PERSONAL_SOURCES_PATH, SOURCES_PATH, getSourcesPath } from '../../routes'; import { ContentSourceFullData, Meta, DocumentSummaryItem, SourceContentItem } from '../../types'; export interface SourceActions { @@ -155,8 +155,14 @@ export const SourceLogic = kea>({ clearFlashMessages(); } } catch (e) { - if (e.response.status === 404) { - KibanaLogic.values.navigateToUrl(NOT_FOUND_PATH); + if (e?.response?.status === 404) { + const redirect = isOrganization ? SOURCES_PATH : PERSONAL_SOURCES_PATH; + KibanaLogic.values.navigateToUrl(redirect); + setErrorMessage( + i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.notFoundErrorMessage', { + defaultMessage: 'Source not found.', + }) + ); } else { flashAPIErrors(e); } diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.test.tsx index afe0d1f89faea..fbc8eb159a7a8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.test.tsx @@ -90,7 +90,7 @@ describe('SourceRouter', () => { expect(wrapper.find(Overview)).toHaveLength(1); expect(wrapper.find(SourceSettings)).toHaveLength(1); expect(wrapper.find(SourceContent)).toHaveLength(1); - expect(wrapper.find(Route)).toHaveLength(3); + expect(wrapper.find(Route)).toHaveLength(4); }); it('renders source routes (custom)', () => { @@ -100,6 +100,6 @@ describe('SourceRouter', () => { expect(wrapper.find(DisplaySettingsRouter)).toHaveLength(1); expect(wrapper.find(Schema)).toHaveLength(1); expect(wrapper.find(SchemaChangeErrors)).toHaveLength(1); - expect(wrapper.find(Route)).toHaveLength(6); + expect(wrapper.find(Route)).toHaveLength(7); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.tsx index bf68a60757c0d..9f793fcd34fbe 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.tsx @@ -13,7 +13,7 @@ import { useActions, useValues } from 'kea'; import { AppLogic } from '../../app_logic'; import { WorkplaceSearchPageTemplate, PersonalDashboardLayout } from '../../components/layout'; -import { CUSTOM_SERVICE_TYPE } from '../../constants'; +import { NAV, CUSTOM_SERVICE_TYPE } from '../../constants'; import { REINDEX_JOB_PATH, SOURCE_DETAILS_PATH, @@ -24,6 +24,7 @@ import { getContentSourcePath as sourcePath, getSourcesPath, } from '../../routes'; +import { NotFound } from '../../views/not_found'; import { DisplaySettingsRouter } from './components/display_settings'; import { Overview } from './components/overview'; @@ -85,6 +86,9 @@ export const SourceRouter: React.FC = () => { + + + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/not_found/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/not_found/index.ts new file mode 100644 index 0000000000000..482c1a58faa9c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/not_found/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { NotFound } from './not_found'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/not_found/not_found.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/not_found/not_found.test.tsx new file mode 100644 index 0000000000000..0e388a73f0e18 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/not_found/not_found.test.tsx @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { NotFoundPrompt } from '../../../shared/not_found'; +import { SendWorkplaceSearchTelemetry } from '../../../shared/telemetry'; +import { WorkplaceSearchPageTemplate, PersonalDashboardLayout } from '../../components/layout'; + +import { NotFound } from './'; + +describe('NotFound', () => { + it('renders the shared not found prompt', () => { + const wrapper = shallow(); + expect(wrapper.find(NotFoundPrompt)).toHaveLength(1); + }); + + it('renders a telemetry error event', () => { + const wrapper = shallow(); + expect(wrapper.find(SendWorkplaceSearchTelemetry).prop('action')).toEqual('error'); + }); + + it('passes optional preceding page chrome', () => { + const wrapper = shallow(); + expect(wrapper.prop('pageChrome')).toEqual(['Sources', '404']); + }); + + describe('organization views', () => { + it('renders the WorkplaceSearchPageTemplate', () => { + const wrapper = shallow(); + expect(wrapper.type()).toEqual(WorkplaceSearchPageTemplate); + }); + }); + + describe('personal views', () => { + it('renders the PersonalDashboardLayout', () => { + const wrapper = shallow(); + expect(wrapper.type()).toEqual(PersonalDashboardLayout); + }); + + it('sets the "Back to dashboard" link to /p/sources', () => { + const wrapper = shallow(); + expect(wrapper.find(NotFoundPrompt).prop('backToLink')).toEqual('/p/sources'); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/not_found/not_found.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/not_found/not_found.tsx new file mode 100644 index 0000000000000..ef55668775513 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/not_found/not_found.tsx @@ -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 React from 'react'; + +import { WORKPLACE_SEARCH_PLUGIN } from '../../../../../common/constants'; +import { PageTemplateProps } from '../../../shared/layout'; +import { NotFoundPrompt } from '../../../shared/not_found'; +import { SendWorkplaceSearchTelemetry } from '../../../shared/telemetry'; +import { WorkplaceSearchPageTemplate, PersonalDashboardLayout } from '../../components/layout'; +import { PERSONAL_SOURCES_PATH } from '../../routes'; + +interface Props { + isOrganization?: boolean; + pageChrome?: PageTemplateProps['pageChrome']; +} +export const NotFound: React.FC = ({ isOrganization = true, pageChrome = [] }) => { + const Layout = isOrganization ? WorkplaceSearchPageTemplate : PersonalDashboardLayout; + + return ( + + + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_router.test.tsx index 74092f17eadcf..123167f0ad1d0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_router.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_router.test.tsx @@ -25,8 +25,8 @@ import { SettingsRouter } from './settings_router'; describe('SettingsRouter', () => { const initializeSettings = jest.fn(); const NUM_SOURCES = staticSourceData.length; - // Should be 3 routes other than the sources listed Connectors, Customize, & OauthApplication - const NUM_ROUTES = NUM_SOURCES + 3; + // Should be 4 routes other than the sources listed: Connectors, Customize, & OauthApplication, & a redirect + const NUM_ROUTES = NUM_SOURCES + 4; beforeEach(() => { setMockActions({ initializeSettings }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_router.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_router.tsx index f8c8050e20153..d9aeba361d240 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_router.tsx @@ -11,7 +11,6 @@ import { Redirect, Route, Switch } from 'react-router-dom'; import { useActions } from 'kea'; import { - ORG_SETTINGS_PATH, ORG_SETTINGS_CUSTOMIZE_PATH, ORG_SETTINGS_CONNECTORS_PATH, ORG_SETTINGS_OAUTH_APPLICATION_PATH, @@ -33,7 +32,6 @@ export const SettingsRouter: React.FC = () => { return ( - @@ -48,6 +46,9 @@ export const SettingsRouter: React.FC = () => { ))} + + + ); }; From 0857e620c7565281290c3de4334a6ae0e04fc8d8 Mon Sep 17 00:00:00 2001 From: Scotty Bollinger Date: Thu, 24 Jun 2021 14:59:10 -0500 Subject: [PATCH 51/69] [Workplace Search] Remove `isFederatedAuth` checks to expose user features (#103278) * Remove isFederated from main app and routes * Expose all overview cards that were hidden for federated auth * Expose all user features that were hidden for groups * Remove remaining isFederatedAuth references * Lint fixes * Add modified test back for Workplace Search * Remove extraCell Co-authored-by: Constance * Remove brackets Co-authored-by: Constance * Update test name Co-authored-by: Constance Co-authored-by: Constance --- .../common/__mocks__/initial_app_data.ts | 1 - .../enterprise_search/common/types/index.ts | 1 - .../workplace_search/app_logic.test.ts | 3 -- .../workplace_search/app_logic.ts | 12 +----- .../workplace_search/index.test.tsx | 6 ++- .../applications/workplace_search/routes.ts | 1 - .../groups/components/group_overview.tsx | 7 +-- .../groups/components/group_row.test.tsx | 7 --- .../views/groups/components/group_row.tsx | 24 ++++------- .../components/group_users_table.test.tsx | 11 +---- .../groups/components/group_users_table.tsx | 11 ++--- .../groups/components/groups_table.test.tsx | 10 +---- .../views/groups/components/groups_table.tsx | 4 +- .../groups/components/table_filters.test.tsx | 9 +--- .../views/groups/components/table_filters.tsx | 10 ++--- .../views/groups/groups.test.tsx | 20 +-------- .../workplace_search/views/groups/groups.tsx | 11 ++--- .../overview/__mocks__/overview_logic.mock.ts | 2 +- .../views/overview/onboarding_steps.test.tsx | 19 ++++---- .../views/overview/onboarding_steps.tsx | 40 ++++++++--------- .../overview/organization_stats.test.tsx | 9 ---- .../views/overview/organization_stats.tsx | 43 ++++++++----------- .../lib/enterprise_search_config_api.test.ts | 1 - .../lib/enterprise_search_config_api.ts | 1 - 24 files changed, 76 insertions(+), 187 deletions(-) diff --git a/x-pack/plugins/enterprise_search/common/__mocks__/initial_app_data.ts b/x-pack/plugins/enterprise_search/common/__mocks__/initial_app_data.ts index 7ea4289b21967..a4e3ada1c06cb 100644 --- a/x-pack/plugins/enterprise_search/common/__mocks__/initial_app_data.ts +++ b/x-pack/plugins/enterprise_search/common/__mocks__/initial_app_data.ts @@ -8,7 +8,6 @@ export const DEFAULT_INITIAL_APP_DATA = { readOnlyMode: false, ilmEnabled: true, - isFederatedAuth: false, configuredLimits: { appSearch: { engine: { diff --git a/x-pack/plugins/enterprise_search/common/types/index.ts b/x-pack/plugins/enterprise_search/common/types/index.ts index 68904483720f2..f405c86de18f0 100644 --- a/x-pack/plugins/enterprise_search/common/types/index.ts +++ b/x-pack/plugins/enterprise_search/common/types/index.ts @@ -17,7 +17,6 @@ import { export interface InitialAppData { readOnlyMode?: boolean; ilmEnabled?: boolean; - isFederatedAuth?: boolean; configuredLimits?: ConfiguredLimits; access?: ProductAccess; appSearch?: AppSearchAccount; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.test.ts index b2cc835da4ecd..24a156bbd67b8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.test.ts @@ -20,7 +20,6 @@ describe('AppLogic', () => { const DEFAULT_VALUES = { account: {}, hasInitialized: false, - isFederatedAuth: true, isOrganization: false, organization: {}, }; @@ -36,7 +35,6 @@ describe('AppLogic', () => { viewedOnboardingPage: true, }, hasInitialized: true, - isFederatedAuth: false, isOrganization: false, organization: { defaultOrgName: 'My Organization', @@ -61,7 +59,6 @@ describe('AppLogic', () => { expect(AppLogic.values).toEqual({ ...DEFAULT_VALUES, hasInitialized: true, - isFederatedAuth: false, }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.ts index 26e1d7fbb93fd..ee1f6a69fa4b9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.ts @@ -16,7 +16,6 @@ import { interface AppValues extends WorkplaceSearchInitialData { hasInitialized: boolean; - isFederatedAuth: boolean; isOrganization: boolean; } interface AppActions { @@ -32,10 +31,7 @@ const emptyAccount = {} as Account; export const AppLogic = kea>({ path: ['enterprise_search', 'workplace_search', 'app_logic'], actions: { - initializeAppData: ({ workplaceSearch, isFederatedAuth }) => ({ - workplaceSearch, - isFederatedAuth, - }), + initializeAppData: ({ workplaceSearch }) => ({ workplaceSearch }), setContext: (isOrganization) => isOrganization, setOrgName: (name: string) => name, setSourceRestriction: (canCreatePersonalSources: boolean) => canCreatePersonalSources, @@ -47,12 +43,6 @@ export const AppLogic = kea>({ initializeAppData: () => true, }, ], - isFederatedAuth: [ - true, - { - initializeAppData: (_, { isFederatedAuth }) => !!isFederatedAuth, - }, - ], isOrganization: [ false, { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx index 2743dfc794ec6..3ddccde6abd33 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx @@ -6,6 +6,7 @@ */ import '../__mocks__/shallow_useeffect.mock'; +import { DEFAULT_INITIAL_APP_DATA } from '../../../common/__mocks__'; import { setMockValues, setMockActions, mockKibanaValues } from '../__mocks__/kea_logic'; import { mockUseRouteMatch } from '../__mocks__/react_router'; @@ -75,9 +76,10 @@ describe('WorkplaceSearchConfigured', () => { }); it('initializes app data with passed props', () => { - shallow(); + const { workplaceSearch } = DEFAULT_INITIAL_APP_DATA; + shallow(); - expect(initializeAppData).toHaveBeenCalledWith({ isFederatedAuth: true }); + expect(initializeAppData).toHaveBeenCalledWith({ workplaceSearch }); }); it('does not re-initialize app data or re-render header actions', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts index b9309ffd94809..3dac5f80700c0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts @@ -50,7 +50,6 @@ export const PERSONAL_PATH = '/p'; export const USERS_AND_ROLES_PATH = '/users_and_roles'; -export const USERS_PATH = '/users'; export const SECURITY_PATH = '/security'; export const GROUPS_PATH = '/groups'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.tsx index 6914c5dcfcad1..76658cec75e51 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.tsx @@ -24,7 +24,6 @@ import { import { i18n } from '@kbn/i18n'; import { TruncatedContent } from '../../../../shared/truncate'; -import { AppLogic } from '../../../app_logic'; import noSharedSourcesIcon from '../../../assets/share_circle.svg'; import { WorkplaceSearchPageTemplate } from '../../../components/layout'; import { ContentSection } from '../../../components/shared/content_section'; @@ -124,8 +123,6 @@ export const GroupOverview: React.FC = () => { confirmDeleteModalVisible, } = useValues(GroupLogic); - const { isFederatedAuth } = useValues(AppLogic); - const truncatedName = name && ( ); @@ -167,7 +164,7 @@ export const GroupOverview: React.FC = () => { {MANAGE_SOURCES_BUTTON_TEXT} ); - const manageUsersButton = !isFederatedAuth && ( + const manageUsersButton = ( {MANAGE_USERS_BUTTON_TEXT} @@ -199,7 +196,7 @@ export const GroupOverview: React.FC = () => { ); - const usersSection = !isFederatedAuth && ( + const usersSection = ( { - beforeEach(() => { - setMockValues({ isFederatedAuth: true }); - }); - it('renders', () => { const wrapper = shallow(); @@ -30,7 +25,6 @@ describe('GroupRow', () => { }); it('renders group users', () => { - setMockValues({ isFederatedAuth: false }); const wrapper = shallow(); expect(wrapper.find(GroupUsers)).toHaveLength(1); @@ -51,7 +45,6 @@ describe('GroupRow', () => { }); it('renders empty users message when no users present', () => { - setMockValues({ isFederatedAuth: false }); const wrapper = shallow(); expect(wrapper.find('.user-group__accounts').text()).toEqual(NO_USERS_MESSAGE); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row.tsx index 204d8f5655172..94d44fde57aed 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row.tsx @@ -7,7 +7,6 @@ import React from 'react'; -import { useValues } from 'kea'; import moment from 'moment'; import { EuiTableRow, EuiTableRowCell, EuiIcon } from '@elastic/eui'; @@ -15,7 +14,6 @@ import { i18n } from '@kbn/i18n'; import { EuiLinkTo } from '../../../../shared/react_router_helpers'; import { TruncatedContent } from '../../../../shared/truncate'; -import { AppLogic } from '../../../app_logic'; import { getGroupPath } from '../../../routes'; import { Group } from '../../../types'; import { MAX_NAME_LENGTH } from '../group_logic'; @@ -50,8 +48,6 @@ export const GroupRow: React.FC = ({ users, usersCount, }) => { - const { isFederatedAuth } = useValues(AppLogic); - const GROUP_UPDATED_TEXT = i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.groups.groupUpdatedText', { @@ -80,17 +76,15 @@ export const GroupRow: React.FC = ({ )} - {!isFederatedAuth && ( - -
- {usersCount > 0 ? ( - - ) : ( - NO_USERS_MESSAGE - )} -
-
- )} + +
+ {usersCount > 0 ? ( + + ) : ( + NO_USERS_MESSAGE + )} +
+
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users_table.test.tsx index 0dde2f5eaf7f7..cc5f9c4effd21 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users_table.test.tsx @@ -23,17 +23,10 @@ const group = groups[0]; describe('GroupUsersTable', () => { it('renders', () => { - setMockValues({ isFederatedAuth: true, group }); + setMockValues({ group }); const wrapper = shallow(); expect(wrapper.find(EuiTable)).toHaveLength(1); - expect(wrapper.find(TableHeader).prop('headerItems')).toHaveLength(1); - }); - - it('adds header item for non-federated auth', () => { - setMockValues({ isFederatedAuth: false, group }); - const wrapper = shallow(); - expect(wrapper.find(TableHeader).prop('headerItems')).toHaveLength(2); }); @@ -48,7 +41,7 @@ describe('GroupUsersTable', () => { }); }); - setMockValues({ isFederatedAuth: true, group: { users } }); + setMockValues({ group: { users } }); const wrapper = shallow(); const pagination = wrapper.find(EuiTablePagination); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users_table.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users_table.tsx index 050aaf1dadf89..3e9e40fef44a6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users_table.tsx @@ -14,7 +14,6 @@ import { Pager } from '@elastic/eui'; import { USERNAME_LABEL, EMAIL_LABEL } from '../../../../shared/constants'; import { TableHeader } from '../../../../shared/table_header'; -import { AppLogic } from '../../../app_logic'; import { UserRow } from '../../../components/shared/user_row'; import { User } from '../../../types'; import { GroupLogic } from '../group_logic'; @@ -22,14 +21,10 @@ import { GroupLogic } from '../group_logic'; const USERS_PER_PAGE = 10; export const GroupUsersTable: React.FC = () => { - const { isFederatedAuth } = useValues(AppLogic); const { group: { users }, } = useValues(GroupLogic); - const headerItems = [USERNAME_LABEL]; - if (!isFederatedAuth) { - headerItems.push(EMAIL_LABEL); - } + const headerItems = [USERNAME_LABEL, EMAIL_LABEL]; const [firstItem, setFirstItem] = useState(0); const [lastItem, setLastItem] = useState(USERS_PER_PAGE - 1); @@ -58,10 +53,10 @@ export const GroupUsersTable: React.FC = () => { return ( <> - + {users.slice(firstItem, lastItem + 1).map((user: User) => ( - + ))} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/groups_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/groups_table.test.tsx index d11b830a8fc4b..cbb028139623c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/groups_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/groups_table.test.tsx @@ -12,7 +12,7 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { EuiTable, EuiTableHeaderCell } from '@elastic/eui'; +import { EuiTable } from '@elastic/eui'; import { DEFAULT_META } from '../../../../shared/constants'; import { TablePaginationBar } from '../../../components/shared/table_pagination_bar'; @@ -27,7 +27,6 @@ const mockValues = { groupsMeta: DEFAULT_META, groups, hasFiltersSet: false, - isFederatedAuth: true, }; describe('GroupsTable', () => { @@ -43,13 +42,6 @@ describe('GroupsTable', () => { expect(wrapper.find(GroupRow)).toHaveLength(1); }); - it('renders extra header for non-federated auth', () => { - setMockValues({ ...mockValues, isFederatedAuth: false }); - const wrapper = shallow(); - - expect(wrapper.find(EuiTableHeaderCell)).toHaveLength(4); - }); - it('handles pagination', () => { setMockValues({ ...mockValues, diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/groups_table.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/groups_table.tsx index deaf223afa6b3..cfb3ed8044235 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/groups_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/groups_table.tsx @@ -18,7 +18,6 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { AppLogic } from '../../../app_logic'; import { TablePaginationBar } from '../../../components/shared/table_pagination_bar'; import { GroupsLogic } from '../groups_logic'; @@ -53,7 +52,6 @@ export const GroupsTable: React.FC<{}> = () => { groups, hasFiltersSet, } = useValues(GroupsLogic); - const { isFederatedAuth } = useValues(AppLogic); const clearFiltersLink = hasFiltersSet ? : undefined; @@ -79,7 +77,7 @@ export const GroupsTable: React.FC<{}> = () => { {GROUP_TABLE_HEADER} {SOURCES_TABLE_HEADER} - {!isFederatedAuth && {USERS_TABLE_HEADER}} + {USERS_TABLE_HEADER} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filters.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filters.test.tsx index 83a20efe5257e..f0758831e2af6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filters.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filters.test.tsx @@ -21,7 +21,7 @@ const setFilterValue = jest.fn(); describe('TableFilters', () => { beforeEach(() => { - setMockValues({ filterValue: '', isFederatedAuth: true }); + setMockValues({ filterValue: '' }); setMockActions({ setFilterValue }); }); it('renders', () => { @@ -29,13 +29,6 @@ describe('TableFilters', () => { expect(wrapper.find(EuiFieldSearch)).toHaveLength(1); expect(wrapper.find(TableFilterSourcesDropdown)).toHaveLength(1); - expect(wrapper.find(TableFilterUsersDropdown)).toHaveLength(0); - }); - - it('renders for non-federated Auth', () => { - setMockValues({ filterValue: '', isFederatedAuth: false }); - const wrapper = shallow(); - expect(wrapper.find(TableFilterUsersDropdown)).toHaveLength(1); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filters.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filters.tsx index e9ea6a7c6b4aa..0907e0b8b3740 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filters.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filters.tsx @@ -12,7 +12,6 @@ import { useActions, useValues } from 'kea'; import { EuiFieldSearch, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { AppLogic } from '../../../app_logic'; import { GroupsLogic } from '../groups_logic'; import { TableFilterSourcesDropdown } from './table_filter_sources_dropdown'; @@ -28,7 +27,6 @@ const FILTER_GROUPS_PLACEHOLDER = i18n.translate( export const TableFilters: React.FC = () => { const { setFilterValue } = useActions(GroupsLogic); const { filterValue } = useValues(GroupsLogic); - const { isFederatedAuth } = useValues(AppLogic); const handleSearchChange = (e: ChangeEvent) => setFilterValue(e.target.value); @@ -47,11 +45,9 @@ export const TableFilters: React.FC = () => { - {!isFederatedAuth && ( - - - - )} + + +
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.test.tsx index 5be61da22fda9..dccb1ad114db9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.test.tsx @@ -50,7 +50,6 @@ const mockValues = { filteredSources: [], filteredUsers: [], filterValue: '', - isFederatedAuth: false, }; describe('GroupOverview', () => { @@ -112,12 +111,7 @@ describe('GroupOverview', () => { expect(wrapper.find(ClearFiltersLink)).toHaveLength(1); }); - it('renders inviteUsersButton when not federated auth', () => { - setMockValues({ - ...mockValues, - isFederatedAuth: false, - }); - + it('renders inviteUsersButton', () => { const wrapper = shallow(); const actions = getPageHeaderActions(wrapper); @@ -125,18 +119,6 @@ describe('GroupOverview', () => { expect(actions.find(EuiButtonTo)).toHaveLength(1); }); - it('does not render inviteUsersButton when federated auth', () => { - setMockValues({ - ...mockValues, - isFederatedAuth: true, - }); - - const wrapper = shallow(); - const actions = getPageHeaderActions(wrapper); - - expect(actions.find('[data-test-subj="InviteUsersButton"]')).toHaveLength(0); - }); - it('renders EuiLoadingSpinner when loading', () => { setMockValues({ ...mockValues, diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.tsx index 60806a53deb5f..1a4c4f51e93ea 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.tsx @@ -14,10 +14,9 @@ import { i18n } from '@kbn/i18n'; import { FlashMessagesLogic } from '../../../shared/flash_messages'; import { EuiButtonTo } from '../../../shared/react_router_helpers'; -import { AppLogic } from '../../app_logic'; import { WorkplaceSearchPageTemplate } from '../../components/layout'; import { NAV } from '../../constants'; -import { getGroupPath, USERS_PATH } from '../../routes'; +import { getGroupPath, USERS_AND_ROLES_PATH } from '../../routes'; import { AddGroupModal } from './components/add_group_modal'; import { ClearFiltersLink } from './components/clear_filters_link'; @@ -43,8 +42,6 @@ export const Groups: React.FC = () => { filterValue, } = useValues(GroupsLogic); - const { isFederatedAuth } = useValues(AppLogic); - const hasMessages = messages.length > 0; useEffect(() => { @@ -68,7 +65,7 @@ export const Groups: React.FC = () => { const clearFilters = hasFiltersSet && ; const inviteUsersButton = ( - + {i18n.translate('xpack.enterpriseSearch.workplaceSearch.groups.inviteUsers.action', { defaultMessage: 'Invite users', })} @@ -81,9 +78,7 @@ export const Groups: React.FC = () => { })} ); - const headerActions = !isFederatedAuth - ? [inviteUsersButton, createGroupButton] - : [createGroupButton]; + const headerActions = [inviteUsersButton, createGroupButton]; const noResults = ( diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/__mocks__/overview_logic.mock.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/__mocks__/overview_logic.mock.ts index 9aaa6253ea0ab..f6468aefa4fb9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/__mocks__/overview_logic.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/__mocks__/overview_logic.mock.ts @@ -27,7 +27,7 @@ export const mockActions = { initializeOverview: jest.fn(() => ({})), }; -const mockValues = { ...mockOverviewValues, ...mockAppValues, isFederatedAuth: true }; +const mockValues = { ...mockOverviewValues, ...mockAppValues }; setMockActions({ ...mockActions }); setMockKeaValues({ ...mockValues }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.test.tsx index 01e5245c597eb..fd3e1877fdb3e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.test.tsx @@ -13,7 +13,7 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { SOURCES_PATH, USERS_PATH } from '../../routes'; +import { SOURCES_PATH, USERS_AND_ROLES_PATH } from '../../routes'; import { OnboardingCard } from './onboarding_card'; import { OnboardingSteps, OrgNameOnboarding } from './onboarding_steps'; @@ -33,9 +33,9 @@ describe('OnboardingSteps', () => { setMockValues({ canCreateContentSources: true }); const wrapper = shallow(); - expect(wrapper.find(OnboardingCard)).toHaveLength(1); - expect(wrapper.find(OnboardingCard).prop('actionPath')).toBe(SOURCES_PATH); - expect(wrapper.find(OnboardingCard).prop('description')).toBe( + expect(wrapper.find(OnboardingCard)).toHaveLength(2); + expect(wrapper.find(OnboardingCard).first().prop('actionPath')).toBe(SOURCES_PATH); + expect(wrapper.find(OnboardingCard).first().prop('description')).toBe( 'Add shared sources for your organization to start searching.' ); }); @@ -44,7 +44,7 @@ describe('OnboardingSteps', () => { setMockValues({ sourcesCount: 2, hasOrgSources: true }); const wrapper = shallow(); - expect(wrapper.find(OnboardingCard).prop('description')).toEqual( + expect(wrapper.find(OnboardingCard).first().prop('description')).toEqual( 'You have added 2 shared sources. Happy searching.' ); }); @@ -53,14 +53,13 @@ describe('OnboardingSteps', () => { setMockValues({ canCreateContentSources: false }); const wrapper = shallow(); - expect(wrapper.find(OnboardingCard).prop('actionPath')).toBe(undefined); + expect(wrapper.find(OnboardingCard).first().prop('actionPath')).toBe(undefined); }); }); describe('Users & Invitations', () => { - it('renders 0 users when not on federated auth', () => { + it('renders 0 users state', () => { setMockValues({ - isFederatedAuth: false, account, accountsCount: 0, hasUsers: false, @@ -68,7 +67,7 @@ describe('OnboardingSteps', () => { const wrapper = shallow(); expect(wrapper.find(OnboardingCard)).toHaveLength(2); - expect(wrapper.find(OnboardingCard).last().prop('actionPath')).toBe(USERS_PATH); + expect(wrapper.find(OnboardingCard).last().prop('actionPath')).toBe(USERS_AND_ROLES_PATH); expect(wrapper.find(OnboardingCard).last().prop('description')).toEqual( 'Invite your colleagues into this organization to search with you.' ); @@ -76,7 +75,6 @@ describe('OnboardingSteps', () => { it('renders completed users state', () => { setMockValues({ - isFederatedAuth: false, account, accountsCount: 1, hasUsers: true, @@ -90,7 +88,6 @@ describe('OnboardingSteps', () => { it('disables link when the user cannot create invitations', () => { setMockValues({ - isFederatedAuth: false, account: { ...account, canCreateInvitations: false, diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.tsx index a89e3294597e4..44d09ed73ed1f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.tsx @@ -26,7 +26,7 @@ import { TelemetryLogic } from '../../../shared/telemetry'; import { AppLogic } from '../../app_logic'; import sharedSourcesIcon from '../../components/shared/assets/source_icons/share_circle.svg'; import { ContentSection } from '../../components/shared/content_section'; -import { SOURCES_PATH, USERS_PATH, ORG_SETTINGS_PATH } from '../../routes'; +import { SOURCES_PATH, USERS_AND_ROLES_PATH, ORG_SETTINGS_PATH } from '../../routes'; import { OnboardingCard } from './onboarding_card'; import { OverviewLogic } from './overview_logic'; @@ -58,7 +58,6 @@ const ONBOARDING_USERS_CARD_DESCRIPTION = i18n.translate( export const OnboardingSteps: React.FC = () => { const { - isFederatedAuth, organization: { name, defaultOrgName }, account: { isCurated, canCreateInvitations }, } = useValues(AppLogic); @@ -71,8 +70,7 @@ export const OnboardingSteps: React.FC = () => { sourcesCount, } = useValues(OverviewLogic); - const accountsPath = - !isFederatedAuth && (canCreateInvitations || isCurated) ? USERS_PATH : undefined; + const accountsPath = canCreateInvitations || isCurated ? USERS_AND_ROLES_PATH : undefined; const sourcesPath = canCreateContentSources || isCurated ? SOURCES_PATH : undefined; const SOURCES_CARD_DESCRIPTION = i18n.translate( @@ -86,7 +84,7 @@ export const OnboardingSteps: React.FC = () => { return ( - + { actionPath={sourcesPath} complete={hasOrgSources} /> - {!isFederatedAuth && ( - 0 ? 'more' : '' }, - } - )} - actionPath={accountsPath} - complete={hasUsers} - /> - )} + 0 ? 'more' : '' }, + } + )} + actionPath={accountsPath} + complete={hasUsers} + /> {name === defaultOrgName && ( <> diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/organization_stats.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/organization_stats.test.tsx index 110557ac4087a..0d690e500f9e2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/organization_stats.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/organization_stats.test.tsx @@ -5,7 +5,6 @@ * 2.0. */ -import { setMockValues } from './__mocks__'; import './__mocks__/overview_logic.mock'; import React from 'react'; @@ -21,14 +20,6 @@ describe('OrganizationStats', () => { it('renders', () => { const wrapper = shallow(); - expect(wrapper.find(StatisticCard)).toHaveLength(2); - expect(wrapper.find(EuiFlexGrid).prop('columns')).toEqual(2); - }); - - it('renders additional cards for federated auth', () => { - setMockValues({ isFederatedAuth: false }); - const wrapper = shallow(); - expect(wrapper.find(StatisticCard)).toHaveLength(4); expect(wrapper.find(EuiFlexGrid).prop('columns')).toEqual(4); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/organization_stats.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/organization_stats.tsx index d1f0f6a030421..5608032f8bdd9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/organization_stats.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/organization_stats.tsx @@ -13,16 +13,13 @@ import { EuiFlexGrid, EuiPanel } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { AppLogic } from '../../app_logic'; import { ContentSection } from '../../components/shared/content_section'; -import { SOURCES_PATH, USERS_PATH } from '../../routes'; +import { SOURCES_PATH, USERS_AND_ROLES_PATH } from '../../routes'; import { OverviewLogic } from './overview_logic'; import { StatisticCard } from './statistic_card'; export const OrganizationStats: React.FC = () => { - const { isFederatedAuth } = useValues(AppLogic); - const { sourcesCount, pendingInvitationsCount, accountsCount, personalSourcesCount } = useValues( OverviewLogic ); @@ -37,7 +34,7 @@ export const OrganizationStats: React.FC = () => { } > - + { count={sourcesCount} actionPath={SOURCES_PATH} /> - {!isFederatedAuth && ( - <> - - - - )} + + { publicUrl: undefined, readOnlyMode: false, ilmEnabled: false, - isFederatedAuth: false, configuredLimits: { appSearch: { engine: { diff --git a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts index 8cce01d1932ee..833419e34ef5f 100644 --- a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts +++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts @@ -75,7 +75,6 @@ export const callEnterpriseSearchConfigAPI = async ({ publicUrl: stripTrailingSlash(data?.settings?.external_url), readOnlyMode: !!data?.settings?.read_only_mode, ilmEnabled: !!data?.settings?.ilm_enabled, - isFederatedAuth: !!data?.settings?.is_federated_auth, // i.e., not standard auth configuredLimits: { appSearch: { engine: { From cebf16fb538895e8bd4fbbc787d964392bfe9430 Mon Sep 17 00:00:00 2001 From: Candace Park <56409205+parkiino@users.noreply.github.com> Date: Thu, 24 Jun 2021 16:11:16 -0400 Subject: [PATCH 52/69] [Security Solution][Endpoint][Host Isolation] Remove agent status for non endpoint alerts (#102976) --- .../event_details/alert_summary_view.tsx | 11 +++++-- .../common/utils/endpoint_alert_check.test.ts | 31 +++++++++++++++++++ .../common/utils/endpoint_alert_check.ts | 14 +++++++++ .../side_panel/event_details/index.tsx | 4 +-- .../timeline/body/renderers/constants.tsx | 2 +- .../body/renderers/formatted_field.tsx | 4 +-- 6 files changed, 58 insertions(+), 8 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/utils/endpoint_alert_check.test.ts create mode 100644 x-pack/plugins/security_solution/public/common/utils/endpoint_alert_check.ts diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.tsx index e229c0c6fae49..9cc0b43f52123 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.tsx @@ -37,6 +37,7 @@ import { SummaryView } from './summary_view'; import { AlertSummaryRow, getSummaryColumns, SummaryRow } from './helpers'; import { useRuleAsync } from '../../../detections/containers/detection_engine/rules/use_rule_async'; import { LineClamp } from '../line_clamp'; +import { endpointAlertCheck } from '../../utils/endpoint_alert_check'; const StyledEuiDescriptionList = styled(EuiDescriptionList)` padding: 24px 4px 4px; @@ -53,7 +54,7 @@ const fields = [ { id: 'signal.rule.severity', label: ALERTS_HEADERS_SEVERITY }, { id: 'signal.rule.risk_score', label: ALERTS_HEADERS_RISK_SCORE }, { id: 'host.name' }, - { id: 'host.status' }, + { id: 'agent.status' }, { id: 'user.name' }, { id: SOURCE_IP_FIELD_NAME, fieldType: IP_FIELD_TYPE }, { id: DESTINATION_IP_FIELD_NAME, fieldType: IP_FIELD_TYPE }, @@ -178,6 +179,10 @@ const AlertSummaryViewComponent: React.FC<{ timelineId, ]); + const isEndpointAlert = useMemo(() => { + return endpointAlertCheck({ data }); + }, [data]); + const agentId = useMemo(() => { const findAgentId = find({ category: 'agent', field: 'agent.id' }, data)?.values; return findAgentId ? findAgentId[0] : ''; @@ -188,7 +193,7 @@ const AlertSummaryViewComponent: React.FC<{ description: { contextId: timelineId, eventId, - fieldName: 'host.status', + fieldName: 'agent.status', value: agentId, linkValue: undefined, }, @@ -209,7 +214,7 @@ const AlertSummaryViewComponent: React.FC<{ {maybeRule?.note && ( diff --git a/x-pack/plugins/security_solution/public/common/utils/endpoint_alert_check.test.ts b/x-pack/plugins/security_solution/public/common/utils/endpoint_alert_check.test.ts new file mode 100644 index 0000000000000..b085fe67d3814 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/utils/endpoint_alert_check.test.ts @@ -0,0 +1,31 @@ +/* + * 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 _ from 'lodash'; +import { mockDetailItemData } from '../mock'; +import { endpointAlertCheck } from './endpoint_alert_check'; + +describe('utils', () => { + describe('endpointAlertCheck', () => { + it('should return false if detections data does not come from endpoint rule', () => { + expect(endpointAlertCheck({ data: mockDetailItemData })).toBeFalsy(); + }); + it('should return true if detections data comes from an endpoint rule', () => { + _.remove(mockDetailItemData, function (o) { + return o.field === 'agent.type'; + }); + const mockEndpointDetailItemData = _.concat(mockDetailItemData, { + field: 'agent.type', + originalValue: 'endpoint', + values: ['endpoint'], + isObjectArray: false, + }); + + expect(endpointAlertCheck({ data: mockEndpointDetailItemData })).toBeTruthy(); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/utils/endpoint_alert_check.ts b/x-pack/plugins/security_solution/public/common/utils/endpoint_alert_check.ts new file mode 100644 index 0000000000000..e399cec0f3bbe --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/utils/endpoint_alert_check.ts @@ -0,0 +1,14 @@ +/* + * 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 { find } from 'lodash/fp'; +import { TimelineEventsDetailsItem } from '../../../common/search_strategy'; + +export const endpointAlertCheck = ({ data }: { data: TimelineEventsDetailsItem[] | null }) => { + const findEndpointAlert = find({ field: 'agent.type' }, data)?.values; + return findEndpointAlert ? findEndpointAlert[0] === 'endpoint' : false; +}; diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx index 76341055f28ef..395538610f567 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx @@ -32,6 +32,7 @@ import { } from '../../../../detections/components/host_isolation/translations'; import { ALERT_DETAILS } from './translations'; import { useIsolationPrivileges } from '../../../../common/hooks/endpoint/use_isolate_privileges'; +import { endpointAlertCheck } from '../../../../common/utils/endpoint_alert_check'; const StyledEuiFlyoutBody = styled(EuiFlyoutBody)` .euiFlyoutBody__overflow { @@ -92,8 +93,7 @@ const EventDetailsPanelComponent: React.FC = ({ const isAlert = some({ category: 'signal', field: 'signal.rule.id' }, detailsData); const isEndpointAlert = useMemo(() => { - const findEndpointAlert = find({ category: 'agent', field: 'agent.type' }, detailsData)?.values; - return findEndpointAlert ? findEndpointAlert[0] === 'endpoint' : false; + return endpointAlertCheck({ data: detailsData }); }, [detailsData]); const agentId = useMemo(() => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/constants.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/constants.tsx index 761d82b482af2..aeb40bed26c8e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/constants.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/constants.tsx @@ -16,4 +16,4 @@ export const REFERENCE_URL_FIELD_NAME = 'reference.url'; export const EVENT_URL_FIELD_NAME = 'event.url'; export const SIGNAL_RULE_NAME_FIELD_NAME = 'signal.rule.name'; export const SIGNAL_STATUS_FIELD_NAME = 'signal.status'; -export const HOST_STATUS_FIELD_NAME = 'host.status'; +export const AGENT_STATUS_FIELD_NAME = 'agent.status'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx index efb51916e3765..3d5d410abb87e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx @@ -32,7 +32,7 @@ import { REFERENCE_URL_FIELD_NAME, EVENT_URL_FIELD_NAME, SIGNAL_STATUS_FIELD_NAME, - HOST_STATUS_FIELD_NAME, + AGENT_STATUS_FIELD_NAME, GEO_FIELD_TYPE, } from './constants'; import { RenderRuleName, renderEventModule, renderUrl } from './formatted_field_helpers'; @@ -120,7 +120,7 @@ const FormattedFieldValueComponent: React.FC<{ return ( ); - } else if (fieldName === HOST_STATUS_FIELD_NAME) { + } else if (fieldName === AGENT_STATUS_FIELD_NAME) { return ( Date: Thu, 24 Jun 2021 14:17:09 -0600 Subject: [PATCH 53/69] [Maps] Disable draw mode on layer remove (#103188) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/maps/public/actions/layer_actions.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/x-pack/plugins/maps/public/actions/layer_actions.ts b/x-pack/plugins/maps/public/actions/layer_actions.ts index 8bd79474e7f71..1d239a75d1499 100644 --- a/x-pack/plugins/maps/public/actions/layer_actions.ts +++ b/x-pack/plugins/maps/public/actions/layer_actions.ts @@ -11,6 +11,7 @@ import { Query } from 'src/plugins/data/public'; import { MapStoreState } from '../reducers/store'; import { createLayerInstance, + getEditState, getLayerById, getLayerList, getLayerListRaw, @@ -481,6 +482,11 @@ function removeLayerFromLayerList(layerId: string) { type: REMOVE_LAYER, id: layerId, }); + // Clean up draw state if needed + const editState = getEditState(getState()); + if (layerId === editState?.layerId) { + dispatch(setDrawMode(DRAW_MODE.NONE)); + } }; } From e1ef2ea5cd2d13ee7ff09bfc015dc43ef4120002 Mon Sep 17 00:00:00 2001 From: Aaron Caldwell Date: Thu, 24 Jun 2021 14:18:44 -0600 Subject: [PATCH 54/69] [Maps] Disable edit features if editing already enabled for layer (#103300) --- .../__snapshots__/toc_entry.test.tsx.snap | 5 + .../layer_toc/toc_entry/toc_entry.tsx | 1 + .../toc_entry_actions_popover.test.tsx.snap | 115 ++++++++++++++++++ .../toc_entry_actions_popover.test.tsx | 14 +++ .../toc_entry_actions_popover.tsx | 3 +- 5 files changed, 137 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_toc/toc_entry/__snapshots__/toc_entry.test.tsx.snap b/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_toc/toc_entry/__snapshots__/toc_entry.test.tsx.snap index 6310b5507dca5..42618d099ffcf 100644 --- a/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_toc/toc_entry/__snapshots__/toc_entry.test.tsx.snap +++ b/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_toc/toc_entry/__snapshots__/toc_entry.test.tsx.snap @@ -11,6 +11,7 @@ exports[`TOCEntry is rendered 1`] = ` > { openLayerSettings={this._openLayerPanelWithCheck} isEditButtonDisabled={this.props.isEditButtonDisabled} supportsFitToBounds={this.state.supportsFitToBounds} + editModeActiveForLayer={this.props.editModeActiveForLayer} /> {this._renderQuickActions()} diff --git a/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/__snapshots__/toc_entry_actions_popover.test.tsx.snap b/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/__snapshots__/toc_entry_actions_popover.test.tsx.snap index 5068a5dc1ad71..5d1fc8e28f993 100644 --- a/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/__snapshots__/toc_entry_actions_popover.test.tsx.snap +++ b/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/__snapshots__/toc_entry_actions_popover.test.tsx.snap @@ -115,6 +115,121 @@ exports[`TOCEntryActionsPopover is rendered 1`] = ` `; +exports[`TOCEntryActionsPopover should disable Edit features when edit mode active for layer 1`] = ` + + } + className="mapLayTocActions" + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="testLayer" + isOpen={false} + ownFocus={true} + panelPaddingSize="none" +> + , + "name": "Fit to data", + "onClick": [Function], + "toolTipContent": null, + }, + Object { + "data-test-subj": "layerVisibilityToggleButton", + "icon": , + "name": "Hide layer", + "onClick": [Function], + "toolTipContent": null, + }, + Object { + "data-test-subj": "layerSettingsButton", + "disabled": false, + "icon": , + "name": "Edit layer settings", + "onClick": [Function], + "toolTipContent": null, + }, + Object { + "data-test-subj": "cloneLayerButton", + "icon": , + "name": "Clone layer", + "onClick": [Function], + "toolTipContent": null, + }, + Object { + "data-test-subj": "removeLayerButton", + "icon": , + "name": "Remove layer", + "onClick": [Function], + "toolTipContent": null, + }, + ], + "title": "Layer actions", + }, + ] + } + size="m" + /> + +`; + exports[`TOCEntryActionsPopover should disable fit to data when supportsFitToBounds is false 1`] = ` {}, enablePointEditing: () => {}, openLayerSettings: () => {}, + editModeActiveForLayer: false, }; describe('TOCEntryActionsPopover', () => { @@ -100,4 +101,17 @@ describe('TOCEntryActionsPopover', () => { expect(component).toMatchSnapshot(); }); + + test('should disable Edit features when edit mode active for layer', async () => { + const component = shallow( + + ); + + // Ensure all promises resolve + await new Promise((resolve) => process.nextTick(resolve)); + // Ensure the state changes are reflected + component.update(); + + expect(component).toMatchSnapshot(); + }); }); diff --git a/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.tsx b/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.tsx index ab7a54be37404..83b4d2c2a756b 100644 --- a/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.tsx +++ b/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.tsx @@ -36,6 +36,7 @@ export interface Props { removeLayer: (layerId: string) => void; supportsFitToBounds: boolean; toggleVisible: (layerId: string) => void; + editModeActiveForLayer: boolean; } interface State { @@ -170,7 +171,7 @@ export class TOCEntryActionsPopover extends Component { defaultMessage: 'Edit features only supported for document layers without clustering, joins, or time filtering', }), - disabled: !this.state.isFeatureEditingEnabled, + disabled: !this.state.isFeatureEditingEnabled || this.props.editModeActiveForLayer, onClick: async () => { this._closePopover(); const supportedShapeTypes = await (this.props.layer.getSource() as ESSearchSource).getSupportedShapeTypes(); From 60086a9aac1fe2fbaa4211210bcb9b3446c1f83a Mon Sep 17 00:00:00 2001 From: Constance Date: Thu, 24 Jun 2021 13:30:02 -0700 Subject: [PATCH 55/69] Fix Engine Overview not properly stretching to full page height (#103337) - Caused by the wrapping
around the child views - removing that div and moving the `data-test-subj` hooks to the individual views fixes the issue --- .../components/engine_overview/engine_overview.test.tsx | 5 ----- .../components/engine_overview/engine_overview.tsx | 6 +----- .../components/engine_overview/engine_overview_empty.tsx | 1 + .../components/engine_overview/engine_overview_metrics.tsx | 1 + 4 files changed, 3 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx index a2e0ba4fcd44d..01472987e4d48 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx @@ -29,11 +29,6 @@ describe('EngineOverview', () => { setMockValues(values); }); - it('renders', () => { - const wrapper = shallow(); - expect(wrapper.find('[data-test-subj="EngineOverview"]')).toHaveLength(1); - }); - describe('EmptyEngineOverview', () => { it('renders when the engine has no documents & the user can add documents', () => { const myRole = { canManageEngineDocuments: true, canViewEngineCredentials: true }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx index a3f98d8c13e8e..e966709dc1084 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx @@ -25,9 +25,5 @@ export const EngineOverview: React.FC = () => { const canAddDocuments = canManageEngineDocuments && canViewEngineCredentials; const showEngineOverview = !isEngineEmpty || !canAddDocuments || isMetaEngine; - return ( -
- {showEngineOverview ? : } -
- ); + return showEngineOverview ? : ; }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.tsx index 27d9c3723f126..6f8332e1e332e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.tsx @@ -34,6 +34,7 @@ export const EmptyEngineOverview: React.FC = () => { , ], }} + data-test-subj="EngineOverview" > diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_metrics.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_metrics.tsx index 3cc7138623735..9c3a900dfe115 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_metrics.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_metrics.tsx @@ -36,6 +36,7 @@ export const EngineOverviewMetrics: React.FC = () => { }), }} isLoading={dataLoading} + data-test-subj="EngineOverview" > From d5f68eef4f391a2cf9efcf57e258bbf594cf5f0a Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Thu, 24 Jun 2021 16:46:50 -0400 Subject: [PATCH 56/69] [Lens] Fix formula formatting in Metric visualization type (#103167) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../metric_visualization/expression.test.tsx | 98 +++++++++++++++---- .../metric_visualization/expression.tsx | 2 +- 2 files changed, 82 insertions(+), 18 deletions(-) diff --git a/x-pack/plugins/lens/public/metric_visualization/expression.test.tsx b/x-pack/plugins/lens/public/metric_visualization/expression.test.tsx index 2e189e094ef01..b31125a1912ef 100644 --- a/x-pack/plugins/lens/public/metric_visualization/expression.test.tsx +++ b/x-pack/plugins/lens/public/metric_visualization/expression.test.tsx @@ -20,17 +20,22 @@ function sampleArgs() { l1: { type: 'datatable', columns: [ - { id: 'a', name: 'a', meta: { type: 'string' } }, + // Simulating a calculated column like a formula + { id: 'a', name: 'a', meta: { type: 'string', params: { id: 'string' } } }, { id: 'b', name: 'b', meta: { type: 'string' } }, - { id: 'c', name: 'c', meta: { type: 'number' } }, + { + id: 'c', + name: 'c', + meta: { type: 'number', params: { id: 'percent', params: { format: '0.000%' } } }, + }, ], - rows: [{ a: 10110, b: 2, c: 3 }], + rows: [{ a: 'last', b: 'last', c: 3 }], }, }, }; const args: MetricConfig = { - accessor: 'a', + accessor: 'c', layerId: 'l1', title: 'My fanci metric chart', description: 'Fancy chart description', @@ -39,7 +44,7 @@ function sampleArgs() { }; const noAttributesArgs: MetricConfig = { - accessor: 'a', + accessor: 'c', layerId: 'l1', title: '', description: '', @@ -65,11 +70,17 @@ describe('metric_expression', () => { }); describe('MetricChart component', () => { - test('it renders the all attributes when passed (title, description, metricTitle, value)', () => { + test('it renders all attributes when passed (title, description, metricTitle, value)', () => { const { data, args } = sampleArgs(); expect( - shallow( x as IFieldFormat} />) + shallow( + ({ convert: (x) => x } as IFieldFormat)} + /> + ) ).toMatchInlineSnapshot(` { } } > - 10110 + 3 +
+
+ My fanci metric chart +
+ + + `); + }); + + test('it renders strings', () => { + const { data, args } = sampleArgs(); + args.accessor = 'a'; + + expect( + shallow( + ({ convert: (x) => x } as IFieldFormat)} + /> + ) + ).toMatchInlineSnapshot(` + + +
+ last
{ x as IFieldFormat} + formatFactory={() => ({ convert: (x) => x } as IFieldFormat)} /> ) ).toMatchInlineSnapshot(` @@ -130,7 +186,7 @@ describe('metric_expression', () => { } } > - 10110 + 3
{ x as IFieldFormat} + formatFactory={() => ({ convert: (x) => x } as IFieldFormat)} /> ) ).toMatchInlineSnapshot(` @@ -174,7 +230,7 @@ describe('metric_expression', () => { } } > - 10110 + 3
@@ -189,7 +245,7 @@ describe('metric_expression', () => { x as IFieldFormat} + formatFactory={() => ({ convert: (x) => x } as IFieldFormat)} /> ) ).toMatchInlineSnapshot(` @@ -202,14 +258,14 @@ describe('metric_expression', () => { test('it renders an EmptyPlaceholder when null value is passed as data', () => { const { data, noAttributesArgs } = sampleArgs(); - data.tables.l1.rows[0].a = null; + data.tables.l1.rows[0].c = null; expect( shallow( x as IFieldFormat} + formatFactory={() => ({ convert: (x) => x } as IFieldFormat)} /> ) ).toMatchInlineSnapshot(` @@ -222,14 +278,14 @@ describe('metric_expression', () => { test('it renders 0 value', () => { const { data, noAttributesArgs } = sampleArgs(); - data.tables.l1.rows[0].a = 0; + data.tables.l1.rows[0].c = 0; expect( shallow( x as IFieldFormat} + formatFactory={() => ({ convert: (x) => x } as IFieldFormat)} /> ) ).toMatchInlineSnapshot(` @@ -264,5 +320,13 @@ describe('metric_expression', () => { `); }); + + test('it finds the right column to format', () => { + const { data, args } = sampleArgs(); + const factory = jest.fn(() => ({ convert: (x) => x } as IFieldFormat)); + + shallow(); + expect(factory).toHaveBeenCalledWith({ id: 'percent', params: { format: '0.000%' } }); + }); }); }); diff --git a/x-pack/plugins/lens/public/metric_visualization/expression.tsx b/x-pack/plugins/lens/public/metric_visualization/expression.tsx index 70b2cb17c7fe1..60d9d66bce995 100644 --- a/x-pack/plugins/lens/public/metric_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/metric_visualization/expression.tsx @@ -127,7 +127,7 @@ export function MetricChart({ return ; } - const column = firstTable.columns[0]; + const column = firstTable.columns.find(({ id }) => id === accessor)!; const row = firstTable.rows[0]; // NOTE: Cardinality and Sum never receives "null" as value, but always 0, even for empty dataset. From b12095b079d7836e554ab0b15702574398fe4149 Mon Sep 17 00:00:00 2001 From: Chris Cowan Date: Thu, 24 Jun 2021 14:13:15 -0700 Subject: [PATCH 57/69] [Metrics UI] Prevent saved views from trampling URL state (#103146) * [Metrics UI] Prevent saved views from trampling URL state * Adding space back in --- .../public/containers/saved_view/saved_view.tsx | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/infra/public/containers/saved_view/saved_view.tsx b/x-pack/plugins/infra/public/containers/saved_view/saved_view.tsx index 56a2a13e31ff7..c54a2a69a994c 100644 --- a/x-pack/plugins/infra/public/containers/saved_view/saved_view.tsx +++ b/x-pack/plugins/infra/public/containers/saved_view/saved_view.tsx @@ -255,12 +255,21 @@ export const useSavedView = (props: Props) => { }, [urlState, setUrlState, currentView, defaultViewId, data]); useEffect(() => { - if (!currentView && !loading && data) { + if (!currentView && !loading && data && shouldLoadDefault) { const viewToSet = views.find((v) => v.id === urlState.viewId); if (viewToSet) setCurrentView(viewToSet); else loadDefaultViewIfSet(); } - }, [loading, currentView, data, views, setCurrentView, loadDefaultViewIfSet, urlState.viewId]); + }, [ + loading, + currentView, + data, + views, + setCurrentView, + loadDefaultViewIfSet, + urlState.viewId, + shouldLoadDefault, + ]); return { views, From 45b660172ae868c4e6ddce6e3f88c9ba7e374f87 Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Thu, 24 Jun 2021 16:19:01 -0500 Subject: [PATCH 58/69] skip suite failing es promotion. #103364 --- .../apis/management/index_lifecycle_management/policies.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/api_integration/apis/management/index_lifecycle_management/policies.js b/x-pack/test/api_integration/apis/management/index_lifecycle_management/policies.js index 756fda7566843..d59d364c38827 100644 --- a/x-pack/test/api_integration/apis/management/index_lifecycle_management/policies.js +++ b/x-pack/test/api_integration/apis/management/index_lifecycle_management/policies.js @@ -32,7 +32,8 @@ export default function ({ getService }) { const { addPolicyToIndex } = registerIndexHelpers({ supertest }); - describe('policies', () => { + // failing ES promotion https://github.com/elastic/kibana/issues/103364 + describe.skip('policies', () => { after(() => Promise.all([cleanUpEsResources(), cleanUpPolicies()])); describe('list', () => { From c1ced880bc3b6d3cf819576e0001d46380fb7495 Mon Sep 17 00:00:00 2001 From: Ross Wolf <31489089+rw-access@users.noreply.github.com> Date: Thu, 24 Jun 2021 15:31:25 -0600 Subject: [PATCH 59/69] [Detections] Adds automatic updating for Prebuilt Security Detection Rules package (#101846) * Automatically install and update the security_detection_engine package * Remove security_detection_engine from required Fleet packages * Update fleet package-registry image * Add sha256: to the distribution package * Use distribution from https://beats-ci.elastic.co/job/Ingest-manager/job/release-distribution/152 * Change fleet required packag * Fix bad merge * Update rules to 0.13.1 package * Fix NOTICE.txt --- NOTICE.txt | 18 ++-- .../public/app/home/index.tsx | 4 +- ...de.ts => use_upgrade_security_packages.ts} | 20 ++--- .../apm_403_response_to_a_post.json | 2 +- .../apm_405_response_method_not_allowed.json | 2 +- .../apm_null_user_agent.json | 2 +- .../apm_sqlmap_user_agent.json | 2 +- ...tion_added_to_google_workspace_domain.json | 2 +- ...tempt_to_deactivate_okta_network_zone.json | 2 +- .../attempt_to_delete_okta_network_zone.json | 2 +- ...collection_cloudtrail_logging_created.json | 2 +- ...ion_gcp_pub_sub_subscription_creation.json | 2 +- ...collection_gcp_pub_sub_topic_creation.json | 2 +- ...llection_microsoft_365_new_inbox_rule.json | 2 +- ...collection_update_event_hub_auth_rule.json | 2 +- ...d_control_certutil_network_connection.json | 7 +- ...mand_and_control_cobalt_strike_beacon.json | 2 +- ...cobalt_strike_default_teamserver_cert.json | 2 +- ..._control_dns_directly_to_the_internet.json | 7 +- ...nd_and_control_dns_tunneling_nslookup.json | 2 +- ...download_rar_powershell_from_internet.json | 7 +- .../command_and_control_fin7_c2_behavior.json | 2 +- .../command_and_control_halfbaked_beacon.json | 2 +- ...d_control_nat_traversal_port_activity.json | 2 +- .../command_and_control_port_26_activity.json | 2 +- ...te_desktop_protocol_from_the_internet.json | 7 +- ...mand_and_control_telnet_port_activity.json | 2 +- ...l_network_computing_from_the_internet.json | 7 +- ...ual_network_computing_to_the_internet.json | 7 +- ...l_access_attempted_bypass_of_okta_mfa.json | 2 +- ...mpts_to_brute_force_okta_user_account.json | 10 ++- ...ccess_aws_iam_assume_role_brute_force.json | 2 +- ...ial_access_collection_sensitive_files.json | 2 +- ...dential_access_dumping_hashes_bi_cmds.json | 2 +- ...ial_access_iam_user_addition_to_group.json | 2 +- .../credential_access_kerberosdump_kcc.json | 2 +- .../credential_access_key_vault_modified.json | 2 +- ..._365_brute_force_user_account_attempt.json | 4 +- ...65_potential_password_spraying_attack.json | 2 +- ...ential_access_mitm_localhost_webproxy.json | 2 +- ...okta_brute_force_or_password_spraying.json | 2 +- ...ntial_access_potential_ssh_bruteforce.json | 2 +- ...cess_root_console_failure_brute_force.json | 2 +- ..._access_secretsmanager_getsecretvalue.json | 2 +- ...ccess_storage_account_key_regenerated.json | 2 +- .../credential_access_systemkey_dumping.json | 2 +- .../defense_evasion_amsienable_key_mod.json | 57 ++++++++++++ ...vasion_apple_softupdates_modification.json | 2 +- ...evasion_attempt_to_disable_gatekeeper.json | 2 +- ...tempt_to_disable_iptables_or_firewall.json | 2 +- ...ion_attempt_to_disable_syslog_service.json | 2 +- ...e_application_credential_modification.json | 2 +- ...on_azure_diagnostic_settings_deletion.json | 2 +- ...sion_azure_service_principal_addition.json | 2 +- ..._base32_encoding_or_decoding_activity.json | 2 +- ...vasion_clearing_windows_security_logs.json | 2 +- ...se_evasion_cloudtrail_logging_deleted.json | 2 +- ..._evasion_cloudtrail_logging_suspended.json | 2 +- ...nse_evasion_cloudwatch_alarm_deletion.json | 2 +- ..._evasion_config_service_rule_deletion.json | 2 +- ...vasion_configuration_recorder_stopped.json | 2 +- .../defense_evasion_cve_2020_0601.json | 2 +- ...fense_evasion_disable_selinux_attempt.json | 2 +- ...defense_evasion_ec2_flow_log_deletion.json | 2 +- ...ense_evasion_ec2_network_acl_deletion.json | 2 +- .../defense_evasion_event_hub_deletion.json | 2 +- ...fense_evasion_file_deletion_via_shred.json | 2 +- ...defense_evasion_file_mod_writable_dir.json | 2 +- ...ense_evasion_firewall_policy_deletion.json | 2 +- ...nse_evasion_gcp_firewall_rule_created.json | 2 +- ...nse_evasion_gcp_firewall_rule_deleted.json | 2 +- ...se_evasion_gcp_firewall_rule_modified.json | 2 +- ...e_evasion_gcp_logging_bucket_deletion.json | 2 +- ...nse_evasion_gcp_logging_sink_deletion.json | 2 +- ...ion_gcp_pub_sub_subscription_deletion.json | 2 +- ...se_evasion_gcp_pub_sub_topic_deletion.json | 2 +- ...storage_bucket_configuration_modified.json | 2 +- ...p_storage_bucket_permissions_modified.json | 2 +- ...e_evasion_guardduty_detector_deletion.json | 2 +- .../defense_evasion_hidden_file_dir_tmp.json | 2 +- .../defense_evasion_injection_msbuild.json | 2 +- ...ense_evasion_install_root_certificate.json | 2 +- ...defense_evasion_kernel_module_removal.json | 2 +- ...osoft_365_exchange_dlp_policy_removed.json | 2 +- ...change_malware_filter_policy_deletion.json | 2 +- ..._365_exchange_malware_filter_rule_mod.json | 2 +- ...65_exchange_safe_attach_rule_disabled.json | 2 +- ...isc_lolbin_connecting_to_the_internet.json | 7 +- ..._evasion_modify_environment_launchctl.json | 2 +- .../defense_evasion_msxsl_network.json | 7 +- ...ense_evasion_network_watcher_deletion.json | 2 +- ...sion_s3_bucket_configuration_deletion.json | 2 +- .../defense_evasion_safari_config_change.json | 2 +- ...dboxed_office_app_suspicious_zip_file.json | 2 +- ...vasion_stop_process_service_threshold.json | 2 +- ...picious_execution_from_mounted_device.json | 89 +++++++++++++++++++ ...ser_password_reset_or_unlock_attempts.json | 12 +-- ...vasion_tcc_bypass_mounted_apfs_access.json | 2 +- ..._evasion_unload_endpointsecurity_kext.json | 2 +- ...nusual_network_connection_via_dllhost.json | 51 +++++++++++ ...usual_network_connection_via_rundll32.json | 7 +- .../defense_evasion_waf_acl_deletion.json | 2 +- ...asion_waf_rule_or_rule_group_deletion.json | 2 +- .../discovery_blob_container_access_mod.json | 2 +- .../discovery_kernel_module_enumeration.json | 2 +- ...covery_virtual_machine_fingerprinting.json | 2 +- ...d_to_google_workspace_trusted_domains.json | 2 +- .../elastic_endpoint_security.json | 2 +- .../endgame_adversary_behavior_detected.json | 2 +- .../endgame_cred_dumping_detected.json | 2 +- .../endgame_cred_dumping_prevented.json | 2 +- .../endgame_cred_manipulation_detected.json | 2 +- .../endgame_cred_manipulation_prevented.json | 2 +- .../endgame_exploit_detected.json | 2 +- .../endgame_exploit_prevented.json | 2 +- .../endgame_malware_detected.json | 2 +- .../endgame_malware_prevented.json | 2 +- .../endgame_permission_theft_detected.json | 2 +- .../endgame_permission_theft_prevented.json | 2 +- .../endgame_process_injection_detected.json | 2 +- .../endgame_process_injection_prevented.json | 2 +- .../endgame_ransomware_detected.json | 2 +- .../endgame_ransomware_prevented.json | 2 +- ...and_prompt_connecting_to_the_internet.json | 7 +- .../execution_command_virtual_machine.json | 2 +- ...vasion_electron_app_childproc_node_js.json | 2 +- ...le_program_connecting_to_the_internet.json | 7 +- ...ution_installer_spawned_network_event.json | 7 +- ...on_pentest_eggshell_remote_admin_tool.json | 2 +- .../execution_perl_tty_shell.json | 2 +- .../execution_python_tty_shell.json | 2 +- ...er_program_connecting_to_the_internet.json | 7 +- ...ing_osascript_exec_followed_by_netcon.json | 7 +- ...ltration_ec2_snapshot_change_activity.json | 2 +- .../exfiltration_ec2_vm_export_failure.json | 70 +++++++++++++++ ...tration_gcp_logging_sink_modification.json | 2 +- ..._365_exchange_transport_rule_creation.json | 2 +- ...osoft_365_exchange_transport_rule_mod.json | 2 +- .../prepackaged_rules/external_alerts.json | 2 +- .../google_workspace_admin_role_deletion.json | 2 +- ...le_workspace_mfa_enforcement_disabled.json | 2 +- .../google_workspace_policy_modified.json | 2 +- ...pact_attempt_to_revoke_okta_api_token.json | 2 +- ...pact_azure_automation_runbook_deleted.json | 2 +- .../impact_cloudtrail_logging_updated.json | 2 +- .../impact_cloudwatch_log_group_deletion.json | 2 +- ...impact_cloudwatch_log_stream_deletion.json | 2 +- .../impact_ec2_disable_ebs_encryption.json | 2 +- .../impact_gcp_iam_role_deletion.json | 2 +- .../impact_gcp_service_account_deleted.json | 2 +- .../impact_gcp_service_account_disabled.json | 2 +- .../impact_gcp_storage_bucket_deleted.json | 2 +- ...virtual_private_cloud_network_deleted.json | 2 +- ...p_virtual_private_cloud_route_created.json | 2 +- ...p_virtual_private_cloud_route_deleted.json | 2 +- .../impact_iam_deactivate_mfa_device.json | 2 +- .../impact_iam_group_deletion.json | 2 +- .../impact_possible_okta_dos_attack.json | 2 +- .../impact_rds_cluster_deletion.json | 2 +- .../impact_rds_instance_cluster_stoppage.json | 2 +- .../impact_resource_group_deletion.json | 2 +- .../rules/prepackaged_rules/index.ts | 10 ++- ...ure_active_directory_high_risk_signin.json | 2 +- ...re_active_directory_powershell_signin.json | 2 +- ...tack_via_azure_registered_application.json | 2 +- .../initial_access_console_login_root.json | 2 +- ...ial_access_external_guest_user_invite.json | 2 +- ...l_access_gcp_iam_custom_role_creation.json | 2 +- .../initial_access_login_failures.json | 2 +- .../initial_access_login_location.json | 2 +- .../initial_access_login_sessions.json | 2 +- .../initial_access_login_time.json | 2 +- ...5_exchange_anti_phish_policy_deletion.json | 2 +- ...soft_365_exchange_anti_phish_rule_mod.json | 2 +- ...osoft_365_exchange_safelinks_disabled.json | 2 +- .../initial_access_password_recovery.json | 2 +- ...mote_procedure_call_from_the_internet.json | 7 +- ...remote_procedure_call_to_the_internet.json | 7 +- ...file_sharing_activity_to_the_internet.json | 2 +- ...icious_activity_reported_by_okta_user.json | 2 +- ...al_access_unsecure_elasticsearch_node.json | 2 +- .../initial_access_via_system_manager.json | 2 +- ..._access_zoom_meeting_with_no_passcode.json | 2 +- ...ential_access_kerberos_bifrostconsole.json | 2 +- .../lateral_movement_dns_server_overflow.json | 2 +- ...ral_movement_remote_ssh_login_enabled.json | 2 +- ...ment_telnet_network_activity_external.json | 7 +- ...ment_telnet_network_activity_internal.json | 7 +- .../linux_hping_activity.json | 2 +- .../linux_iodine_activity.json | 2 +- .../linux_nping_activity.json | 2 +- ...nux_process_started_in_temp_directory.json | 2 +- .../linux_strace_activity.json | 2 +- ...led_for_google_workspace_organization.json | 2 +- ...exchange_dkim_signing_config_disabled.json | 2 +- ..._teams_custom_app_interaction_allowed.json | 2 +- .../ml_high_count_network_denies.json | 6 +- .../ml_high_count_network_events.json | 6 +- .../ml_linux_anomalous_metadata_process.json | 7 +- .../ml_linux_anomalous_metadata_user.json | 7 +- ...linux_anomalous_network_port_activity.json | 7 +- .../ml_linux_anomalous_process_all_hosts.json | 7 +- .../ml_linux_anomalous_user_name.json | 7 +- .../ml_rare_destination_country.json | 6 +- .../ml_rare_process_by_host_linux.json | 7 +- .../ml_rare_process_by_host_windows.json | 7 +- .../ml_spike_in_traffic_to_a_country.json | 6 +- ...ml_windows_anomalous_metadata_process.json | 7 +- .../ml_windows_anomalous_metadata_user.json | 7 +- ...ml_windows_anomalous_network_activity.json | 7 +- .../ml_windows_anomalous_path_activity.json | 7 +- ...l_windows_anomalous_process_all_hosts.json | 7 +- ...ml_windows_anomalous_process_creation.json | 7 +- .../ml_windows_anomalous_user_name.json | 7 +- .../rules/prepackaged_rules/notice.ts | 18 ++-- ...ttempt_to_deactivate_okta_application.json | 2 +- ...kta_attempt_to_deactivate_okta_policy.json | 2 +- ...ttempt_to_deactivate_okta_policy_rule.json | 2 +- ...ta_attempt_to_delete_okta_application.json | 2 +- .../okta_attempt_to_delete_okta_policy.json | 2 +- ...ta_attempt_to_delete_okta_policy_rule.json | 2 +- ...ta_attempt_to_modify_okta_application.json | 2 +- ...a_attempt_to_modify_okta_network_zone.json | 2 +- .../okta_attempt_to_modify_okta_policy.json | 2 +- ...ta_attempt_to_modify_okta_policy_rule.json | 2 +- ..._or_delete_application_sign_on_policy.json | 2 +- ...threat_detected_by_okta_threatinsight.json | 2 +- ...stence_account_creation_hide_at_logon.json | 2 +- ...tor_privileges_assigned_to_okta_group.json | 2 +- ...inistrator_role_assigned_to_okta_user.json | 2 +- ...ence_attempt_to_create_okta_api_token.json | 2 +- ..._deactivate_mfa_for_okta_user_account.json | 2 +- ...set_mfa_factors_for_okta_user_account.json | 2 +- ...ence_azure_automation_account_created.json | 2 +- ...utomation_runbook_created_or_modified.json | 2 +- ...ence_azure_automation_webhook_created.json | 2 +- ...re_conditional_access_policy_modified.json | 2 +- ...nce_azure_pim_user_added_global_admin.json | 2 +- ...ged_identity_management_role_modified.json | 2 +- ..._access_authorization_plugin_creation.json | 2 +- ...l_access_modify_auth_module_or_config.json | 2 +- ...credential_access_modify_ssh_binaries.json | 2 +- ...launch_agent_deamon_logonitem_process.json | 2 +- ...rectory_services_plugins_modification.json | 2 +- ...e_docker_shortcuts_plist_modification.json | 2 +- .../persistence_ec2_network_acl_creation.json | 2 +- .../persistence_enable_root_account.json | 2 +- ..._gcp_iam_service_account_key_deletion.json | 2 +- ...e_gcp_key_created_for_service_account.json | 2 +- ...rsistence_gcp_service_account_created.json | 2 +- ...workspace_admin_role_assigned_to_user.json | 2 +- ...a_domain_wide_delegation_of_authority.json | 2 +- ...e_workspace_custom_admin_role_created.json | 2 +- ...stence_google_workspace_role_modified.json | 2 +- .../persistence_iam_group_creation.json | 2 +- ...stence_loginwindow_plist_modification.json | 2 +- ...rsistence_mfa_disabled_for_azure_user.json | 2 +- ...5_exchange_management_role_assignment.json | 2 +- ...oft_365_teams_external_access_enabled.json | 2 +- ...rosoft_365_teams_guest_access_enabled.json | 2 +- ...ersistence_periodic_tasks_file_mdofiy.json | 2 +- .../persistence_rds_cluster_creation.json | 2 +- ...sistence_shell_activity_by_web_server.json | 2 +- ...ersistence_shell_profile_modification.json | 2 +- ...ence_ssh_authorized_keys_modification.json | 2 +- ...ence_suspicious_calendar_modification.json | 2 +- ...stence_suspicious_com_hijack_registry.json | 4 +- ..._added_as_owner_for_azure_application.json | 2 +- ..._as_owner_for_azure_service_principal.json | 2 +- ...tence_via_atom_init_file_modification.json | 2 +- ...lege_escalation_echo_nopasswd_sudoers.json | 2 +- ...calation_explicit_creds_via_scripting.json | 2 +- ...alation_exploit_adobe_acrobat_updater.json | 2 +- ...lation_ld_preload_shared_object_modif.json | 2 +- ..._escalation_local_user_added_to_admin.json | 2 +- ...ge_escalation_persistence_phantom_dll.json | 4 +- ...ilege_escalation_root_crontab_filemod.json | 2 +- ...ege_escalation_root_login_without_mfa.json | 2 +- ...ation_setuid_setgid_bit_set_via_chmod.json | 2 +- ...ilege_escalation_sudo_buffer_overflow.json | 2 +- ...privilege_escalation_sudoers_file_mod.json | 2 +- ...ege_escalation_updateassumerolepolicy.json | 2 +- .../threat_intel_module_match.json | 2 +- .../fleet_api_integration/apis/epm/delete.ts | 2 +- x-pack/test/fleet_api_integration/config.ts | 3 +- 285 files changed, 728 insertions(+), 362 deletions(-) rename x-pack/plugins/security_solution/public/common/hooks/{endpoint/upgrade.ts => use_upgrade_security_packages.ts} (80%) create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_amsienable_key_mod.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_suspicious_execution_from_mounted_device.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_unusual_network_connection_via_dllhost.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/exfiltration_ec2_vm_export_failure.json diff --git a/NOTICE.txt b/NOTICE.txt index b0f7e65f46fa9..4ede43610ca7b 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -149,17 +149,17 @@ SOFTWARE. --- Detection Rules -Copyright 2020 Elasticsearch B.V. +Copyright 2021 Elasticsearch B.V. --- This product bundles rules based on https://github.com/BlueTeamLabs/sentinel-attack -which is available under a "MIT" license. The files based on this license are: +which is available under a "MIT" license. The rules based on this license are: -- defense_evasion_via_filter_manager -- discovery_process_discovery_via_tasklist_command -- persistence_priv_escalation_via_accessibility_features -- persistence_via_application_shimming -- defense_evasion_execution_via_trusted_developer_utilities +- "Potential Evasion via Filter Manager" (06dceabf-adca-48af-ac79-ffdf4c3b1e9a) +- "Process Discovery via Tasklist" (cc16f774-59f9-462d-8b98-d27ccd4519ec) +- "Potential Modification of Accessibility Binaries" (7405ddf1-6c8e-41ce-818f-48bea6bcaed8) +- "Potential Application Shimming via Sdbinst" (fd4a992d-6130-4802-9ff8-829b89ae801f) +- "Trusted Developer Application Usage" (9d110cb3-5f4b-4c9a-b9f5-53f0a1707ae1) MIT License @@ -185,9 +185,9 @@ SOFTWARE. --- This product bundles rules based on https://github.com/FSecureLABS/leonidas -which is available under a "MIT" license. The files based on this license are: +which is available under a "MIT" license. The rules based on this license are: -- credential_access_secretsmanager_getsecretvalue.toml +- "AWS Access Secret in Secrets Manager" (a00681e3-9ed6-447c-ab2c-be648821c622) MIT License diff --git a/x-pack/plugins/security_solution/public/app/home/index.tsx b/x-pack/plugins/security_solution/public/app/home/index.tsx index 9a57ab3fc3a73..17a6fab103d6f 100644 --- a/x-pack/plugins/security_solution/public/app/home/index.tsx +++ b/x-pack/plugins/security_solution/public/app/home/index.tsx @@ -17,7 +17,7 @@ import { useInitSourcerer, useSourcererScope } from '../../common/containers/sou import { useKibana } from '../../common/lib/kibana'; import { DETECTIONS_SUB_PLUGIN_ID } from '../../../common/constants'; import { SourcererScopeName } from '../../common/store/sourcerer/model'; -import { useUpgradeEndpointPackage } from '../../common/hooks/endpoint/upgrade'; +import { useUpgradeSecurityPackages } from '../../common/hooks/use_upgrade_security_packages'; import { GlobalHeader } from './global_header'; import { SecuritySolutionTemplateWrapper } from './template_wrapper'; @@ -56,7 +56,7 @@ const HomePageComponent: React.FC = ({ // tabs in the app. This is useful for keeping the endpoint package as up to date as possible until // a background task solution can be built on the server side. Once a background task solution is available we // can remove this. - useUpgradeEndpointPackage(); + useUpgradeSecurityPackages(); return ( diff --git a/x-pack/plugins/security_solution/public/common/hooks/endpoint/upgrade.ts b/x-pack/plugins/security_solution/public/common/hooks/use_upgrade_security_packages.ts similarity index 80% rename from x-pack/plugins/security_solution/public/common/hooks/endpoint/upgrade.ts rename to x-pack/plugins/security_solution/public/common/hooks/use_upgrade_security_packages.ts index 8e083b3c6b5f2..6a3afccd8794d 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/endpoint/upgrade.ts +++ b/x-pack/plugins/security_solution/public/common/hooks/use_upgrade_security_packages.ts @@ -6,31 +6,31 @@ */ import { useEffect } from 'react'; -import { HttpFetchOptions, HttpStart } from 'src/core/public'; -import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { HttpFetchOptions, HttpStart } from 'kibana/public'; +import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; import { epmRouteService, appRoutesService, CheckPermissionsResponse, BulkInstallPackagesResponse, -} from '../../../../../fleet/common'; -import { StartServices } from '../../../types'; -import { useIngestEnabledCheck } from './ingest_enabled'; +} from '../../../../fleet/common'; +import { StartServices } from '../../types'; +import { useIngestEnabledCheck } from './endpoint/ingest_enabled'; /** - * Requests that the endpoint package be upgraded to the latest version + * Requests that the endpoint and security_detection_engine package be upgraded to the latest version * * @param http an http client for sending the request * @param options an object containing options for the request */ -const sendUpgradeEndpointPackage = async ( +const sendUpgradeSecurityPackages = async ( http: HttpStart, options: HttpFetchOptions = {} ): Promise => { return http.post(epmRouteService.getBulkInstallPath(), { ...options, body: JSON.stringify({ - packages: ['endpoint'], + packages: ['endpoint', 'security_detection_engine'], }), }); }; @@ -51,7 +51,7 @@ const sendCheckPermissions = async ( }); }; -export const useUpgradeEndpointPackage = () => { +export const useUpgradeSecurityPackages = () => { const context = useKibana(); const { allEnabled: ingestEnabled } = useIngestEnabledCheck(); @@ -79,7 +79,7 @@ export const useUpgradeEndpointPackage = () => { } // ignore the response for now since we aren't notifying the user - await sendUpgradeEndpointPackage(context.services.http, { signal }); + await sendUpgradeSecurityPackages(context.services.http, { signal }); } catch (error) { // Ignore Errors, since this should not hinder the user's ability to use the UI diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/apm_403_response_to_a_post.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/apm_403_response_to_a_post.json index 4c40216391c8b..76ba58be0a428 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/apm_403_response_to_a_post.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/apm_403_response_to_a_post.json @@ -12,7 +12,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Web Application Suspicious Activity: POST Request Declined", - "query": "http.response.status_code:403 and http.request.method:post", + "query": "http.response.status_code:403 and http.request.method:post\n", "references": [ "https://en.wikipedia.org/wiki/HTTP_403" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/apm_405_response_method_not_allowed.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/apm_405_response_method_not_allowed.json index 40138f470c631..0633004273952 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/apm_405_response_method_not_allowed.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/apm_405_response_method_not_allowed.json @@ -12,7 +12,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Web Application Suspicious Activity: Unauthorized Method", - "query": "http.response.status_code:405", + "query": "http.response.status_code:405\n", "references": [ "https://en.wikipedia.org/wiki/HTTP_405" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/apm_null_user_agent.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/apm_null_user_agent.json index a2e9b130f0840..87bbfd727cdf3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/apm_null_user_agent.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/apm_null_user_agent.json @@ -30,7 +30,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Web Application Suspicious Activity: No User Agent", - "query": "url.path:*", + "query": "url.path:*\n", "references": [ "https://en.wikipedia.org/wiki/User_agent" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/apm_sqlmap_user_agent.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/apm_sqlmap_user_agent.json index ec65f7cb57661..334cf78b4338c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/apm_sqlmap_user_agent.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/apm_sqlmap_user_agent.json @@ -12,7 +12,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Web Application Suspicious Activity: sqlmap User Agent", - "query": "user_agent.original:\"sqlmap/1.3.11#stable (http://sqlmap.org)\"", + "query": "user_agent.original:\"sqlmap/1.3.11#stable (http://sqlmap.org)\"\n", "references": [ "http://sqlmap.org/" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/application_added_to_google_workspace_domain.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/application_added_to_google_workspace_domain.json index b447e59e71435..c45d377645b05 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/application_added_to_google_workspace_domain.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/application_added_to_google_workspace_domain.json @@ -16,7 +16,7 @@ "license": "Elastic License v2", "name": "Application Added to Google Workspace Domain", "note": "## Config\n\nThe Google Workspace Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.\n\n### Important Information Regarding Google Workspace Event Lag Times\n- As per Google's documentation, Google Workspace administrators may observe lag times ranging from minutes up to 3 days between the time of an event's occurrence and the event being visible in the Google Workspace admin/audit logs.\n- This rule is configured to run every 10 minutes with a lookback time of 130 minutes.\n- To reduce the risk of false negatives, consider reducing the interval that the Google Workspace (formerly G Suite) Filebeat module polls Google's reporting API for new events.\n- By default, `var.interval` is set to 2 hours (2h). Consider changing this interval to a lower value, such as 10 minutes (10m).\n- See the following references for further information.\n - https://support.google.com/a/answer/7061566\n - https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-module-gsuite.html", - "query": "event.dataset:(gsuite.admin or google_workspace.admin) and event.provider:admin and event.category:iam and event.action:ADD_APPLICATION", + "query": "event.dataset:(gsuite.admin or google_workspace.admin) and event.provider:admin and event.category:iam and event.action:ADD_APPLICATION\n", "references": [ "https://support.google.com/a/answer/6328701?hl=en#" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/attempt_to_deactivate_okta_network_zone.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/attempt_to_deactivate_okta_network_zone.json index fbdf3d58f2b81..8c8353f1d5f9c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/attempt_to_deactivate_okta_network_zone.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/attempt_to_deactivate_okta_network_zone.json @@ -14,7 +14,7 @@ "license": "Elastic License v2", "name": "Attempt to Deactivate an Okta Network Zone", "note": "## Config\n\nThe Okta Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:okta.system and event.action:zone.deactivate", + "query": "event.dataset:okta.system and event.action:zone.deactivate\n", "references": [ "https://help.okta.com/en/prod/Content/Topics/Security/network/network-zones.htm", "https://developer.okta.com/docs/reference/api/system-log/", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/attempt_to_delete_okta_network_zone.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/attempt_to_delete_okta_network_zone.json index b0bd850c1d63c..903475b441d01 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/attempt_to_delete_okta_network_zone.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/attempt_to_delete_okta_network_zone.json @@ -14,7 +14,7 @@ "license": "Elastic License v2", "name": "Attempt to Delete an Okta Network Zone", "note": "## Config\n\nThe Okta Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:okta.system and event.action:zone.delete", + "query": "event.dataset:okta.system and event.action:zone.delete\n", "references": [ "https://help.okta.com/en/prod/Content/Topics/Security/network/network-zones.htm", "https://developer.okta.com/docs/reference/api/system-log/", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/collection_cloudtrail_logging_created.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/collection_cloudtrail_logging_created.json index 3e2479369ebb2..7debf76f371c5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/collection_cloudtrail_logging_created.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/collection_cloudtrail_logging_created.json @@ -16,7 +16,7 @@ "license": "Elastic License v2", "name": "AWS CloudTrail Log Created", "note": "## Config\n\nThe AWS Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:aws.cloudtrail and event.provider:cloudtrail.amazonaws.com and event.action:CreateTrail and event.outcome:success", + "query": "event.dataset:aws.cloudtrail and event.provider:cloudtrail.amazonaws.com and event.action:CreateTrail and event.outcome:success\n", "references": [ "https://docs.aws.amazon.com/awscloudtrail/latest/APIReference/API_CreateTrail.html", "https://awscli.amazonaws.com/v2/documentation/api/latest/reference/cloudtrail/create-trail.html" diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/collection_gcp_pub_sub_subscription_creation.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/collection_gcp_pub_sub_subscription_creation.json index d50013c90b122..a24c533e2c272 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/collection_gcp_pub_sub_subscription_creation.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/collection_gcp_pub_sub_subscription_creation.json @@ -14,7 +14,7 @@ "license": "Elastic License v2", "name": "GCP Pub/Sub Subscription Creation", "note": "## Config\n\nThe GCP Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:(googlecloud.audit or gcp.audit) and event.action:google.pubsub.v*.Subscriber.CreateSubscription and event.outcome:success", + "query": "event.dataset:(googlecloud.audit or gcp.audit) and event.action:google.pubsub.v*.Subscriber.CreateSubscription and event.outcome:success\n", "references": [ "https://cloud.google.com/pubsub/docs/overview" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/collection_gcp_pub_sub_topic_creation.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/collection_gcp_pub_sub_topic_creation.json index 5438b8fb2a8c9..af20bdf46e42a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/collection_gcp_pub_sub_topic_creation.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/collection_gcp_pub_sub_topic_creation.json @@ -14,7 +14,7 @@ "license": "Elastic License v2", "name": "GCP Pub/Sub Topic Creation", "note": "## Config\n\nThe GCP Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:(googlecloud.audit or gcp.audit) and event.action:google.pubsub.v*.Publisher.CreateTopic and event.outcome:success", + "query": "event.dataset:(googlecloud.audit or gcp.audit) and event.action:google.pubsub.v*.Publisher.CreateTopic and event.outcome:success\n", "references": [ "https://cloud.google.com/pubsub/docs/admin" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/collection_microsoft_365_new_inbox_rule.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/collection_microsoft_365_new_inbox_rule.json index 5fcfbd797e9d3..1a9ccc9c70696 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/collection_microsoft_365_new_inbox_rule.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/collection_microsoft_365_new_inbox_rule.json @@ -17,7 +17,7 @@ "license": "Elastic License v2", "name": "Microsoft 365 New Inbox Rule Created", "note": "## Config\n\nThe Microsoft 365 Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:o365.audit and event.provider:Exchange and event.category:web and event.action:\"New-InboxRule\" and event.outcome:success", + "query": "event.dataset:o365.audit and event.provider:Exchange and event.category:web and event.action:\"New-InboxRule\" and event.outcome:success\n", "references": [ "https://docs.microsoft.com/en-us/microsoft-365/security/office-365-security/responding-to-a-compromised-email-account?view=o365-worldwide", "https://docs.microsoft.com/en-us/powershell/module/exchange/new-inboxrule?view=exchange-ps", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/collection_update_event_hub_auth_rule.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/collection_update_event_hub_auth_rule.json index a9fd96efcb383..7e8ab8d94d8f0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/collection_update_event_hub_auth_rule.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/collection_update_event_hub_auth_rule.json @@ -15,7 +15,7 @@ "license": "Elastic License v2", "name": "Azure Event Hub Authorization Rule Created or Updated", "note": "## Config\n\nThe Azure Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:azure.activitylogs and azure.activitylogs.operation_name:\"MICROSOFT.EVENTHUB/NAMESPACES/AUTHORIZATIONRULES/WRITE\" and event.outcome:(Success or success)", + "query": "event.dataset:azure.activitylogs and azure.activitylogs.operation_name:\"MICROSOFT.EVENTHUB/NAMESPACES/AUTHORIZATIONRULES/WRITE\" and event.outcome:(Success or success)\n", "references": [ "https://docs.microsoft.com/en-us/azure/event-hubs/authorize-access-shared-access-signature" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_certutil_network_connection.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_certutil_network_connection.json index c6622deed9502..2f265710feb0a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_certutil_network_connection.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_certutil_network_connection.json @@ -12,7 +12,10 @@ "language": "eql", "license": "Elastic License v2", "name": "Network Connection via Certutil", - "query": "sequence by process.entity_id\n [process where process.name : \"certutil.exe\" and event.type == \"start\"]\n [network where process.name : \"certutil.exe\" and\n not cidrmatch(destination.ip, \"10.0.0.0/8\", \"172.16.0.0/12\", \"192.168.0.0/16\")]\n", + "query": "sequence by process.entity_id\n [process where process.name : \"certutil.exe\" and event.type == \"start\"]\n [network where process.name : \"certutil.exe\" and\n not cidrmatch(destination.ip, \"10.0.0.0/8\", \"127.0.0.0/8\", \"169.254.0.0/16\", \"172.16.0.0/12\", \"192.0.0.0/24\",\n \"192.0.0.0/29\", \"192.0.0.8/32\", \"192.0.0.9/32\", \"192.0.0.10/32\", \"192.0.0.170/32\",\n \"192.0.0.171/32\", \"192.0.2.0/24\", \"192.31.196.0/24\", \"192.52.193.0/24\",\n \"192.168.0.0/16\", \"192.88.99.0/24\", \"224.0.0.0/4\", \"100.64.0.0/10\", \"192.175.48.0/24\",\n \"198.18.0.0/15\", \"198.51.100.0/24\", \"203.0.113.0/24\", \"240.0.0.0/4\", \"::1\",\n \"FE80::/10\", \"FF00::/8\")]\n", + "references": [ + "https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml" + ], "risk_score": 21, "rule_id": "3838e0e3-1850-4850-a411-2e8c5ba40ba8", "severity": "low", @@ -41,5 +44,5 @@ } ], "type": "eql", - "version": 5 + "version": 6 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_cobalt_strike_beacon.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_cobalt_strike_beacon.json index eba26c7be6e94..4e1780af8df82 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_cobalt_strike_beacon.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_cobalt_strike_beacon.json @@ -13,7 +13,7 @@ "license": "Elastic License v2", "name": "Cobalt Strike Command and Control Beacon", "note": "## Threat intel\n\nThis activity has been observed in FIN7 campaigns.", - "query": "event.category:(network OR network_traffic) AND type:(tls OR http) AND network.transport:tcp AND destination.domain:/[a-z]{3}.stage.[0-9]{8}\\..*/", + "query": "event.category:(network OR network_traffic) AND type:(tls OR http) AND network.transport:tcp AND destination.domain:/[a-z]{3}.stage.[0-9]{8}\\..*/\n", "references": [ "https://blog.morphisec.com/fin7-attacks-restaurant-industry", "https://www.fireeye.com/blog/threat-research/2017/04/fin7-phishing-lnk.html" diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_cobalt_strike_default_teamserver_cert.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_cobalt_strike_default_teamserver_cert.json index 6fffa5c4634a0..1d57f302554b7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_cobalt_strike_default_teamserver_cert.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_cobalt_strike_default_teamserver_cert.json @@ -11,7 +11,7 @@ "license": "Elastic License v2", "name": "Default Cobalt Strike Team Server Certificate", "note": "## Threat intel\n\nWhile Cobalt Strike is intended to be used for penetration tests and IR training, it is frequently used by actual threat actors (TA) such as APT19, APT29, APT32, APT41, FIN6, DarkHydrus, CopyKittens, Cobalt Group, Leviathan, and many other unnamed criminal TAs. This rule uses high-confidence atomic indicators, alerts should be investigated rapidly.", - "query": "event.category:(network or network_traffic) and (tls.server.hash.md5:950098276A495286EB2A2556FBAB6D83 or tls.server.hash.sha1:6ECE5ECE4192683D2D84E25B0BA7E04F9CB7EB7C or tls.server.hash.sha256:87F2085C32B6A2CC709B365F55873E207A9CAA10BFFECF2FD16D3CF9D94D390C)", + "query": "event.category:(network or network_traffic) and (tls.server.hash.md5:950098276A495286EB2A2556FBAB6D83 or\n tls.server.hash.sha1:6ECE5ECE4192683D2D84E25B0BA7E04F9CB7EB7C or\n tls.server.hash.sha256:87F2085C32B6A2CC709B365F55873E207A9CAA10BFFECF2FD16D3CF9D94D390C)\n", "references": [ "https://attack.mitre.org/software/S0154/", "https://www.cobaltstrike.com/help-setup-collaboration", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_dns_directly_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_dns_directly_to_the_internet.json index d283b489c278d..ec05c1645c699 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_dns_directly_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_dns_directly_to_the_internet.json @@ -13,10 +13,11 @@ "language": "kuery", "license": "Elastic License v2", "name": "DNS Activity to the Internet", - "query": "event.category:(network or network_traffic) and (event.type:connection or type:dns) and (destination.port:53 or event.dataset:zeek.dns) and source.ip:( 10.0.0.0/8 or 172.16.0.0/12 or 192.168.0.0/16 ) and not destination.ip:( 10.0.0.0/8 or 127.0.0.0/8 or 169.254.0.0/16 or 172.16.0.0/12 or 192.168.0.0/16 or 224.0.0.0/4 or 255.255.255.255 or \"::1\" or \"FE80::/10\" or \"FF00::/8\")", + "query": "event.category:(network or network_traffic) and (event.type:connection or type:dns) and (destination.port:53 or event.dataset:zeek.dns)\n and source.ip:(\n 10.0.0.0/8 or\n 172.16.0.0/12 or\n 192.168.0.0/16\n ) and\n not destination.ip:(\n 10.0.0.0/8 or\n 127.0.0.0/8 or\n 169.254.0.0/16 or\n 172.16.0.0/12 or\n 192.0.0.0/24 or\n 192.0.0.0/29 or\n 192.0.0.8/32 or\n 192.0.0.9/32 or\n 192.0.0.10/32 or\n 192.0.0.170/32 or\n 192.0.0.171/32 or\n 192.0.2.0/24 or\n 192.31.196.0/24 or\n 192.52.193.0/24 or\n 192.168.0.0/16 or\n 192.88.99.0/24 or\n 224.0.0.0/4 or\n 100.64.0.0/10 or\n 192.175.48.0/24 or\n 198.18.0.0/15 or\n 198.51.100.0/24 or\n 203.0.113.0/24 or\n 240.0.0.0/4 or\n \"::1\" or\n \"FE80::/10\" or\n \"FF00::/8\"\n )\n", "references": [ "https://www.us-cert.gov/ncas/alerts/TA15-240A", - "https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-81-2.pdf" + "https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-81-2.pdf", + "https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml" ], "risk_score": 47, "rule_id": "6ea71ff0-9e95-475b-9506-2580d1ce6154", @@ -40,5 +41,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 8 + "version": 9 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_dns_tunneling_nslookup.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_dns_tunneling_nslookup.json index 62654dfb2a9dd..0920f336bab44 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_dns_tunneling_nslookup.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_dns_tunneling_nslookup.json @@ -12,7 +12,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Potential DNS Tunneling via NsLookup", - "query": "event.category:process and event.type:start and process.name:nslookup.exe and process.args:(-querytype=* or -qt=* or -q=* or -type=*)", + "query": "event.category:process and event.type:start and process.name:nslookup.exe and process.args:(-querytype=* or -qt=* or -q=* or -type=*)\n", "references": [ "https://unit42.paloaltonetworks.com/dns-tunneling-in-the-wild-overview-of-oilrigs-dns-tunneling/" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_download_rar_powershell_from_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_download_rar_powershell_from_internet.json index 7cfba90cf67c8..50010d809f6fd 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_download_rar_powershell_from_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_download_rar_powershell_from_internet.json @@ -13,10 +13,11 @@ "license": "Elastic License v2", "name": "Roshal Archive (RAR) or PowerShell File Downloaded from the Internet", "note": "## Threat intel\n\nThis activity has been observed in FIN7 campaigns.", - "query": "event.category:(network OR network_traffic) AND network.protocol:http AND url.path:/.*(rar|ps1)/ AND source.ip:(10.0.0.0\\/8 OR 172.16.0.0\\/12 OR 192.168.0.0\\/16)", + "query": "event.category:(network or network_traffic) and network.protocol:http and\n (url.extension:(ps1 or rar) or url.path:(*.ps1 or *.rar)) and\n not destination.ip:(\n 10.0.0.0/8 or\n 127.0.0.0/8 or\n 169.254.0.0/16 or\n 172.16.0.0/12 or\n 192.0.0.0/24 or\n 192.0.0.0/29 or\n 192.0.0.8/32 or\n 192.0.0.9/32 or\n 192.0.0.10/32 or\n 192.0.0.170/32 or\n 192.0.0.171/32 or\n 192.0.2.0/24 or\n 192.31.196.0/24 or\n 192.52.193.0/24 or\n 192.168.0.0/16 or\n 192.88.99.0/24 or\n 224.0.0.0/4 or\n 100.64.0.0/10 or\n 192.175.48.0/24 or\n 198.18.0.0/15 or\n 198.51.100.0/24 or\n 203.0.113.0/24 or\n 240.0.0.0/4 or\n \"::1\" or\n \"FE80::/10\" or\n \"FF00::/8\"\n ) and\n source.ip:(\n 10.0.0.0/8 or\n 172.16.0.0/12 or\n 192.168.0.0/16\n )\n", "references": [ "https://www.fireeye.com/blog/threat-research/2017/04/fin7-phishing-lnk.html", - "https://www.justice.gov/opa/press-release/file/1084361/download" + "https://www.justice.gov/opa/press-release/file/1084361/download", + "https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml" ], "risk_score": 47, "rule_id": "ff013cb4-274d-434a-96bb-fe15ddd3ae92", @@ -46,5 +47,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 4 + "version": 5 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_fin7_c2_behavior.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_fin7_c2_behavior.json index 305612da0f47e..80087935eae0f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_fin7_c2_behavior.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_fin7_c2_behavior.json @@ -13,7 +13,7 @@ "license": "Elastic License v2", "name": "Possible FIN7 DGA Command and Control Behavior", "note": "## Triage and analysis\n\nIn the event this rule identifies benign domains in your environment, the `destination.domain` field in the rule can be modified to include those domains. Example: `...AND NOT destination.domain:(zoom.us OR benign.domain1 OR benign.domain2)`.", - "query": "event.category:(network OR network_traffic) AND type:(tls OR http) AND network.transport:tcp AND destination.domain:/[a-zA-Z]{4,5}\\.(pw|us|club|info|site|top)/ AND NOT destination.domain:zoom.us", + "query": "event.category:(network OR network_traffic) AND type:(tls OR http) AND network.transport:tcp\nAND destination.domain:/[a-zA-Z]{4,5}\\.(pw|us|club|info|site|top)/ AND NOT destination.domain:zoom.us\n", "references": [ "https://www.fireeye.com/blog/threat-research/2018/08/fin7-pursuing-an-enigmatic-and-evasive-global-criminal-operation.html" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_halfbaked_beacon.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_halfbaked_beacon.json index ed6fb726896ac..f7a770b4cfcc9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_halfbaked_beacon.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_halfbaked_beacon.json @@ -13,7 +13,7 @@ "license": "Elastic License v2", "name": "Halfbaked Command and Control Beacon", "note": "## Threat intel\n\nThis activity has been observed in FIN7 campaigns.", - "query": "event.category:(network OR network_traffic) AND network.protocol:http AND network.transport:tcp AND url.full:/http:\\/\\/[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}\\/cd/ AND destination.port:(53 OR 80 OR 8080 OR 443)", + "query": "event.category:(network OR network_traffic) AND network.protocol:http AND\n network.transport:tcp AND url.full:/http:\\/\\/[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}\\/cd/ AND\n destination.port:(53 OR 80 OR 8080 OR 443)\n", "references": [ "https://www.fireeye.com/blog/threat-research/2017/04/fin7-phishing-lnk.html", "https://attack.mitre.org/software/S0151/" diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_nat_traversal_port_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_nat_traversal_port_activity.json index 33b7cb020a69f..7cd0a865c4b3e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_nat_traversal_port_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_nat_traversal_port_activity.json @@ -15,7 +15,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "IPSEC NAT Traversal Port Activity", - "query": "event.category:(network or network_traffic) and network.transport:udp and destination.port:4500", + "query": "event.category:(network or network_traffic) and network.transport:udp and destination.port:4500\n", "risk_score": 21, "rule_id": "a9cb3641-ff4b-4cdc-a063-b4b8d02a67c7", "severity": "low", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_port_26_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_port_26_activity.json index 48a7fae4b259e..1ea0da645d785 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_port_26_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_port_26_activity.json @@ -15,7 +15,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "SMTP on Port 26/TCP", - "query": "event.category:(network or network_traffic) and network.transport:tcp and (destination.port:26 or (event.dataset:zeek.smtp and destination.port:26))", + "query": "event.category:(network or network_traffic) and network.transport:tcp and (destination.port:26 or (event.dataset:zeek.smtp and destination.port:26))\n", "references": [ "https://unit42.paloaltonetworks.com/unit42-badpatch/", "https://isc.sans.edu/forums/diary/Next+up+whats+up+with+TCP+port+26/25564/" diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_rdp_remote_desktop_protocol_from_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_rdp_remote_desktop_protocol_from_the_internet.json index 1ecfdc45352aa..625b1e470192f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_rdp_remote_desktop_protocol_from_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_rdp_remote_desktop_protocol_from_the_internet.json @@ -15,7 +15,10 @@ "language": "kuery", "license": "Elastic License v2", "name": "RDP (Remote Desktop Protocol) from the Internet", - "query": "event.category:(network or network_traffic) and network.transport:tcp and (destination.port:3389 or event.dataset:zeek.rdp) and not source.ip:( 10.0.0.0/8 or 127.0.0.0/8 or 169.254.0.0/16 or 172.16.0.0/12 or 192.168.0.0/16 or 224.0.0.0/4 or \"::1\" or \"FE80::/10\" or \"FF00::/8\" ) and destination.ip:( 10.0.0.0/8 or 172.16.0.0/12 or 192.168.0.0/16 )", + "query": "event.category:(network or network_traffic) and network.transport:tcp and (destination.port:3389 or event.dataset:zeek.rdp) and\n not source.ip:(\n 10.0.0.0/8 or\n 127.0.0.0/8 or\n 169.254.0.0/16 or\n 172.16.0.0/12 or\n 192.0.0.0/24 or\n 192.0.0.0/29 or\n 192.0.0.8/32 or\n 192.0.0.9/32 or\n 192.0.0.10/32 or\n 192.0.0.170/32 or\n 192.0.0.171/32 or\n 192.0.2.0/24 or\n 192.31.196.0/24 or\n 192.52.193.0/24 or\n 192.168.0.0/16 or\n 192.88.99.0/24 or\n 224.0.0.0/4 or\n 100.64.0.0/10 or\n 192.175.48.0/24 or\n 198.18.0.0/15 or\n 198.51.100.0/24 or\n 203.0.113.0/24 or\n 240.0.0.0/4 or\n \"::1\" or\n \"FE80::/10\" or\n \"FF00::/8\"\n ) and\n destination.ip:(\n 10.0.0.0/8 or\n 172.16.0.0/12 or\n 192.168.0.0/16\n )\n", + "references": [ + "https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml" + ], "risk_score": 47, "rule_id": "8c1bdde8-4204-45c0-9e0c-c85ca3902488", "severity": "medium", @@ -69,5 +72,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 8 + "version": 9 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_telnet_port_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_telnet_port_activity.json index f66e2ce891198..34adaa49df9c4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_telnet_port_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_telnet_port_activity.json @@ -15,7 +15,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Telnet Port Activity", - "query": "event.category:(network or network_traffic) and network.transport:tcp and destination.port:23", + "query": "event.category:(network or network_traffic) and network.transport:tcp and destination.port:23\n", "risk_score": 47, "rule_id": "34fde489-94b0-4500-a76f-b8a157cf9269", "severity": "medium", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_vnc_virtual_network_computing_from_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_vnc_virtual_network_computing_from_the_internet.json index e593a2fed8ba0..7a3da39baad33 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_vnc_virtual_network_computing_from_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_vnc_virtual_network_computing_from_the_internet.json @@ -15,7 +15,10 @@ "language": "kuery", "license": "Elastic License v2", "name": "VNC (Virtual Network Computing) from the Internet", - "query": "event.category:(network or network_traffic) and network.transport:tcp and destination.port >= 5800 and destination.port <= 5810 and not source.ip:( 10.0.0.0/8 or 127.0.0.0/8 or 169.254.0.0/16 or 172.16.0.0/12 or 192.168.0.0/16 or 224.0.0.0/4 or \"::1\" or \"FE80::/10\" or \"FF00::/8\" ) and destination.ip:( 10.0.0.0/8 or 172.16.0.0/12 or 192.168.0.0/16 )", + "query": "event.category:(network or network_traffic) and network.transport:tcp and destination.port >= 5800 and destination.port <= 5810 and\n not source.ip:(\n 10.0.0.0/8 or\n 127.0.0.0/8 or\n 169.254.0.0/16 or\n 172.16.0.0/12 or\n 192.0.0.0/24 or\n 192.0.0.0/29 or\n 192.0.0.8/32 or\n 192.0.0.9/32 or\n 192.0.0.10/32 or\n 192.0.0.170/32 or\n 192.0.0.171/32 or\n 192.0.2.0/24 or\n 192.31.196.0/24 or\n 192.52.193.0/24 or\n 192.168.0.0/16 or\n 192.88.99.0/24 or\n 224.0.0.0/4 or\n 100.64.0.0/10 or\n 192.175.48.0/24 or\n 198.18.0.0/15 or\n 198.51.100.0/24 or\n 203.0.113.0/24 or\n 240.0.0.0/4 or\n \"::1\" or\n \"FE80::/10\" or\n \"FF00::/8\"\n ) and\n destination.ip:(\n 10.0.0.0/8 or\n 172.16.0.0/12 or\n 192.168.0.0/16\n )\n", + "references": [ + "https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml" + ], "risk_score": 73, "rule_id": "5700cb81-df44-46aa-a5d7-337798f53eb8", "severity": "high", @@ -60,5 +63,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 8 + "version": 9 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_vnc_virtual_network_computing_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_vnc_virtual_network_computing_to_the_internet.json index 2e43ad410daec..4b30de0ee0211 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_vnc_virtual_network_computing_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_vnc_virtual_network_computing_to_the_internet.json @@ -15,7 +15,10 @@ "language": "kuery", "license": "Elastic License v2", "name": "VNC (Virtual Network Computing) to the Internet", - "query": "event.category:(network or network_traffic) and network.transport:tcp and destination.port >= 5800 and destination.port <= 5810 and source.ip:( 10.0.0.0/8 or 172.16.0.0/12 or 192.168.0.0/16 ) and not destination.ip:( 10.0.0.0/8 or 127.0.0.0/8 or 169.254.0.0/16 or 172.16.0.0/12 or 192.168.0.0/16 or 224.0.0.0/4 or \"::1\" or \"FE80::/10\" or \"FF00::/8\" )", + "query": "event.category:(network or network_traffic) and network.transport:tcp and destination.port >= 5800 and destination.port <= 5810 and\n source.ip:(\n 10.0.0.0/8 or\n 172.16.0.0/12 or\n 192.168.0.0/16\n ) and\n not destination.ip:(\n 10.0.0.0/8 or\n 127.0.0.0/8 or\n 169.254.0.0/16 or\n 172.16.0.0/12 or\n 192.0.0.0/24 or\n 192.0.0.0/29 or\n 192.0.0.8/32 or\n 192.0.0.9/32 or\n 192.0.0.10/32 or\n 192.0.0.170/32 or\n 192.0.0.171/32 or\n 192.0.2.0/24 or\n 192.31.196.0/24 or\n 192.52.193.0/24 or\n 192.168.0.0/16 or\n 192.88.99.0/24 or\n 224.0.0.0/4 or\n 100.64.0.0/10 or\n 192.175.48.0/24 or\n 198.18.0.0/15 or\n 198.51.100.0/24 or\n 203.0.113.0/24 or\n 240.0.0.0/4 or\n \"::1\" or\n \"FE80::/10\" or\n \"FF00::/8\"\n )\n", + "references": [ + "https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml" + ], "risk_score": 47, "rule_id": "3ad49c61-7adc-42c1-b788-732eda2f5abf", "severity": "medium", @@ -45,5 +48,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 8 + "version": 9 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_attempted_bypass_of_okta_mfa.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_attempted_bypass_of_okta_mfa.json index c3614e0e69e9a..fc3c3f5ca90d9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_attempted_bypass_of_okta_mfa.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_attempted_bypass_of_okta_mfa.json @@ -11,7 +11,7 @@ "license": "Elastic License v2", "name": "Attempted Bypass of Okta MFA", "note": "## Config\n\nThe Okta Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:okta.system and event.action:user.mfa.attempt_bypass", + "query": "event.dataset:okta.system and event.action:user.mfa.attempt_bypass\n", "references": [ "https://developer.okta.com/docs/reference/api/system-log/", "https://developer.okta.com/docs/reference/api/event-types/" diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_attempts_to_brute_force_okta_user_account.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_attempts_to_brute_force_okta_user_account.json index 5556f3787fbdd..ea2b05b485585 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_attempts_to_brute_force_okta_user_account.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_attempts_to_brute_force_okta_user_account.json @@ -1,6 +1,8 @@ { "author": [ - "Elastic" + "Elastic", + "@BenB196", + "Austin Songer" ], "description": "Identifies when an Okta user account is locked out 3 times within a 3 hour window. An adversary may attempt a brute force or password spraying attack to obtain unauthorized access to user accounts. The default Okta authentication policy ensures that a user account is locked out after 10 failed authentication attempts.", "from": "now-180m", @@ -12,7 +14,7 @@ "license": "Elastic License v2", "name": "Attempts to Brute Force an Okta User Account", "note": "## Config\n\nThe Okta Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:okta.system and event.action:user.account.lock", + "query": "event.dataset:okta.system and event.action:user.account.lock\n", "references": [ "https://developer.okta.com/docs/reference/api/system-log/", "https://developer.okta.com/docs/reference/api/event-types/" @@ -47,10 +49,10 @@ ], "threshold": { "field": [ - "okta.actor.id" + "okta.actor.alternate_id" ], "value": 3 }, "type": "threshold", - "version": 4 + "version": 5 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_aws_iam_assume_role_brute_force.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_aws_iam_assume_role_brute_force.json index 3ea689aaa3570..6ef3e3d255779 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_aws_iam_assume_role_brute_force.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_aws_iam_assume_role_brute_force.json @@ -12,7 +12,7 @@ "license": "Elastic License v2", "name": "AWS IAM Brute Force of Assume Role Policy", "note": "## Config\n\nThe AWS Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:aws.cloudtrail and event.provider:iam.amazonaws.com and event.action:UpdateAssumeRolePolicy and aws.cloudtrail.error_code:MalformedPolicyDocumentException and event.outcome:failure", + "query": "event.dataset:aws.cloudtrail and\n event.provider:iam.amazonaws.com and event.action:UpdateAssumeRolePolicy and\n aws.cloudtrail.error_code:MalformedPolicyDocumentException and event.outcome:failure\n", "references": [ "https://www.praetorian.com/blog/aws-iam-assume-role-vulnerabilities", "https://rhinosecuritylabs.com/aws/assume-worst-aws-assume-role-enumeration/" diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_collection_sensitive_files.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_collection_sensitive_files.json index b8a608dc47ab1..bd155ce8e914d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_collection_sensitive_files.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_collection_sensitive_files.json @@ -11,7 +11,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Sensitive Files Compression", - "query": "event.category:process and event.type:start and process.name:(zip or tar or gzip or hdiutil or 7z) and process.args: ( /root/.ssh/id_rsa or /root/.ssh/id_rsa.pub or /root/.ssh/id_ed25519 or /root/.ssh/id_ed25519.pub or /root/.ssh/authorized_keys or /root/.ssh/authorized_keys2 or /root/.ssh/known_hosts or /root/.bash_history or /etc/hosts or /home/*/.ssh/id_rsa or /home/*/.ssh/id_rsa.pub or /home/*/.ssh/id_ed25519 or /home/*/.ssh/id_ed25519.pub or /home/*/.ssh/authorized_keys or /home/*/.ssh/authorized_keys2 or /home/*/.ssh/known_hosts or /home/*/.bash_history or /root/.aws/credentials or /root/.aws/config or /home/*/.aws/credentials or /home/*/.aws/config or /root/.docker/config.json or /home/*/.docker/config.json or /etc/group or /etc/passwd or /etc/shadow or /etc/gshadow )", + "query": "event.category:process and event.type:start and\n process.name:(zip or tar or gzip or hdiutil or 7z) and\n process.args:\n (\n /root/.ssh/id_rsa or\n /root/.ssh/id_rsa.pub or\n /root/.ssh/id_ed25519 or\n /root/.ssh/id_ed25519.pub or\n /root/.ssh/authorized_keys or\n /root/.ssh/authorized_keys2 or\n /root/.ssh/known_hosts or\n /root/.bash_history or\n /etc/hosts or\n /home/*/.ssh/id_rsa or\n /home/*/.ssh/id_rsa.pub or\n /home/*/.ssh/id_ed25519 or\n /home/*/.ssh/id_ed25519.pub or\n /home/*/.ssh/authorized_keys or\n /home/*/.ssh/authorized_keys2 or\n /home/*/.ssh/known_hosts or\n /home/*/.bash_history or\n /root/.aws/credentials or\n /root/.aws/config or\n /home/*/.aws/credentials or\n /home/*/.aws/config or\n /root/.docker/config.json or\n /home/*/.docker/config.json or\n /etc/group or\n /etc/passwd or\n /etc/shadow or\n /etc/gshadow\n )\n", "references": [ "https://www.trendmicro.com/en_ca/research/20/l/teamtnt-now-deploying-ddos-capable-irc-bot-tntbotinger.html" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_dumping_hashes_bi_cmds.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_dumping_hashes_bi_cmds.json index c9070cc09dd65..bb513cbebdc3f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_dumping_hashes_bi_cmds.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_dumping_hashes_bi_cmds.json @@ -11,7 +11,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Dumping Account Hashes via Built-In Commands", - "query": "event.category:process and event.type:start and process.name:(defaults or mkpassdb) and process.args:(ShadowHashData or \"-dump\")", + "query": "event.category:process and event.type:start and\n process.name:(defaults or mkpassdb) and process.args:(ShadowHashData or \"-dump\")\n", "references": [ "https://apple.stackexchange.com/questions/186893/os-x-10-9-where-are-password-hashes-stored", "https://www.unix.com/man-page/osx/8/mkpassdb/" diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_iam_user_addition_to_group.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_iam_user_addition_to_group.json index 0d94ac36944a9..b66008c6931a4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_iam_user_addition_to_group.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_iam_user_addition_to_group.json @@ -16,7 +16,7 @@ "license": "Elastic License v2", "name": "AWS IAM User Addition to Group", "note": "## Config\n\nThe AWS Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:aws.cloudtrail and event.provider:iam.amazonaws.com and event.action:AddUserToGroup and event.outcome:success", + "query": "event.dataset:aws.cloudtrail and event.provider:iam.amazonaws.com and event.action:AddUserToGroup and event.outcome:success\n", "references": [ "https://docs.aws.amazon.com/IAM/latest/APIReference/API_AddUserToGroup.html" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_kerberosdump_kcc.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_kerberosdump_kcc.json index 9429e3414e615..de5a9d80ed3df 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_kerberosdump_kcc.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_kerberosdump_kcc.json @@ -11,7 +11,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Kerberos Cached Credentials Dumping", - "query": "event.category:process and event.type:(start or process_started) and process.name:kcc and process.args:copy_cred_cache", + "query": "event.category:process and event.type:(start or process_started) and\n process.name:kcc and\n process.args:copy_cred_cache\n", "references": [ "https://github.com/EmpireProject/EmPyre/blob/master/lib/modules/collection/osx/kerberosdump.py", "https://opensource.apple.com/source/Heimdal/Heimdal-323.12/kuser/kcc-commands.in.auto.html" diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_key_vault_modified.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_key_vault_modified.json index 27f76a13d657d..8efd74d4e7f1c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_key_vault_modified.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_key_vault_modified.json @@ -15,7 +15,7 @@ "license": "Elastic License v2", "name": "Azure Key Vault Modified", "note": "## Config\n\nThe Azure Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:azure.activitylogs and azure.activitylogs.operation_name:\"MICROSOFT.KEYVAULT/VAULTS/WRITE\" and event.outcome:(Success or success)", + "query": "event.dataset:azure.activitylogs and azure.activitylogs.operation_name:\"MICROSOFT.KEYVAULT/VAULTS/WRITE\" and event.outcome:(Success or success)\n", "references": [ "https://docs.microsoft.com/en-us/azure/key-vault/general/basic-concepts", "https://docs.microsoft.com/en-us/azure/key-vault/general/secure-your-key-vault" diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_microsoft_365_brute_force_user_account_attempt.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_microsoft_365_brute_force_user_account_attempt.json index 8e9c142929a05..532b9bf3b17b3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_microsoft_365_brute_force_user_account_attempt.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_microsoft_365_brute_force_user_account_attempt.json @@ -15,7 +15,7 @@ "license": "Elastic License v2", "name": "Attempts to Brute Force a Microsoft 365 User Account", "note": "## Config\n\nThe Microsoft 365 Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:o365.audit and event.provider:AzureActiveDirectory and event.category:authentication and event.action:UserLoginFailed and event.outcome:failure", + "query": "event.dataset:o365.audit and event.provider:(Exchange or AzureActiveDirectory) and event.category:authentication and \nevent.action:(\"UserLoginFailed\" or \"PasswordLogonInitialAuthUsingPassword\") and event.outcome:failure\n", "risk_score": 73, "rule_id": "26f68dba-ce29-497b-8e13-b4fde1db5a2d", "severity": "high", @@ -51,5 +51,5 @@ "value": 10 }, "type": "threshold", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_microsoft_365_potential_password_spraying_attack.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_microsoft_365_potential_password_spraying_attack.json index 14314feebb712..536f893236dee 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_microsoft_365_potential_password_spraying_attack.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_microsoft_365_potential_password_spraying_attack.json @@ -15,7 +15,7 @@ "license": "Elastic License v2", "name": "Potential Password Spraying of Microsoft 365 User Accounts", "note": "## Config\n\nThe Microsoft 365 Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:o365.audit and event.provider:AzureActiveDirectory and event.category:authentication and event.action:UserLoginFailed and event.outcome:failure", + "query": "event.dataset:o365.audit and event.provider:AzureActiveDirectory and event.category:authentication and event.action:UserLoginFailed and event.outcome:failure\n", "risk_score": 73, "rule_id": "3efee4f0-182a-40a8-a835-102c68a4175d", "severity": "high", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_mitm_localhost_webproxy.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_mitm_localhost_webproxy.json index e226df7a23da9..9c0f2d0c07dc9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_mitm_localhost_webproxy.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_mitm_localhost_webproxy.json @@ -14,7 +14,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "WebProxy Settings Modification", - "query": "event.category : process and event.type : start and process.name : networksetup and process.args : ((\"-setwebproxy\" or \"-setsecurewebproxy\" or \"-setautoproxyurl\") and not (Bluetooth or off)) and not process.parent.executable : (\"/Library/PrivilegedHelperTools/com.80pct.FreedomHelper\" or \"/Applications/Fiddler Everywhere.app/Contents/Resources/app/out/WebServer/Fiddler.WebUi\" or \"/usr/libexec/xpcproxy\")", + "query": "event.category : process and event.type : start and\n process.name : networksetup and process.args : ((\"-setwebproxy\" or \"-setsecurewebproxy\" or \"-setautoproxyurl\") and not (Bluetooth or off)) and\n not process.parent.executable : (\"/Library/PrivilegedHelperTools/com.80pct.FreedomHelper\" or\n \"/Applications/Fiddler Everywhere.app/Contents/Resources/app/out/WebServer/Fiddler.WebUi\" or\n \"/usr/libexec/xpcproxy\")\n", "references": [ "https://unit42.paloaltonetworks.com/mac-malware-steals-cryptocurrency-exchanges-cookies/", "https://objectivebythesea.com/v2/talks/OBTS_v2_Zohar.pdf" diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_okta_brute_force_or_password_spraying.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_okta_brute_force_or_password_spraying.json index 16c2816e30690..f5449e4e30a1c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_okta_brute_force_or_password_spraying.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_okta_brute_force_or_password_spraying.json @@ -14,7 +14,7 @@ "license": "Elastic License v2", "name": "Okta Brute Force or Password Spraying Attack", "note": "## Config\n\nThe Okta Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:okta.system and event.category:authentication and event.outcome:failure", + "query": "event.dataset:okta.system and event.category:authentication and event.outcome:failure\n", "references": [ "https://developer.okta.com/docs/reference/api/system-log/", "https://developer.okta.com/docs/reference/api/event-types/" diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_potential_ssh_bruteforce.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_potential_ssh_bruteforce.json index 410db35bf77d3..4f945e22abdb3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_potential_ssh_bruteforce.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_potential_ssh_bruteforce.json @@ -11,7 +11,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Potential SSH Brute Force Detected", - "query": "event.category:process and event.type:start and process.name:\"sshd-keygen-wrapper\" and process.parent.name:launchd", + "query": "event.category:process and event.type:start and process.name:\"sshd-keygen-wrapper\" and process.parent.name:launchd\n", "references": [ "https://themittenmac.com/detecting-ssh-activity-via-process-monitoring/" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_root_console_failure_brute_force.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_root_console_failure_brute_force.json index fb296d82b992b..b20ef22a9d152 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_root_console_failure_brute_force.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_root_console_failure_brute_force.json @@ -15,7 +15,7 @@ "license": "Elastic License v2", "name": "AWS Management Console Brute Force of Root User Identity", "note": "## Config\n\nThe AWS Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:aws.cloudtrail and event.provider:signin.amazonaws.com and event.action:ConsoleLogin and aws.cloudtrail.user_identity.type:Root and event.outcome:failure", + "query": "event.dataset:aws.cloudtrail and event.provider:signin.amazonaws.com and event.action:ConsoleLogin and aws.cloudtrail.user_identity.type:Root and event.outcome:failure\n", "references": [ "https://docs.aws.amazon.com/IAM/latest/UserGuide/id_root-user.html" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_secretsmanager_getsecretvalue.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_secretsmanager_getsecretvalue.json index 8eebcc5c45096..6d0d4c8d112a4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_secretsmanager_getsecretvalue.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_secretsmanager_getsecretvalue.json @@ -17,7 +17,7 @@ "license": "Elastic License v2", "name": "AWS Access Secret in Secrets Manager", "note": "## Config\n\nThe AWS Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:aws.cloudtrail and event.provider:secretsmanager.amazonaws.com and event.action:GetSecretValue", + "query": "event.dataset:aws.cloudtrail and event.provider:secretsmanager.amazonaws.com and event.action:GetSecretValue\n", "references": [ "https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_GetSecretValue.html", "http://detectioninthe.cloud/credential_access/access_secret_in_secrets_manager/" diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_storage_account_key_regenerated.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_storage_account_key_regenerated.json index d6f5a05c86e1a..3f286081c49f2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_storage_account_key_regenerated.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_storage_account_key_regenerated.json @@ -15,7 +15,7 @@ "license": "Elastic License v2", "name": "Azure Storage Account Key Regenerated", "note": "## Config\n\nThe Azure Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:azure.activitylogs and azure.activitylogs.operation_name:\"MICROSOFT.STORAGE/STORAGEACCOUNTS/REGENERATEKEY/ACTION\" and event.outcome:(Success or success)", + "query": "event.dataset:azure.activitylogs and azure.activitylogs.operation_name:\"MICROSOFT.STORAGE/STORAGEACCOUNTS/REGENERATEKEY/ACTION\" and event.outcome:(Success or success)\n", "references": [ "https://docs.microsoft.com/en-us/azure/storage/common/storage-account-keys-manage?tabs=azure-portal" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_systemkey_dumping.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_systemkey_dumping.json index 9094124c480bd..9eaa51279bf4b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_systemkey_dumping.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_systemkey_dumping.json @@ -11,7 +11,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "SystemKey Access via Command Line", - "query": "event.category:process and event.type:(start or process_started) and process.args:\"/private/var/db/SystemKey\"", + "query": "event.category:process and event.type:(start or process_started) and\n process.args:\"/private/var/db/SystemKey\"\n", "references": [ "https://github.com/AlessandroZ/LaZagne/blob/master/Mac/lazagne/softwares/system/chainbreaker.py" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_amsienable_key_mod.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_amsienable_key_mod.json new file mode 100644 index 0000000000000..6f30b53d24bdb --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_amsienable_key_mod.json @@ -0,0 +1,57 @@ +{ + "author": [ + "Elastic" + ], + "description": "Jscript tries to query the AmsiEnable registry key from the HKEY_USERS registry hive before initializing Antimalware Scan Interface (AMSI). If this key is set to 0, AMSI is not enabled for the Jscript process. An adversary can modify this key to disable AMSI protections.", + "from": "now-9m", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*", + "logs-windows.*" + ], + "language": "eql", + "license": "Elastic License v2", + "name": "Modification of AmsiEnable Registry Key", + "query": "registry where event.type in (\"creation\", \"change\") and\n registry.path: \"HKEY_USERS\\\\*\\\\Software\\\\Microsoft\\\\Windows Script\\\\Settings\\\\AmsiEnable\" and\n registry.data.strings: \"0\"\n", + "references": [ + "https://hackinparis.com/data/slides/2019/talks/HIP2019-Dominic_Chell-Cracking_The_Perimeter_With_Sharpshooter.pdf", + "https://docs.microsoft.com/en-us/windows/win32/amsi/antimalware-scan-interface-portal" + ], + "risk_score": 73, + "rule_id": "f874315d-5188-4b4a-8521-d1c73093a7e4", + "severity": "high", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Defense Evasion" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0005", + "name": "Defense Evasion", + "reference": "https://attack.mitre.org/tactics/TA0005/" + }, + "technique": [ + { + "id": "T1562", + "name": "Impair Defenses", + "reference": "https://attack.mitre.org/techniques/T1562/", + "subtechnique": [ + { + "id": "T1562.001", + "name": "Disable or Modify Tools", + "reference": "https://attack.mitre.org/techniques/T1562/001/" + } + ] + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_apple_softupdates_modification.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_apple_softupdates_modification.json index 9340425c518a7..f354f521d168e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_apple_softupdates_modification.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_apple_softupdates_modification.json @@ -14,7 +14,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "SoftwareUpdate Preferences Modification", - "query": "event.category:process and event.type:(start or process_started) and process.name:defaults and process.args:(write and \"-bool\" and (com.apple.SoftwareUpdate or /Library/Preferences/com.apple.SoftwareUpdate.plist) and not (TRUE or true))", + "query": "event.category:process and event.type:(start or process_started) and\n process.name:defaults and \n process.args:(write and \"-bool\" and (com.apple.SoftwareUpdate or /Library/Preferences/com.apple.SoftwareUpdate.plist) and not (TRUE or true))\n", "references": [ "https://blog.checkpoint.com/2017/07/13/osxdok-refuses-go-away-money/" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_attempt_to_disable_gatekeeper.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_attempt_to_disable_gatekeeper.json index ab423dc0ebe14..9bb48361ccf89 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_attempt_to_disable_gatekeeper.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_attempt_to_disable_gatekeeper.json @@ -11,7 +11,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Attempt to Disable Gatekeeper", - "query": "event.category:process and event.type:(start or process_started) and process.args:(spctl and \"--master-disable\")", + "query": "event.category:process and event.type:(start or process_started) and \n process.args:(spctl and \"--master-disable\")\n", "references": [ "https://support.apple.com/en-us/HT202491", "https://www.carbonblack.com/blog/tau-threat-intelligence-notification-new-macos-malware-variant-of-shlayer-osx-discovered/" diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_attempt_to_disable_iptables_or_firewall.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_attempt_to_disable_iptables_or_firewall.json index 8913c63f811dd..3eb5f25298c72 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_attempt_to_disable_iptables_or_firewall.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_attempt_to_disable_iptables_or_firewall.json @@ -11,7 +11,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Attempt to Disable IPTables or Firewall", - "query": "event.category:process and event.type:(start or process_started) and process.name:ufw and process.args:(allow or disable or reset) or (((process.name:service and process.args:stop) or (process.name:chkconfig and process.args:off) or (process.name:systemctl and process.args:(disable or stop or kill))) and process.args:(firewalld or ip6tables or iptables))", + "query": "event.category:process and event.type:(start or process_started) and\n process.name:ufw and process.args:(allow or disable or reset) or\n\n (((process.name:service and process.args:stop) or\n (process.name:chkconfig and process.args:off) or\n (process.name:systemctl and process.args:(disable or stop or kill))) and\n process.args:(firewalld or ip6tables or iptables))\n", "risk_score": 47, "rule_id": "125417b8-d3df-479f-8418-12d7e034fee3", "severity": "medium", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_attempt_to_disable_syslog_service.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_attempt_to_disable_syslog_service.json index b465340a83223..610648df94d37 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_attempt_to_disable_syslog_service.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_attempt_to_disable_syslog_service.json @@ -11,7 +11,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Attempt to Disable Syslog Service", - "query": "event.category:process and event.type:(start or process_started) and ((process.name:service and process.args:stop) or (process.name:chkconfig and process.args:off) or (process.name:systemctl and process.args:(disable or stop or kill))) and process.args:(syslog or rsyslog or \"syslog-ng\")", + "query": "event.category:process and event.type:(start or process_started) and\n ((process.name:service and process.args:stop) or\n (process.name:chkconfig and process.args:off) or\n (process.name:systemctl and process.args:(disable or stop or kill)))\n and process.args:(syslog or rsyslog or \"syslog-ng\")\n", "risk_score": 47, "rule_id": "2f8a1226-5720-437d-9c20-e0029deb6194", "severity": "medium", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_azure_application_credential_modification.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_azure_application_credential_modification.json index d695695e2c23b..5f96b81247942 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_azure_application_credential_modification.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_azure_application_credential_modification.json @@ -15,7 +15,7 @@ "license": "Elastic License v2", "name": "Azure Application Credential Modification", "note": "## Config\n\nThe Azure Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:azure.auditlogs and azure.auditlogs.operation_name:\"Update application - Certificates and secrets management\" and event.outcome:(success or Success)", + "query": "event.dataset:azure.auditlogs and azure.auditlogs.operation_name:\"Update application - Certificates and secrets management\" and event.outcome:(success or Success)\n", "references": [ "https://msrc-blog.microsoft.com/2020/12/13/customer-guidance-on-recent-nation-state-cyber-attacks/" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_azure_diagnostic_settings_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_azure_diagnostic_settings_deletion.json index 31eec6ee4d1c2..d9f70f88a23b7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_azure_diagnostic_settings_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_azure_diagnostic_settings_deletion.json @@ -15,7 +15,7 @@ "license": "Elastic License v2", "name": "Azure Diagnostic Settings Deletion", "note": "## Config\n\nThe Azure Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:azure.activitylogs and azure.activitylogs.operation_name:\"MICROSOFT.INSIGHTS/DIAGNOSTICSETTINGS/DELETE\" and event.outcome:(Success or success)", + "query": "event.dataset:azure.activitylogs and azure.activitylogs.operation_name:\"MICROSOFT.INSIGHTS/DIAGNOSTICSETTINGS/DELETE\" and event.outcome:(Success or success)\n", "references": [ "https://docs.microsoft.com/en-us/azure/azure-monitor/platform/diagnostic-settings" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_azure_service_principal_addition.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_azure_service_principal_addition.json index 43557bfeb31ba..c715ea42e4b7e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_azure_service_principal_addition.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_azure_service_principal_addition.json @@ -15,7 +15,7 @@ "license": "Elastic License v2", "name": "Azure Service Principal Addition", "note": "## Config\n\nThe Azure Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:azure.auditlogs and azure.auditlogs.operation_name:\"Add service principal\" and event.outcome:(success or Success)", + "query": "event.dataset:azure.auditlogs and azure.auditlogs.operation_name:\"Add service principal\" and event.outcome:(success or Success)\n", "references": [ "https://msrc-blog.microsoft.com/2020/12/13/customer-guidance-on-recent-nation-state-cyber-attacks/", "https://docs.microsoft.com/en-us/azure/active-directory/develop/howto-create-service-principal-portal" diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_base16_or_base32_encoding_or_decoding_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_base16_or_base32_encoding_or_decoding_activity.json index bedb41cb726a6..1cc353f1add3a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_base16_or_base32_encoding_or_decoding_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_base16_or_base32_encoding_or_decoding_activity.json @@ -14,7 +14,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Base16 or Base32 Encoding/Decoding Activity", - "query": "event.category:process and event.type:(start or process_started) and process.name:(base16 or base32 or base32plain or base32hex)", + "query": "event.category:process and event.type:(start or process_started) and\n process.name:(base16 or base32 or base32plain or base32hex)\n", "risk_score": 21, "rule_id": "debff20a-46bc-4a4d-bae5-5cdd14222795", "severity": "low", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_clearing_windows_security_logs.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_clearing_windows_security_logs.json index ca7485a29f4fc..d04c2b2a38915 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_clearing_windows_security_logs.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_clearing_windows_security_logs.json @@ -12,7 +12,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Windows Event Logs Cleared", - "query": "event.action:(\"audit-log-cleared\" or \"Log clear\")", + "query": "event.action:(\"audit-log-cleared\" or \"Log clear\")\n", "risk_score": 21, "rule_id": "45ac4800-840f-414c-b221-53dd36a5aaf7", "severity": "low", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_cloudtrail_logging_deleted.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_cloudtrail_logging_deleted.json index 8daf17dc8c386..744543ab8a1f0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_cloudtrail_logging_deleted.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_cloudtrail_logging_deleted.json @@ -16,7 +16,7 @@ "license": "Elastic License v2", "name": "AWS CloudTrail Log Deleted", "note": "## Config\n\nThe AWS Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:aws.cloudtrail and event.provider:cloudtrail.amazonaws.com and event.action:DeleteTrail and event.outcome:success", + "query": "event.dataset:aws.cloudtrail and event.provider:cloudtrail.amazonaws.com and event.action:DeleteTrail and event.outcome:success\n", "references": [ "https://docs.aws.amazon.com/awscloudtrail/latest/APIReference/API_DeleteTrail.html", "https://awscli.amazonaws.com/v2/documentation/api/latest/reference/cloudtrail/delete-trail.html" diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_cloudtrail_logging_suspended.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_cloudtrail_logging_suspended.json index c140a1f9b9734..27990a74ac5b2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_cloudtrail_logging_suspended.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_cloudtrail_logging_suspended.json @@ -16,7 +16,7 @@ "license": "Elastic License v2", "name": "AWS CloudTrail Log Suspended", "note": "## Config\n\nThe AWS Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:aws.cloudtrail and event.provider:cloudtrail.amazonaws.com and event.action:StopLogging and event.outcome:success", + "query": "event.dataset:aws.cloudtrail and event.provider:cloudtrail.amazonaws.com and event.action:StopLogging and event.outcome:success\n", "references": [ "https://docs.aws.amazon.com/awscloudtrail/latest/APIReference/API_StopLogging.html", "https://awscli.amazonaws.com/v2/documentation/api/latest/reference/cloudtrail/stop-logging.html" diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_cloudwatch_alarm_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_cloudwatch_alarm_deletion.json index b5984d5d624c3..61806b640fae2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_cloudwatch_alarm_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_cloudwatch_alarm_deletion.json @@ -16,7 +16,7 @@ "license": "Elastic License v2", "name": "AWS CloudWatch Alarm Deletion", "note": "## Config\n\nThe AWS Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:aws.cloudtrail and event.provider:monitoring.amazonaws.com and event.action:DeleteAlarms and event.outcome:success", + "query": "event.dataset:aws.cloudtrail and event.provider:monitoring.amazonaws.com and event.action:DeleteAlarms and event.outcome:success\n", "references": [ "https://awscli.amazonaws.com/v2/documentation/api/latest/reference/cloudwatch/delete-alarms.html", "https://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/API_DeleteAlarms.html" diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_config_service_rule_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_config_service_rule_deletion.json index a512cd8c842ee..f58164d1a483f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_config_service_rule_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_config_service_rule_deletion.json @@ -17,7 +17,7 @@ "license": "Elastic License v2", "name": "AWS Config Service Tampering", "note": "## Config\n\nThe AWS Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:aws.cloudtrail and event.provider:config.amazonaws.com and event.action:(DeleteConfigRule or DeleteOrganizationConfigRule or DeleteConfigurationAggregator or DeleteConfigurationRecorder or DeleteConformancePack or DeleteOrganizationConformancePack or DeleteDeliveryChannel or DeleteRemediationConfiguration or DeleteRetentionConfiguration)", + "query": "event.dataset:aws.cloudtrail and event.provider:config.amazonaws.com and\n event.action:(DeleteConfigRule or DeleteOrganizationConfigRule or DeleteConfigurationAggregator or\n DeleteConfigurationRecorder or DeleteConformancePack or DeleteOrganizationConformancePack or\n DeleteDeliveryChannel or DeleteRemediationConfiguration or DeleteRetentionConfiguration)\n", "references": [ "https://docs.aws.amazon.com/config/latest/developerguide/how-does-config-work.html", "https://docs.aws.amazon.com/config/latest/APIReference/API_Operations.html" diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_configuration_recorder_stopped.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_configuration_recorder_stopped.json index abadb416deec8..c222b25721292 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_configuration_recorder_stopped.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_configuration_recorder_stopped.json @@ -16,7 +16,7 @@ "license": "Elastic License v2", "name": "AWS Configuration Recorder Stopped", "note": "## Config\n\nThe AWS Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:aws.cloudtrail and event.provider:config.amazonaws.com and event.action:StopConfigurationRecorder and event.outcome:success", + "query": "event.dataset:aws.cloudtrail and event.provider:config.amazonaws.com and event.action:StopConfigurationRecorder and event.outcome:success\n", "references": [ "https://awscli.amazonaws.com/v2/documentation/api/latest/reference/configservice/stop-configuration-recorder.html", "https://docs.aws.amazon.com/config/latest/APIReference/API_StopConfigurationRecorder.html" diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_cve_2020_0601.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_cve_2020_0601.json index c7ac34a39563e..fba792e80d490 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_cve_2020_0601.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_cve_2020_0601.json @@ -10,7 +10,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Windows CryptoAPI Spoofing Vulnerability (CVE-2020-0601 - CurveBall)", - "query": "event.provider:\"Microsoft-Windows-Audit-CVE\" and message:\"[CVE-2020-0601]\"", + "query": "event.provider:\"Microsoft-Windows-Audit-CVE\" and message:\"[CVE-2020-0601]\"\n", "risk_score": 21, "rule_id": "56557cde-d923-4b88-adee-c61b3f3b5dc3", "severity": "low", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_disable_selinux_attempt.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_disable_selinux_attempt.json index 0a51b0ccc87f5..cc84001428ff0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_disable_selinux_attempt.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_disable_selinux_attempt.json @@ -11,7 +11,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Potential Disabling of SELinux", - "query": "event.category:process and event.type:(start or process_started) and process.name:setenforce and process.args:0", + "query": "event.category:process and event.type:(start or process_started) and process.name:setenforce and process.args:0\n", "risk_score": 47, "rule_id": "eb9eb8ba-a983-41d9-9c93-a1c05112ca5e", "severity": "medium", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_ec2_flow_log_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_ec2_flow_log_deletion.json index f88b8b3589d93..8c74783642395 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_ec2_flow_log_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_ec2_flow_log_deletion.json @@ -16,7 +16,7 @@ "license": "Elastic License v2", "name": "AWS EC2 Flow Log Deletion", "note": "## Config\n\nThe AWS Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:aws.cloudtrail and event.provider:ec2.amazonaws.com and event.action:DeleteFlowLogs and event.outcome:success", + "query": "event.dataset:aws.cloudtrail and event.provider:ec2.amazonaws.com and event.action:DeleteFlowLogs and event.outcome:success\n", "references": [ "https://awscli.amazonaws.com/v2/documentation/api/latest/reference/ec2/delete-flow-logs.html", "https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DeleteFlowLogs.html" diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_ec2_network_acl_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_ec2_network_acl_deletion.json index 06fc3a3f094a5..b64f7eed4be8d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_ec2_network_acl_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_ec2_network_acl_deletion.json @@ -16,7 +16,7 @@ "license": "Elastic License v2", "name": "AWS EC2 Network Access Control List Deletion", "note": "## Config\n\nThe AWS Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:aws.cloudtrail and event.provider:ec2.amazonaws.com and event.action:(DeleteNetworkAcl or DeleteNetworkAclEntry) and event.outcome:success", + "query": "event.dataset:aws.cloudtrail and event.provider:ec2.amazonaws.com and event.action:(DeleteNetworkAcl or DeleteNetworkAclEntry) and event.outcome:success\n", "references": [ "https://awscli.amazonaws.com/v2/documentation/api/latest/reference/ec2/delete-network-acl.html", "https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DeleteNetworkAcl.html", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_event_hub_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_event_hub_deletion.json index 0ea25e05915c6..a8a2f945c76e6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_event_hub_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_event_hub_deletion.json @@ -15,7 +15,7 @@ "license": "Elastic License v2", "name": "Azure Event Hub Deletion", "note": "## Config\n\nThe Azure Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:azure.activitylogs and azure.activitylogs.operation_name:\"MICROSOFT.EVENTHUB/NAMESPACES/EVENTHUBS/DELETE\" and event.outcome:(Success or success)", + "query": "event.dataset:azure.activitylogs and azure.activitylogs.operation_name:\"MICROSOFT.EVENTHUB/NAMESPACES/EVENTHUBS/DELETE\" and event.outcome:(Success or success)\n", "references": [ "https://docs.microsoft.com/en-us/azure/event-hubs/event-hubs-about", "https://azure.microsoft.com/en-in/services/event-hubs/", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_file_deletion_via_shred.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_file_deletion_via_shred.json index 0b934676ac1eb..6d86f27cd52b4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_file_deletion_via_shred.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_file_deletion_via_shred.json @@ -11,7 +11,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "File Deletion via Shred", - "query": "event.category:process and event.type:(start or process_started) and process.name:shred and process.args:(\"-u\" or \"--remove\" or \"-z\" or \"--zero\")", + "query": "event.category:process and event.type:(start or process_started) and process.name:shred and\n process.args:(\"-u\" or \"--remove\" or \"-z\" or \"--zero\")\n", "risk_score": 21, "rule_id": "a1329140-8de3-4445-9f87-908fb6d824f4", "severity": "low", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_file_mod_writable_dir.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_file_mod_writable_dir.json index cd83733d81830..be7e8b71694ba 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_file_mod_writable_dir.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_file_mod_writable_dir.json @@ -14,7 +14,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "File Permission Modification in Writable Directory", - "query": "event.category:process and event.type:(start or process_started) and process.name:(chmod or chown or chattr or chgrp) and process.working_directory:(/tmp or /var/tmp or /dev/shm) and not user.name:root", + "query": "event.category:process and event.type:(start or process_started) and\n process.name:(chmod or chown or chattr or chgrp) and\n process.working_directory:(/tmp or /var/tmp or /dev/shm) and\n not user.name:root\n", "risk_score": 21, "rule_id": "9f9a2a82-93a8-4b1a-8778-1780895626d4", "severity": "low", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_firewall_policy_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_firewall_policy_deletion.json index f8961a832ac1e..98ee5f6eb8cbf 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_firewall_policy_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_firewall_policy_deletion.json @@ -15,7 +15,7 @@ "license": "Elastic License v2", "name": "Azure Firewall Policy Deletion", "note": "## Config\n\nThe Azure Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:azure.activitylogs and azure.activitylogs.operation_name:\"MICROSOFT.NETWORK/FIREWALLPOLICIES/DELETE\" and event.outcome:(Success or success)", + "query": "event.dataset:azure.activitylogs and azure.activitylogs.operation_name:\"MICROSOFT.NETWORK/FIREWALLPOLICIES/DELETE\" and event.outcome:(Success or success)\n", "references": [ "https://docs.microsoft.com/en-us/azure/firewall-manager/policy-overview" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_gcp_firewall_rule_created.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_gcp_firewall_rule_created.json index 7fbe044738386..ff2a20f85d3e3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_gcp_firewall_rule_created.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_gcp_firewall_rule_created.json @@ -14,7 +14,7 @@ "license": "Elastic License v2", "name": "GCP Firewall Rule Creation", "note": "## Config\n\nThe GCP Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:(googlecloud.audit or gcp.audit) and event.action:v*.compute.firewalls.insert", + "query": "event.dataset:(googlecloud.audit or gcp.audit) and event.action:v*.compute.firewalls.insert\n", "references": [ "https://cloud.google.com/vpc/docs/firewalls" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_gcp_firewall_rule_deleted.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_gcp_firewall_rule_deleted.json index 4cc8fde077c3e..3fc9b6bc49d3d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_gcp_firewall_rule_deleted.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_gcp_firewall_rule_deleted.json @@ -14,7 +14,7 @@ "license": "Elastic License v2", "name": "GCP Firewall Rule Deletion", "note": "## Config\n\nThe GCP Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:(googlecloud.audit or gcp.audit) and event.action:v*.compute.firewalls.delete", + "query": "event.dataset:(googlecloud.audit or gcp.audit) and event.action:v*.compute.firewalls.delete\n", "references": [ "https://cloud.google.com/vpc/docs/firewalls" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_gcp_firewall_rule_modified.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_gcp_firewall_rule_modified.json index 614da7f79a46b..e6ad1fc554183 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_gcp_firewall_rule_modified.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_gcp_firewall_rule_modified.json @@ -14,7 +14,7 @@ "license": "Elastic License v2", "name": "GCP Firewall Rule Modification", "note": "## Config\n\nThe GCP Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:(googlecloud.audit or gcp.audit) and event.action:v*.compute.firewalls.patch", + "query": "event.dataset:(googlecloud.audit or gcp.audit) and event.action:v*.compute.firewalls.patch\n", "references": [ "https://cloud.google.com/vpc/docs/firewalls" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_gcp_logging_bucket_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_gcp_logging_bucket_deletion.json index 805f21f875f02..d2fd746f8971e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_gcp_logging_bucket_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_gcp_logging_bucket_deletion.json @@ -14,7 +14,7 @@ "license": "Elastic License v2", "name": "GCP Logging Bucket Deletion", "note": "## Config\n\nThe GCP Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:(googlecloud.audit or gcp.audit) and event.action:google.logging.v*.ConfigServiceV*.DeleteBucket and event.outcome:success", + "query": "event.dataset:(googlecloud.audit or gcp.audit) and event.action:google.logging.v*.ConfigServiceV*.DeleteBucket and event.outcome:success\n", "references": [ "https://cloud.google.com/logging/docs/buckets", "https://cloud.google.com/logging/docs/storage" diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_gcp_logging_sink_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_gcp_logging_sink_deletion.json index 6fa62fe3a6313..3e103413967fe 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_gcp_logging_sink_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_gcp_logging_sink_deletion.json @@ -14,7 +14,7 @@ "license": "Elastic License v2", "name": "GCP Logging Sink Deletion", "note": "## Config\n\nThe GCP Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:(googlecloud.audit or gcp.audit) and event.action:google.logging.v*.ConfigServiceV*.DeleteSink and event.outcome:success", + "query": "event.dataset:(googlecloud.audit or gcp.audit) and event.action:google.logging.v*.ConfigServiceV*.DeleteSink and event.outcome:success\n", "references": [ "https://cloud.google.com/logging/docs/export" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_gcp_pub_sub_subscription_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_gcp_pub_sub_subscription_deletion.json index 4907e0f13e550..78435128865f2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_gcp_pub_sub_subscription_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_gcp_pub_sub_subscription_deletion.json @@ -14,7 +14,7 @@ "license": "Elastic License v2", "name": "GCP Pub/Sub Subscription Deletion", "note": "## Config\n\nThe GCP Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:(googlecloud.audit or gcp.audit) and event.action:google.pubsub.v*.Subscriber.DeleteSubscription and event.outcome:success", + "query": "event.dataset:(googlecloud.audit or gcp.audit) and event.action:google.pubsub.v*.Subscriber.DeleteSubscription and event.outcome:success\n", "references": [ "https://cloud.google.com/pubsub/docs/overview" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_gcp_pub_sub_topic_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_gcp_pub_sub_topic_deletion.json index 7632a73ddfa90..eb6945e88e3fa 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_gcp_pub_sub_topic_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_gcp_pub_sub_topic_deletion.json @@ -14,7 +14,7 @@ "license": "Elastic License v2", "name": "GCP Pub/Sub Topic Deletion", "note": "## Config\n\nThe GCP Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:(googlecloud.audit or gcp.audit) and event.action:google.pubsub.v*.Publisher.DeleteTopic and event.outcome:success", + "query": "event.dataset:(googlecloud.audit or gcp.audit) and event.action:google.pubsub.v*.Publisher.DeleteTopic and event.outcome:success\n", "references": [ "https://cloud.google.com/pubsub/docs/overview" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_gcp_storage_bucket_configuration_modified.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_gcp_storage_bucket_configuration_modified.json index abb6912f0828d..063e844c990be 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_gcp_storage_bucket_configuration_modified.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_gcp_storage_bucket_configuration_modified.json @@ -14,7 +14,7 @@ "license": "Elastic License v2", "name": "GCP Storage Bucket Configuration Modification", "note": "## Config\n\nThe GCP Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:(googlecloud.audit or gcp.audit) and event.action:\"storage.buckets.update\" and event.outcome:success", + "query": "event.dataset:(googlecloud.audit or gcp.audit) and event.action:\"storage.buckets.update\" and event.outcome:success\n", "references": [ "https://cloud.google.com/storage/docs/key-terms#buckets" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_gcp_storage_bucket_permissions_modified.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_gcp_storage_bucket_permissions_modified.json index 8eb238c9dc796..c0a3c90853d79 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_gcp_storage_bucket_permissions_modified.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_gcp_storage_bucket_permissions_modified.json @@ -14,7 +14,7 @@ "license": "Elastic License v2", "name": "GCP Storage Bucket Permissions Modification", "note": "## Config\n\nThe GCP Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:(googlecloud.audit or gcp.audit) and event.action:\"storage.setIamPermissions\" and event.outcome:success", + "query": "event.dataset:(googlecloud.audit or gcp.audit) and event.action:\"storage.setIamPermissions\" and event.outcome:success\n", "references": [ "https://cloud.google.com/storage/docs/access-control/iam-permissions" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_guardduty_detector_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_guardduty_detector_deletion.json index 8ededad39f415..7132fed195ccf 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_guardduty_detector_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_guardduty_detector_deletion.json @@ -16,7 +16,7 @@ "license": "Elastic License v2", "name": "AWS GuardDuty Detector Deletion", "note": "## Config\n\nThe AWS Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:aws.cloudtrail and event.provider:guardduty.amazonaws.com and event.action:DeleteDetector and event.outcome:success", + "query": "event.dataset:aws.cloudtrail and event.provider:guardduty.amazonaws.com and event.action:DeleteDetector and event.outcome:success\n", "references": [ "https://awscli.amazonaws.com/v2/documentation/api/latest/reference/guardduty/delete-detector.html", "https://docs.aws.amazon.com/guardduty/latest/APIReference/API_DeleteDetector.html" diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_hidden_file_dir_tmp.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_hidden_file_dir_tmp.json index a14258e533275..fe8014936316c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_hidden_file_dir_tmp.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_hidden_file_dir_tmp.json @@ -15,7 +15,7 @@ "license": "Elastic License v2", "max_signals": 33, "name": "Creation of Hidden Files and Directories", - "query": "event.category:process AND event.type:(start or process_started) AND process.working_directory:(\"/tmp\" or \"/var/tmp\" or \"/dev/shm\") AND process.args:/\\.[a-zA-Z0-9_\\-][a-zA-Z0-9_\\-\\.]{1,254}/ AND NOT process.name:(ls or find)", + "query": "event.category:process AND event.type:(start or process_started) AND\n process.working_directory:(\"/tmp\" or \"/var/tmp\" or \"/dev/shm\") AND\n process.args:/\\.[a-zA-Z0-9_\\-][a-zA-Z0-9_\\-\\.]{1,254}/ AND\n NOT process.name:(ls or find)\n", "risk_score": 47, "rule_id": "b9666521-4742-49ce-9ddc-b8e84c35acae", "severity": "medium", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_injection_msbuild.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_injection_msbuild.json index 2ddcd0c640415..43d662df271ee 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_injection_msbuild.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_injection_msbuild.json @@ -13,7 +13,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Process Injection by the Microsoft Build Engine", - "query": "process.name:MSBuild.exe and event.action:\"CreateRemoteThread detected (rule: CreateRemoteThread)\"", + "query": "process.name:MSBuild.exe and event.action:\"CreateRemoteThread detected (rule: CreateRemoteThread)\"\n", "risk_score": 21, "rule_id": "9d110cb3-5f4b-4c9a-b9f5-53f0a1707ae9", "severity": "low", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_install_root_certificate.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_install_root_certificate.json index 6585c1b8c8ffb..3397db22f51f3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_install_root_certificate.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_install_root_certificate.json @@ -14,7 +14,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Attempt to Install Root Certificate", - "query": "event.category:process and event.type:(start or process_started) and process.name:security and process.args:\"add-trusted-cert\"", + "query": "event.category:process and event.type:(start or process_started) and\n process.name:security and process.args:\"add-trusted-cert\"\n", "references": [ "https://ss64.com/osx/security-cert.html" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_kernel_module_removal.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_kernel_module_removal.json index 691c30d6f42a5..35c0cb2fb2645 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_kernel_module_removal.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_kernel_module_removal.json @@ -14,7 +14,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Kernel Module Removal", - "query": "event.category:process and event.type:(start or process_started) and process.args:((rmmod and sudo) or (modprobe and sudo and (\"--remove\" or \"-r\")))", + "query": "event.category:process and event.type:(start or process_started) and\n process.args:((rmmod and sudo) or (modprobe and sudo and (\"--remove\" or \"-r\")))\n", "references": [ "http://man7.org/linux/man-pages/man8/modprobe.8.html" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_microsoft_365_exchange_dlp_policy_removed.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_microsoft_365_exchange_dlp_policy_removed.json index b39aee071a874..f4c3e3476c0b6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_microsoft_365_exchange_dlp_policy_removed.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_microsoft_365_exchange_dlp_policy_removed.json @@ -15,7 +15,7 @@ "license": "Elastic License v2", "name": "Microsoft 365 Exchange DLP Policy Removed", "note": "## Config\n\nThe Microsoft 365 Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:o365.audit and event.provider:Exchange and event.category:web and event.action:\"Remove-DlpPolicy\" and event.outcome:success", + "query": "event.dataset:o365.audit and event.provider:Exchange and event.category:web and event.action:\"Remove-DlpPolicy\" and event.outcome:success\n", "references": [ "https://docs.microsoft.com/en-us/powershell/module/exchange/remove-dlppolicy?view=exchange-ps", "https://docs.microsoft.com/en-us/microsoft-365/compliance/data-loss-prevention-policies?view=o365-worldwide" diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_microsoft_365_exchange_malware_filter_policy_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_microsoft_365_exchange_malware_filter_policy_deletion.json index c4de503b66540..ab3399bddbe7a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_microsoft_365_exchange_malware_filter_policy_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_microsoft_365_exchange_malware_filter_policy_deletion.json @@ -15,7 +15,7 @@ "license": "Elastic License v2", "name": "Microsoft 365 Exchange Malware Filter Policy Deletion", "note": "## Config\n\nThe Microsoft 365 Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:o365.audit and event.provider:Exchange and event.category:web and event.action:\"Remove-MalwareFilterPolicy\" and event.outcome:success", + "query": "event.dataset:o365.audit and event.provider:Exchange and event.category:web and event.action:\"Remove-MalwareFilterPolicy\" and event.outcome:success\n", "references": [ "https://docs.microsoft.com/en-us/powershell/module/exchange/remove-malwarefilterpolicy?view=exchange-ps" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_microsoft_365_exchange_malware_filter_rule_mod.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_microsoft_365_exchange_malware_filter_rule_mod.json index c0dcec7e1b0d7..06728dee5b150 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_microsoft_365_exchange_malware_filter_rule_mod.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_microsoft_365_exchange_malware_filter_rule_mod.json @@ -15,7 +15,7 @@ "license": "Elastic License v2", "name": "Microsoft 365 Exchange Malware Filter Rule Modification", "note": "## Config\n\nThe Microsoft 365 Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:o365.audit and event.provider:Exchange and event.category:web and event.action:(\"Remove-MalwareFilterRule\" or \"Disable-MalwareFilterRule\") and event.outcome:success", + "query": "event.dataset:o365.audit and event.provider:Exchange and event.category:web and event.action:(\"Remove-MalwareFilterRule\" or \"Disable-MalwareFilterRule\") and event.outcome:success\n", "references": [ "https://docs.microsoft.com/en-us/powershell/module/exchange/remove-malwarefilterrule?view=exchange-ps", "https://docs.microsoft.com/en-us/powershell/module/exchange/disable-malwarefilterrule?view=exchange-ps" diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_microsoft_365_exchange_safe_attach_rule_disabled.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_microsoft_365_exchange_safe_attach_rule_disabled.json index 5273115ead302..50af384100139 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_microsoft_365_exchange_safe_attach_rule_disabled.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_microsoft_365_exchange_safe_attach_rule_disabled.json @@ -15,7 +15,7 @@ "license": "Elastic License v2", "name": "Microsoft 365 Exchange Safe Attachment Rule Disabled", "note": "## Config\n\nThe Microsoft 365 Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:o365.audit and event.provider:Exchange and event.category:web and event.action:\"Disable-SafeAttachmentRule\" and event.outcome:success", + "query": "event.dataset:o365.audit and event.provider:Exchange and event.category:web and event.action:\"Disable-SafeAttachmentRule\" and event.outcome:success\n", "references": [ "https://docs.microsoft.com/en-us/powershell/module/exchange/disable-safeattachmentrule?view=exchange-ps" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_misc_lolbin_connecting_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_misc_lolbin_connecting_to_the_internet.json index b61a45e1287b7..5f45aa836ddf5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_misc_lolbin_connecting_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_misc_lolbin_connecting_to_the_internet.json @@ -12,7 +12,10 @@ "language": "eql", "license": "Elastic License v2", "name": "Network Connection via Signed Binary", - "query": "sequence by process.entity_id\n [process where (process.name : \"expand.exe\" or process.name : \"extrac.exe\" or\n process.name : \"ieexec.exe\" or process.name : \"makecab.exe\") and\n event.type == \"start\"]\n [network where (process.name : \"expand.exe\" or process.name : \"extrac.exe\" or\n process.name : \"ieexec.exe\" or process.name : \"makecab.exe\") and\n not cidrmatch(destination.ip, \"10.0.0.0/8\", \"172.16.0.0/12\", \"192.168.0.0/16\")]\n", + "query": "sequence by process.entity_id\n [process where (process.name : \"expand.exe\" or process.name : \"extrac.exe\" or\n process.name : \"ieexec.exe\" or process.name : \"makecab.exe\") and\n event.type == \"start\"]\n [network where (process.name : \"expand.exe\" or process.name : \"extrac.exe\" or\n process.name : \"ieexec.exe\" or process.name : \"makecab.exe\") and\n not cidrmatch(destination.ip,\n \"10.0.0.0/8\", \"127.0.0.0/8\", \"169.254.0.0/16\", \"172.16.0.0/12\", \"192.0.0.0/24\", \"192.0.0.0/29\", \"192.0.0.8/32\",\n \"192.0.0.9/32\", \"192.0.0.10/32\", \"192.0.0.170/32\", \"192.0.0.171/32\", \"192.0.2.0/24\", \"192.31.196.0/24\",\n \"192.52.193.0/24\", \"192.168.0.0/16\", \"192.88.99.0/24\", \"224.0.0.0/4\", \"100.64.0.0/10\", \"192.175.48.0/24\",\n \"198.18.0.0/15\", \"198.51.100.0/24\", \"203.0.113.0/24\", \"240.0.0.0/4\", \"::1\", \"FE80::/10\", \"FF00::/8\")]\n", + "references": [ + "https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml" + ], "risk_score": 21, "rule_id": "63e65ec3-43b1-45b0-8f2d-45b34291dc44", "severity": "low", @@ -50,5 +53,5 @@ } ], "type": "eql", - "version": 7 + "version": 8 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_modify_environment_launchctl.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_modify_environment_launchctl.json index d41804247945b..aa0efa290c4f6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_modify_environment_launchctl.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_modify_environment_launchctl.json @@ -11,7 +11,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Modification of Environment Variable via Launchctl", - "query": "event.category:process and event.type:start and process.name:launchctl and process.args:(setenv and not (JAVA*_HOME or RUNTIME_JAVA_HOME or DBUS_LAUNCHD_SESSION_BUS_SOCKET or ANT_HOME or LG_WEBOS_TV_SDK_HOME or WEBOS_CLI_TV or EDEN_ENV) ) and not process.parent.executable:(\"/Applications/NoMachine.app/Contents/Frameworks/bin/nxserver.bin\" or \"/usr/local/bin/kr\" or \"/Applications/NoMachine.app/Contents/Frameworks/bin/nxserver.bin\" or \"/Applications/IntelliJ IDEA CE.app/Contents/jbr/Contents/Home/lib/jspawnhelper\")", + "query": "event.category:process and event.type:start and\n process.name:launchctl and\n process.args:(setenv and not (JAVA*_HOME or\n RUNTIME_JAVA_HOME or\n DBUS_LAUNCHD_SESSION_BUS_SOCKET or\n ANT_HOME or\n LG_WEBOS_TV_SDK_HOME or\n WEBOS_CLI_TV or\n EDEN_ENV)\n ) and\n not process.parent.executable:(\"/Applications/NoMachine.app/Contents/Frameworks/bin/nxserver.bin\" or\n \"/usr/local/bin/kr\" or\n \"/Applications/NoMachine.app/Contents/Frameworks/bin/nxserver.bin\" or\n \"/Applications/IntelliJ IDEA CE.app/Contents/jbr/Contents/Home/lib/jspawnhelper\")\n", "references": [ "https://github.com/rapid7/metasploit-framework/blob/master//modules/post/osx/escalate/tccbypass.rb" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_msxsl_network.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_msxsl_network.json index 6d6541269030d..8f8871b75022e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_msxsl_network.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_msxsl_network.json @@ -12,7 +12,10 @@ "language": "eql", "license": "Elastic License v2", "name": "Network Connection via MsXsl", - "query": "sequence by process.entity_id\n [process where process.name : \"msxsl.exe\" and event.type == \"start\"]\n [network where process.name : \"msxsl.exe\" and\n not cidrmatch(destination.ip, \"10.0.0.0/8\", \"172.16.0.0/12\", \"192.168.0.0/16\")]\n", + "query": "sequence by process.entity_id\n [process where process.name : \"msxsl.exe\" and event.type == \"start\"]\n [network where process.name : \"msxsl.exe\" and\n not cidrmatch(destination.ip, \"10.0.0.0/8\", \"127.0.0.0/8\", \"169.254.0.0/16\", \"172.16.0.0/12\", \"192.0.0.0/24\",\n \"192.0.0.0/29\", \"192.0.0.8/32\", \"192.0.0.9/32\", \"192.0.0.10/32\", \"192.0.0.170/32\", \"192.0.0.171/32\",\n \"192.0.2.0/24\", \"192.31.196.0/24\", \"192.52.193.0/24\", \"192.168.0.0/16\", \"192.88.99.0/24\", \"224.0.0.0/4\",\n \"100.64.0.0/10\", \"192.175.48.0/24\",\"198.18.0.0/15\", \"198.51.100.0/24\", \"203.0.113.0/24\", \"240.0.0.0/4\", \"::1\",\n \"FE80::/10\", \"FF00::/8\")]\n", + "references": [ + "https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml" + ], "risk_score": 21, "rule_id": "b86afe07-0d98-4738-b15d-8d7465f95ff5", "severity": "low", @@ -41,5 +44,5 @@ } ], "type": "eql", - "version": 6 + "version": 7 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_network_watcher_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_network_watcher_deletion.json index 27a590f49cb31..030e72dd4a411 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_network_watcher_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_network_watcher_deletion.json @@ -15,7 +15,7 @@ "license": "Elastic License v2", "name": "Azure Network Watcher Deletion", "note": "## Config\n\nThe Azure Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:azure.activitylogs and azure.activitylogs.operation_name:\"MICROSOFT.NETWORK/NETWORKWATCHERS/DELETE\" and event.outcome:(Success or success)", + "query": "event.dataset:azure.activitylogs and azure.activitylogs.operation_name:\"MICROSOFT.NETWORK/NETWORKWATCHERS/DELETE\" and event.outcome:(Success or success)\n", "references": [ "https://docs.microsoft.com/en-us/azure/network-watcher/network-watcher-monitoring-overview" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_s3_bucket_configuration_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_s3_bucket_configuration_deletion.json index 4eb3bb47beff5..d673b7ef324f6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_s3_bucket_configuration_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_s3_bucket_configuration_deletion.json @@ -16,7 +16,7 @@ "license": "Elastic License v2", "name": "AWS S3 Bucket Configuration Deletion", "note": "## Config\n\nThe AWS Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:aws.cloudtrail and event.provider:s3.amazonaws.com and event.action:(DeleteBucketPolicy or DeleteBucketReplication or DeleteBucketCors or DeleteBucketEncryption or DeleteBucketLifecycle) and event.outcome:success", + "query": "event.dataset:aws.cloudtrail and event.provider:s3.amazonaws.com and\n event.action:(DeleteBucketPolicy or DeleteBucketReplication or DeleteBucketCors or\n DeleteBucketEncryption or DeleteBucketLifecycle)\n and event.outcome:success\n", "references": [ "https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteBucketPolicy.html", "https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteBucketReplication.html", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_safari_config_change.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_safari_config_change.json index 8021a21c8010d..a56b2cf6e6ded 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_safari_config_change.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_safari_config_change.json @@ -11,7 +11,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Modification of Safari Settings via Defaults Command", - "query": "event.category:process and event.type:start and process.name:defaults and process.args: (com.apple.Safari and write and not ( UniversalSearchEnabled or SuppressSearchSuggestions or WebKitTabToLinksPreferenceKey or ShowFullURLInSmartSearchField or com.apple.Safari.ContentPageGroupIdentifier.WebKit2TabsToLinks ) )", + "query": "event.category:process and event.type:start and\n process.name:defaults and process.args:\n (com.apple.Safari and write and not\n (\n UniversalSearchEnabled or\n SuppressSearchSuggestions or\n WebKitTabToLinksPreferenceKey or\n ShowFullURLInSmartSearchField or\n com.apple.Safari.ContentPageGroupIdentifier.WebKit2TabsToLinks\n )\n )\n", "references": [ "https://objectivebythesea.com/v2/talks/OBTS_v2_Zohar.pdf" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_sandboxed_office_app_suspicious_zip_file.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_sandboxed_office_app_suspicious_zip_file.json index 338010e1fe0e3..385f89910ab7d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_sandboxed_office_app_suspicious_zip_file.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_sandboxed_office_app_suspicious_zip_file.json @@ -11,7 +11,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Potential Microsoft Office Sandbox Evasion", - "query": "event.category:file and not event.type:deletion and file.name:~$*.zip", + "query": "event.category:file and not event.type:deletion and file.name:~$*.zip\n", "references": [ "https://i.blackhat.com/USA-20/Wednesday/us-20-Wardle-Office-Drama-On-macOS.pdf", "https://www.mdsec.co.uk/2018/08/escaping-the-sandbox-microsoft-office-on-macos/", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_stop_process_service_threshold.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_stop_process_service_threshold.json index 8339c75c59c52..86903058b62fe 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_stop_process_service_threshold.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_stop_process_service_threshold.json @@ -12,7 +12,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "High Number of Process and/or Service Terminations", - "query": "event.category:process and event.type:start and process.name:(net.exe or sc.exe or taskkill.exe) and process.args:(stop or pause or delete or \"/PID\" or \"/IM\" or \"/T\" or \"/F\" or \"/t\" or \"/f\" or \"/im\" or \"/pid\")", + "query": "event.category:process and event.type:start and process.name:(net.exe or sc.exe or taskkill.exe) and\n process.args:(stop or pause or delete or \"/PID\" or \"/IM\" or \"/T\" or \"/F\" or \"/t\" or \"/f\" or \"/im\" or \"/pid\")\n", "risk_score": 47, "rule_id": "035889c4-2686-4583-a7df-67f89c292f2c", "severity": "medium", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_suspicious_execution_from_mounted_device.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_suspicious_execution_from_mounted_device.json new file mode 100644 index 0000000000000..b05402f419f5a --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_suspicious_execution_from_mounted_device.json @@ -0,0 +1,89 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies when a script interpreter or signed binary is launched via a non-standard working directory. An attacker may use this technique to evade defenses.", + "from": "now-9m", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*", + "logs-windows.*" + ], + "language": "eql", + "license": "Elastic License v2", + "name": "Suspicious Execution from a Mounted Device", + "query": "process where event.type == \"start\" and process.executable : \"C:\\\\*\" and\n (process.working_directory : \"?:\\\\\" and not process.working_directory: \"C:\\\\\") and\n process.parent.name : \"explorer.exe\" and\n process.name : (\"rundll32.exe\", \"mshta.exe\", \"powershell.exe\", \"pwsh.exe\", \"cmd.exe\", \"regsvr32.exe\",\n \"cscript.exe\", \"wscript.exe\")\n", + "references": [ + "https://www.microsoft.com/security/blog/2021/05/27/new-sophisticated-email-based-attack-from-nobelium/", + "https://www.volexity.com/blog/2021/05/27/suspected-apt29-operation-launches-election-fraud-themed-phishing-campaigns/" + ], + "risk_score": 47, + "rule_id": "8a1d4831-3ce6-4859-9891-28931fa6101d", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Defense Evasion" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0005", + "name": "Defense Evasion", + "reference": "https://attack.mitre.org/tactics/TA0005/" + }, + "technique": [ + { + "id": "T1218", + "name": "Signed Binary Proxy Execution", + "reference": "https://attack.mitre.org/techniques/T1218/", + "subtechnique": [ + { + "id": "T1218.011", + "name": "Rundll32", + "reference": "https://attack.mitre.org/techniques/T1218/011/" + }, + { + "id": "T1218.005", + "name": "Mshta", + "reference": "https://attack.mitre.org/techniques/T1218/005/" + }, + { + "id": "T1218.010", + "name": "Regsvr32", + "reference": "https://attack.mitre.org/techniques/T1218/010/" + } + ] + } + ] + }, + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0002", + "name": "Execution", + "reference": "https://attack.mitre.org/tactics/TA0002/" + }, + "technique": [ + { + "id": "T1059", + "name": "Command and Scripting Interpreter", + "reference": "https://attack.mitre.org/techniques/T1059/", + "subtechnique": [ + { + "id": "T1059.001", + "name": "PowerShell", + "reference": "https://attack.mitre.org/techniques/T1059/001/" + } + ] + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_suspicious_okta_user_password_reset_or_unlock_attempts.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_suspicious_okta_user_password_reset_or_unlock_attempts.json index b1ab19a25d840..d33620cbbf63b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_suspicious_okta_user_password_reset_or_unlock_attempts.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_suspicious_okta_user_password_reset_or_unlock_attempts.json @@ -1,8 +1,10 @@ { "author": [ - "Elastic" + "Elastic", + "@BenB196", + "Austin Songer" ], - "description": "Identifies a high number of Okta user password reset or account unlock attempts. An adversary may attempt to obtain unauthorized access to an Okta user account using these methods and attempt to blend in with normal activity in their target's environment and evade detection.", + "description": "Identifies a high number of Okta user password reset or account unlock attempts. An adversary may attempt to obtain unauthorized access to Okta user accounts using these methods and attempt to blend in with normal activity in their target's environment and evade detection.", "false_positives": [ "The number of Okta user password reset or account unlock attempts will likely vary between organizations. To fit this rule to their organization, users can duplicate this rule and edit the schedule and threshold values in the new rule." ], @@ -15,7 +17,7 @@ "license": "Elastic License v2", "name": "High Number of Okta User Password Reset or Unlock Attempts", "note": "## Config\n\nThe Okta Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:okta.system and event.action:(system.email.account_unlock.sent_message or system.email.password_reset.sent_message or system.sms.send_account_unlock_message or system.sms.send_password_reset_message or system.voice.send_account_unlock_call or system.voice.send_password_reset_call or user.account.unlock_token)", + "query": "event.dataset:okta.system and\n event.action:(system.email.account_unlock.sent_message or system.email.password_reset.sent_message or\n system.sms.send_account_unlock_message or system.sms.send_password_reset_message or\n system.voice.send_account_unlock_call or system.voice.send_password_reset_call or\n user.account.unlock_token)\n", "references": [ "https://developer.okta.com/docs/reference/api/system-log/", "https://developer.okta.com/docs/reference/api/event-types/" @@ -80,10 +82,10 @@ ], "threshold": { "field": [ - "okta.actor.id" + "okta.actor.alternate_id" ], "value": 5 }, "type": "threshold", - "version": 4 + "version": 5 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_tcc_bypass_mounted_apfs_access.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_tcc_bypass_mounted_apfs_access.json index 963eb444305c7..4b1f13a10414d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_tcc_bypass_mounted_apfs_access.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_tcc_bypass_mounted_apfs_access.json @@ -11,7 +11,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "TCC Bypass via Mounted APFS Snapshot Access", - "query": "event.category : process and event.type : (start or process_started) and process.name : mount_apfs and process.args : (/System/Volumes/Data and noowners)", + "query": "event.category : process and event.type : (start or process_started) and process.name : mount_apfs and\n process.args : (/System/Volumes/Data and noowners)\n", "references": [ "https://theevilbit.github.io/posts/cve_2020_9771/" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_unload_endpointsecurity_kext.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_unload_endpointsecurity_kext.json index dc854dee776c4..aadcaadaba980 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_unload_endpointsecurity_kext.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_unload_endpointsecurity_kext.json @@ -11,7 +11,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Attempt to Unload Elastic Endpoint Security Kernel Extension", - "query": "event.category:process and event.type:(start or process_started) and process.name:kextunload and process.args:(\"/System/Library/Extensions/EndpointSecurity.kext\" or \"EndpointSecurity.kext\")", + "query": "event.category:process and event.type:(start or process_started) and\n process.name:kextunload and process.args:(\"/System/Library/Extensions/EndpointSecurity.kext\" or \"EndpointSecurity.kext\")\n", "risk_score": 73, "rule_id": "70fa1af4-27fd-4f26-bd03-50b6af6b9e24", "severity": "high", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_unusual_network_connection_via_dllhost.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_unusual_network_connection_via_dllhost.json new file mode 100644 index 0000000000000..a030c88941a41 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_unusual_network_connection_via_dllhost.json @@ -0,0 +1,51 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies unusual instances of dllhost.exe making outbound network connections. This may indicate adversarial Command and Control activity.", + "from": "now-9m", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*", + "logs-windows.*" + ], + "language": "eql", + "license": "Elastic License v2", + "name": "Unusual Network Connection via DllHost", + "query": "sequence by host.id, process.entity_id with maxspan=1m\n [process where event.type in (\"start\", \"process_started\") and process.name : \"dllhost.exe\" and process.args_count == 1]\n [network where process.name : \"dllhost.exe\" and\n not cidrmatch(destination.ip, \"10.0.0.0/8\", \"127.0.0.0/8\", \"169.254.0.0/16\", \"172.16.0.0/12\", \"192.0.0.0/24\",\n \"192.0.0.0/29\", \"192.0.0.8/32\", \"192.0.0.9/32\", \"192.0.0.10/32\", \"192.0.0.170/32\", \"192.0.0.171/32\", \"192.0.2.0/24\",\n \"192.31.196.0/24\", \"192.52.193.0/24\", \"192.168.0.0/16\", \"192.88.99.0/24\", \"224.0.0.0/4\", \"100.64.0.0/10\",\n \"192.175.48.0/24\", \"198.18.0.0/15\", \"198.51.100.0/24\", \"203.0.113.0/24\", \"240.0.0.0/4\", \"::1\", \"FE80::/10\",\n \"FF00::/8\")]\n", + "references": [ + "https://www.microsoft.com/security/blog/2021/05/27/new-sophisticated-email-based-attack-from-nobelium/", + "https://www.volexity.com/blog/2021/05/27/suspected-apt29-operation-launches-election-fraud-themed-phishing-campaigns/", + "https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml" + ], + "risk_score": 47, + "rule_id": "c7894234-7814-44c2-92a9-f7d851ea246a", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Defense Evasion" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0005", + "name": "Defense Evasion", + "reference": "https://attack.mitre.org/tactics/TA0005/" + }, + "technique": [ + { + "id": "T1218", + "name": "Signed Binary Proxy Execution", + "reference": "https://attack.mitre.org/techniques/T1218/" + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_unusual_network_connection_via_rundll32.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_unusual_network_connection_via_rundll32.json index d920c4f853dfd..f37b6a21c7e58 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_unusual_network_connection_via_rundll32.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_unusual_network_connection_via_rundll32.json @@ -12,7 +12,10 @@ "language": "eql", "license": "Elastic License v2", "name": "Unusual Network Connection via RunDLL32", - "query": "sequence by host.id, process.entity_id with maxspan=1m\n [process where event.type in (\"start\", \"process_started\") and process.name : \"rundll32.exe\" and process.args_count == 1]\n [network where process.name : \"rundll32.exe\" and\n not cidrmatch(destination.ip, \"10.0.0.0/8\", \"172.16.0.0/12\", \"192.168.0.0/16\", \"127.0.0.0/8\", \"FE80::/10\", \"::1/128\")]\n", + "query": "sequence by host.id, process.entity_id with maxspan=1m\n [process where event.type in (\"start\", \"process_started\") and process.name : \"rundll32.exe\" and process.args_count == 1]\n [network where process.name : \"rundll32.exe\" and\n not cidrmatch(destination.ip, \"10.0.0.0/8\", \"127.0.0.0/8\", \"169.254.0.0/16\", \"172.16.0.0/12\", \"192.0.0.0/24\",\n \"192.0.0.0/29\", \"192.0.0.8/32\", \"192.0.0.9/32\", \"192.0.0.10/32\", \"192.0.0.170/32\", \"192.0.0.171/32\",\n \"192.0.2.0/24\", \"192.31.196.0/24\", \"192.52.193.0/24\", \"192.168.0.0/16\", \"192.88.99.0/24\", \"224.0.0.0/4\",\n \"100.64.0.0/10\", \"192.175.48.0/24\",\"198.18.0.0/15\", \"198.51.100.0/24\", \"203.0.113.0/24\", \"240.0.0.0/4\", \"::1\",\n \"FE80::/10\", \"FF00::/8\")]\n", + "references": [ + "https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml" + ], "risk_score": 47, "rule_id": "52aaab7b-b51c-441a-89ce-4387b3aea886", "severity": "medium", @@ -48,5 +51,5 @@ } ], "type": "eql", - "version": 9 + "version": 10 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_waf_acl_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_waf_acl_deletion.json index cc2d47812b48c..9a7c7c9f668cc 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_waf_acl_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_waf_acl_deletion.json @@ -16,7 +16,7 @@ "license": "Elastic License v2", "name": "AWS WAF Access Control List Deletion", "note": "## Config\n\nThe AWS Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:aws.cloudtrail and event.action:DeleteWebACL and event.outcome:success", + "query": "event.dataset:aws.cloudtrail and event.action:DeleteWebACL and event.outcome:success\n", "references": [ "https://awscli.amazonaws.com/v2/documentation/api/latest/reference/waf-regional/delete-web-acl.html", "https://docs.aws.amazon.com/waf/latest/APIReference/API_wafRegional_DeleteWebACL.html" diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_waf_rule_or_rule_group_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_waf_rule_or_rule_group_deletion.json index ee21d03f8464b..6f42caa63cf49 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_waf_rule_or_rule_group_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_waf_rule_or_rule_group_deletion.json @@ -16,7 +16,7 @@ "license": "Elastic License v2", "name": "AWS WAF Rule or Rule Group Deletion", "note": "## Config\n\nThe AWS Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:aws.cloudtrail and event.action:(DeleteRule or DeleteRuleGroup) and event.outcome:success", + "query": "event.dataset:aws.cloudtrail and event.action:(DeleteRule or DeleteRuleGroup) and event.outcome:success\n", "references": [ "https://awscli.amazonaws.com/v2/documentation/api/latest/reference/waf/delete-rule-group.html", "https://docs.aws.amazon.com/waf/latest/APIReference/API_waf_DeleteRuleGroup.html" diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_blob_container_access_mod.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_blob_container_access_mod.json index 16eabb82de0f7..ed50b58a91e13 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_blob_container_access_mod.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_blob_container_access_mod.json @@ -15,7 +15,7 @@ "license": "Elastic License v2", "name": "Azure Blob Container Access Level Modification", "note": "## Config\n\nThe Azure Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:azure.activitylogs and azure.activitylogs.operation_name:\"MICROSOFT.STORAGE/STORAGEACCOUNTS/BLOBSERVICES/CONTAINERS/WRITE\" and event.outcome:(Success or success)", + "query": "event.dataset:azure.activitylogs and azure.activitylogs.operation_name:\"MICROSOFT.STORAGE/STORAGEACCOUNTS/BLOBSERVICES/CONTAINERS/WRITE\" and event.outcome:(Success or success)\n", "references": [ "https://docs.microsoft.com/en-us/azure/storage/blobs/anonymous-read-access-prevent" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_kernel_module_enumeration.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_kernel_module_enumeration.json index 01d07b03c079a..96860eb7e6f79 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_kernel_module_enumeration.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_kernel_module_enumeration.json @@ -14,7 +14,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Enumeration of Kernel Modules", - "query": "event.category:process and event.type:(start or process_started) and process.args:(kmod and list and sudo or sudo and (depmod or lsmod or modinfo))", + "query": "event.category:process and event.type:(start or process_started) and\n process.args:(kmod and list and sudo or sudo and (depmod or lsmod or modinfo))\n", "risk_score": 47, "rule_id": "2d8043ed-5bda-4caf-801c-c1feb7410504", "severity": "medium", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_virtual_machine_fingerprinting.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_virtual_machine_fingerprinting.json index 7defca7b649de..09adcdc65b02a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_virtual_machine_fingerprinting.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_virtual_machine_fingerprinting.json @@ -14,7 +14,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Virtual Machine Fingerprinting", - "query": "event.category:process and event.type:(start or process_started) and process.args:(\"/sys/class/dmi/id/bios_version\" or \"/sys/class/dmi/id/product_name\" or \"/sys/class/dmi/id/chassis_vendor\" or \"/proc/scsi/scsi\" or \"/proc/ide/hd0/model\") and not user.name:root", + "query": "event.category:process and event.type:(start or process_started) and\n process.args:(\"/sys/class/dmi/id/bios_version\" or\n \"/sys/class/dmi/id/product_name\" or\n \"/sys/class/dmi/id/chassis_vendor\" or\n \"/proc/scsi/scsi\" or\n \"/proc/ide/hd0/model\") and\n not user.name:root\n", "risk_score": 73, "rule_id": "5b03c9fb-9945-4d2f-9568-fd690fee3fba", "severity": "high", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/domain_added_to_google_workspace_trusted_domains.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/domain_added_to_google_workspace_trusted_domains.json index 96f246f9403bf..300840771081d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/domain_added_to_google_workspace_trusted_domains.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/domain_added_to_google_workspace_trusted_domains.json @@ -16,7 +16,7 @@ "license": "Elastic License v2", "name": "Domain Added to Google Workspace Trusted Domains", "note": "## Config\n\nThe Google Workspace Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.\n\n### Important Information Regarding Google Workspace Event Lag Times\n- As per Google's documentation, Google Workspace administrators may observe lag times ranging from minutes up to 3 days between the time of an event's occurrence and the event being visible in the Google Workspace admin/audit logs.\n- This rule is configured to run every 10 minutes with a lookback time of 130 minutes.\n- To reduce the risk of false negatives, consider reducing the interval that the Google Workspace (formerly G Suite) Filebeat module polls Google's reporting API for new events.\n- By default, `var.interval` is set to 2 hours (2h). Consider changing this interval to a lower value, such as 10 minutes (10m).\n- See the following references for further information.\n - https://support.google.com/a/answer/7061566\n - https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-module-gsuite.html", - "query": "event.dataset:(gsuite.admin or google_workspace.admin) and event.provider:admin and event.category:iam and event.action:ADD_TRUSTED_DOMAINS", + "query": "event.dataset:(gsuite.admin or google_workspace.admin) and event.provider:admin and event.category:iam and event.action:ADD_TRUSTED_DOMAINS\n", "references": [ "https://support.google.com/a/answer/6160020?hl=en" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security.json index c3fb6d83f194f..63bf6fea698ae 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security.json @@ -20,7 +20,7 @@ "license": "Elastic License v2", "max_signals": 10000, "name": "Endpoint Security", - "query": "event.kind:alert and event.module:(endpoint and not endgame)", + "query": "event.kind:alert and event.module:(endpoint and not endgame)\n", "risk_score": 47, "risk_score_mapping": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_adversary_behavior_detected.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_adversary_behavior_detected.json index bf53625cef750..ff690710b5ba3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_adversary_behavior_detected.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_adversary_behavior_detected.json @@ -11,7 +11,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Adversary Behavior - Detected - Elastic Endgame", - "query": "event.kind:alert and event.module:endgame and (event.action:rules_engine_event or endgame.event_subtype_full:rules_engine_event)", + "query": "event.kind:alert and event.module:endgame and (event.action:rules_engine_event or endgame.event_subtype_full:rules_engine_event)\n", "risk_score": 47, "rule_id": "77a3c3df-8ec4-4da4-b758-878f551dee69", "severity": "medium", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_cred_dumping_detected.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_cred_dumping_detected.json index 43cb19f50d675..4aaf9938c29da 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_cred_dumping_detected.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_cred_dumping_detected.json @@ -11,7 +11,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Credential Dumping - Detected - Elastic Endgame", - "query": "event.kind:alert and event.module:endgame and endgame.metadata.type:detection and (event.action:cred_theft_event or endgame.event_subtype_full:cred_theft_event)", + "query": "event.kind:alert and event.module:endgame and endgame.metadata.type:detection and (event.action:cred_theft_event or endgame.event_subtype_full:cred_theft_event)\n", "risk_score": 73, "rule_id": "571afc56-5ed9-465d-a2a9-045f099f6e7e", "severity": "high", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_cred_dumping_prevented.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_cred_dumping_prevented.json index 29b5bc3f39cf1..11e8dece47fb5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_cred_dumping_prevented.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_cred_dumping_prevented.json @@ -11,7 +11,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Credential Dumping - Prevented - Elastic Endgame", - "query": "event.kind:alert and event.module:endgame and endgame.metadata.type:prevention and (event.action:cred_theft_event or endgame.event_subtype_full:cred_theft_event)", + "query": "event.kind:alert and event.module:endgame and endgame.metadata.type:prevention and (event.action:cred_theft_event or endgame.event_subtype_full:cred_theft_event)\n", "risk_score": 47, "rule_id": "db8c33a8-03cd-4988-9e2c-d0a4863adb13", "severity": "medium", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_cred_manipulation_detected.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_cred_manipulation_detected.json index 393591a241114..b6753cbbf784e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_cred_manipulation_detected.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_cred_manipulation_detected.json @@ -11,7 +11,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Credential Manipulation - Detected - Elastic Endgame", - "query": "event.kind:alert and event.module:endgame and endgame.metadata.type:detection and (event.action:token_manipulation_event or endgame.event_subtype_full:token_manipulation_event)", + "query": "event.kind:alert and event.module:endgame and endgame.metadata.type:detection and (event.action:token_manipulation_event or endgame.event_subtype_full:token_manipulation_event)\n", "risk_score": 73, "rule_id": "c0be5f31-e180-48ed-aa08-96b36899d48f", "severity": "high", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_cred_manipulation_prevented.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_cred_manipulation_prevented.json index e9ca199c4a791..9f409b00d6421 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_cred_manipulation_prevented.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_cred_manipulation_prevented.json @@ -11,7 +11,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Credential Manipulation - Prevented - Elastic Endgame", - "query": "event.kind:alert and event.module:endgame and endgame.metadata.type:prevention and (event.action:token_manipulation_event or endgame.event_subtype_full:token_manipulation_event)", + "query": "event.kind:alert and event.module:endgame and endgame.metadata.type:prevention and (event.action:token_manipulation_event or endgame.event_subtype_full:token_manipulation_event)\n", "risk_score": 47, "rule_id": "c9e38e64-3f4c-4bf3-ad48-0e61a60ea1fa", "severity": "medium", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_exploit_detected.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_exploit_detected.json index a169582c2da92..0cc778cc12714 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_exploit_detected.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_exploit_detected.json @@ -11,7 +11,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Exploit - Detected - Elastic Endgame", - "query": "event.kind:alert and event.module:endgame and endgame.metadata.type:detection and (event.action:exploit_event or endgame.event_subtype_full:exploit_event)", + "query": "event.kind:alert and event.module:endgame and endgame.metadata.type:detection and (event.action:exploit_event or endgame.event_subtype_full:exploit_event)\n", "risk_score": 73, "rule_id": "2003cdc8-8d83-4aa5-b132-1f9a8eb48514", "severity": "high", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_exploit_prevented.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_exploit_prevented.json index b781a1fae1847..fe7218fe878ee 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_exploit_prevented.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_exploit_prevented.json @@ -11,7 +11,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Exploit - Prevented - Elastic Endgame", - "query": "event.kind:alert and event.module:endgame and endgame.metadata.type:prevention and (event.action:exploit_event or endgame.event_subtype_full:exploit_event)", + "query": "event.kind:alert and event.module:endgame and endgame.metadata.type:prevention and (event.action:exploit_event or endgame.event_subtype_full:exploit_event)\n", "risk_score": 47, "rule_id": "2863ffeb-bf77-44dd-b7a5-93ef94b72036", "severity": "medium", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_malware_detected.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_malware_detected.json index f7a064961f039..92795c3811345 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_malware_detected.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_malware_detected.json @@ -11,7 +11,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Malware - Detected - Elastic Endgame", - "query": "event.kind:alert and event.module:endgame and endgame.metadata.type:detection and (event.action:file_classification_event or endgame.event_subtype_full:file_classification_event)", + "query": "event.kind:alert and event.module:endgame and endgame.metadata.type:detection and (event.action:file_classification_event or endgame.event_subtype_full:file_classification_event)\n", "risk_score": 99, "rule_id": "0a97b20f-4144-49ea-be32-b540ecc445de", "severity": "critical", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_malware_prevented.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_malware_prevented.json index 59cbd98e2d42b..f712f3f1b221d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_malware_prevented.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_malware_prevented.json @@ -11,7 +11,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Malware - Prevented - Elastic Endgame", - "query": "event.kind:alert and event.module:endgame and endgame.metadata.type:prevention and (event.action:file_classification_event or endgame.event_subtype_full:file_classification_event)", + "query": "event.kind:alert and event.module:endgame and endgame.metadata.type:prevention and (event.action:file_classification_event or endgame.event_subtype_full:file_classification_event)\n", "risk_score": 73, "rule_id": "3b382770-efbb-44f4-beed-f5e0a051b895", "severity": "high", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_permission_theft_detected.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_permission_theft_detected.json index b3db96d6d121b..96b64c026ad0d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_permission_theft_detected.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_permission_theft_detected.json @@ -11,7 +11,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Permission Theft - Detected - Elastic Endgame", - "query": "event.kind:alert and event.module:endgame and endgame.metadata.type:detection and (event.action:token_protection_event or endgame.event_subtype_full:token_protection_event)", + "query": "event.kind:alert and event.module:endgame and endgame.metadata.type:detection and (event.action:token_protection_event or endgame.event_subtype_full:token_protection_event)\n", "risk_score": 73, "rule_id": "c3167e1b-f73c-41be-b60b-87f4df707fe3", "severity": "high", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_permission_theft_prevented.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_permission_theft_prevented.json index 18b316a293da8..fcb453d16100f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_permission_theft_prevented.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_permission_theft_prevented.json @@ -11,7 +11,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Permission Theft - Prevented - Elastic Endgame", - "query": "event.kind:alert and event.module:endgame and endgame.metadata.type:prevention and (event.action:token_protection_event or endgame.event_subtype_full:token_protection_event)", + "query": "event.kind:alert and event.module:endgame and endgame.metadata.type:prevention and (event.action:token_protection_event or endgame.event_subtype_full:token_protection_event)\n", "risk_score": 47, "rule_id": "453f659e-0429-40b1-bfdb-b6957286e04b", "severity": "medium", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_process_injection_detected.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_process_injection_detected.json index 861daa2d004c7..83a8ff2f83c8d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_process_injection_detected.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_process_injection_detected.json @@ -11,7 +11,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Process Injection - Detected - Elastic Endgame", - "query": "event.kind:alert and event.module:endgame and endgame.metadata.type:detection and (event.action:kernel_shellcode_event or endgame.event_subtype_full:kernel_shellcode_event)", + "query": "event.kind:alert and event.module:endgame and endgame.metadata.type:detection and (event.action:kernel_shellcode_event or endgame.event_subtype_full:kernel_shellcode_event)\n", "risk_score": 73, "rule_id": "80c52164-c82a-402c-9964-852533d58be1", "severity": "high", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_process_injection_prevented.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_process_injection_prevented.json index 5f78a3517e931..e171fbcf6bb83 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_process_injection_prevented.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_process_injection_prevented.json @@ -11,7 +11,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Process Injection - Prevented - Elastic Endgame", - "query": "event.kind:alert and event.module:endgame and endgame.metadata.type:prevention and (event.action:kernel_shellcode_event or endgame.event_subtype_full:kernel_shellcode_event)", + "query": "event.kind:alert and event.module:endgame and endgame.metadata.type:prevention and (event.action:kernel_shellcode_event or endgame.event_subtype_full:kernel_shellcode_event)\n", "risk_score": 47, "rule_id": "990838aa-a953-4f3e-b3cb-6ddf7584de9e", "severity": "medium", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_ransomware_detected.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_ransomware_detected.json index 4c060bb52f32f..b12352ee144d9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_ransomware_detected.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_ransomware_detected.json @@ -11,7 +11,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Ransomware - Detected - Elastic Endgame", - "query": "event.kind:alert and event.module:endgame and endgame.metadata.type:detection and (event.action:ransomware_event or endgame.event_subtype_full:ransomware_event)", + "query": "event.kind:alert and event.module:endgame and endgame.metadata.type:detection and (event.action:ransomware_event or endgame.event_subtype_full:ransomware_event)\n", "risk_score": 99, "rule_id": "8cb4f625-7743-4dfb-ae1b-ad92be9df7bd", "severity": "critical", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_ransomware_prevented.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_ransomware_prevented.json index 78845ffc4c845..22150cd72a4c9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_ransomware_prevented.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_ransomware_prevented.json @@ -11,7 +11,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Ransomware - Prevented - Elastic Endgame", - "query": "event.kind:alert and event.module:endgame and endgame.metadata.type:prevention and (event.action:ransomware_event or endgame.event_subtype_full:ransomware_event)", + "query": "event.kind:alert and event.module:endgame and endgame.metadata.type:prevention and (event.action:ransomware_event or endgame.event_subtype_full:ransomware_event)\n", "risk_score": 73, "rule_id": "e3c5d5cb-41d5-4206-805c-f30561eae3ac", "severity": "high", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_prompt_connecting_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_prompt_connecting_to_the_internet.json index 32673c7800f0b..e5bef64a4c747 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_prompt_connecting_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_prompt_connecting_to_the_internet.json @@ -15,7 +15,10 @@ "language": "eql", "license": "Elastic License v2", "name": "Command Prompt Network Connection", - "query": "sequence by process.entity_id\n [process where process.name : \"cmd.exe\" and event.type == \"start\"]\n [network where process.name : \"cmd.exe\" and\n not cidrmatch(destination.ip, \"10.0.0.0/8\", \"172.16.0.0/12\", \"192.168.0.0/16\")]\n", + "query": "sequence by process.entity_id\n [process where process.name : \"cmd.exe\" and event.type == \"start\"]\n [network where process.name : \"cmd.exe\" and\n not cidrmatch(destination.ip, \"10.0.0.0/8\", \"127.0.0.0/8\", \"169.254.0.0/16\", \"172.16.0.0/12\", \"192.0.0.0/24\",\n \"192.0.0.0/29\", \"192.0.0.8/32\", \"192.0.0.9/32\", \"192.0.0.10/32\", \"192.0.0.170/32\",\n \"192.0.0.171/32\", \"192.0.2.0/24\", \"192.31.196.0/24\", \"192.52.193.0/24\",\n \"192.168.0.0/16\", \"192.88.99.0/24\", \"224.0.0.0/4\", \"100.64.0.0/10\", \"192.175.48.0/24\",\n \"198.18.0.0/15\", \"198.51.100.0/24\", \"203.0.113.0/24\", \"240.0.0.0/4\", \"::1\",\n \"FE80::/10\", \"FF00::/8\")]\n", + "references": [ + "https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml" + ], "risk_score": 21, "rule_id": "89f9a4b0-9f8f-4ee0-8823-c4751a6d6696", "severity": "low", @@ -59,5 +62,5 @@ } ], "type": "eql", - "version": 6 + "version": 7 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_virtual_machine.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_virtual_machine.json index 8254e9e1c75e0..1292596404eff 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_virtual_machine.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_virtual_machine.json @@ -15,7 +15,7 @@ "license": "Elastic License v2", "name": "Azure Command Execution on Virtual Machine", "note": "## Config\n\nThe Azure Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:azure.activitylogs and azure.activitylogs.operation_name:\"MICROSOFT.COMPUTE/VIRTUALMACHINES/RUNCOMMAND/ACTION\" and event.outcome:(Success or success)", + "query": "event.dataset:azure.activitylogs and azure.activitylogs.operation_name:\"MICROSOFT.COMPUTE/VIRTUALMACHINES/RUNCOMMAND/ACTION\" and event.outcome:(Success or success)\n", "references": [ "https://adsecurity.org/?p=4277", "https://posts.specterops.io/attacking-azure-azure-ad-and-introducing-powerzure-ca70b330511a", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_defense_evasion_electron_app_childproc_node_js.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_defense_evasion_electron_app_childproc_node_js.json index cb3c3b3d040f3..4bf6c272e59ae 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_defense_evasion_electron_app_childproc_node_js.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_defense_evasion_electron_app_childproc_node_js.json @@ -11,7 +11,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Execution via Electron Child Process Node.js Module", - "query": "event.category:process and event.type:(start or process_started) and process.args:(\"-e\" and const*require*child_process*)", + "query": "event.category:process and event.type:(start or process_started) and process.args:(\"-e\" and const*require*child_process*)\n", "references": [ "https://www.matthewslipper.com/2019/09/22/everything-you-wanted-electron-child-process.html", "https://www.trustedsec.com/blog/macos-injection-via-third-party-frameworks/", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_html_help_executable_program_connecting_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_html_help_executable_program_connecting_to_the_internet.json index 31a81001d9e04..abc41d9f6d5c3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_html_help_executable_program_connecting_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_html_help_executable_program_connecting_to_the_internet.json @@ -12,7 +12,10 @@ "language": "eql", "license": "Elastic License v2", "name": "Network Connection via Compiled HTML File", - "query": "sequence by process.entity_id\n [process where process.name : \"hh.exe\" and event.type == \"start\"]\n [network where process.name : \"hh.exe\" and\n not cidrmatch(destination.ip, \"10.0.0.0/8\", \"172.16.0.0/12\", \"192.168.0.0/16\")]\n", + "query": "sequence by process.entity_id\n [process where process.name : \"hh.exe\" and event.type == \"start\"]\n [network where process.name : \"hh.exe\" and\n not cidrmatch(destination.ip, \"10.0.0.0/8\", \"127.0.0.0/8\", \"169.254.0.0/16\", \"172.16.0.0/12\", \"192.0.0.0/24\",\n \"192.0.0.0/29\", \"192.0.0.8/32\", \"192.0.0.9/32\", \"192.0.0.10/32\", \"192.0.0.170/32\", \"192.0.0.171/32\",\n \"192.0.2.0/24\", \"192.31.196.0/24\", \"192.52.193.0/24\", \"192.168.0.0/16\", \"192.88.99.0/24\", \"224.0.0.0/4\",\n \"100.64.0.0/10\", \"192.175.48.0/24\",\"198.18.0.0/15\", \"198.51.100.0/24\", \"203.0.113.0/24\", \"240.0.0.0/4\", \"::1\",\n \"FE80::/10\", \"FF00::/8\")]\n", + "references": [ + "https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml" + ], "risk_score": 21, "rule_id": "b29ee2be-bf99-446c-ab1a-2dc0183394b8", "severity": "low", @@ -57,5 +60,5 @@ } ], "type": "eql", - "version": 7 + "version": 8 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_installer_spawned_network_event.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_installer_spawned_network_event.json index 6fce74fb44b12..5781c25789b94 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_installer_spawned_network_event.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_installer_spawned_network_event.json @@ -13,9 +13,10 @@ "language": "eql", "license": "Elastic License v2", "name": "macOS Installer Spawns Network Event", - "query": "sequence by process.entity_id with maxspan=1m\n [ process where event.type == \"start\" and host.os.family == \"macos\" and \n process.parent.executable in (\"/usr/sbin/installer\", \"/System/Library/CoreServices/Installer.app/Contents/MacOS/Installer\") ]\n [ network where not cidrmatch(destination.ip,\n \"192.168.0.0/16\",\n \"10.0.0.0/8\",\n \"172.16.0.0/12\",\n \"224.0.0.0/8\",\n \"127.0.0.0/8\",\n \"169.254.0.0/16\",\n \"::1\",\n \"FE80::/10\",\n \"FF00::/8\") ]\n", + "query": "sequence by process.entity_id with maxspan=1m\n [process where event.type == \"start\" and host.os.family == \"macos\" and\n process.parent.executable in (\"/usr/sbin/installer\", \"/System/Library/CoreServices/Installer.app/Contents/MacOS/Installer\") ]\n [network where not cidrmatch(destination.ip,\n \"10.0.0.0/8\", \"127.0.0.0/8\", \"169.254.0.0/16\", \"172.16.0.0/12\", \"192.0.0.0/24\", \"192.0.0.0/29\", \"192.0.0.8/32\",\n \"192.0.0.9/32\", \"192.0.0.10/32\", \"192.0.0.170/32\", \"192.0.0.171/32\", \"192.0.2.0/24\", \"192.31.196.0/24\",\n \"192.52.193.0/24\", \"192.168.0.0/16\", \"192.88.99.0/24\", \"224.0.0.0/4\", \"100.64.0.0/10\", \"192.175.48.0/24\",\n \"198.18.0.0/15\", \"198.51.100.0/24\", \"203.0.113.0/24\", \"240.0.0.0/4\", \"::1\", \"FE80::/10\", \"FF00::/8\")]\n", "references": [ - "https://redcanary.com/blog/clipping-silver-sparrows-wings" + "https://redcanary.com/blog/clipping-silver-sparrows-wings", + "https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml" ], "risk_score": 47, "rule_id": "99239e7d-b0d4-46e3-8609-acafcf99f68c", @@ -75,5 +76,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 1 + "version": 2 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_pentest_eggshell_remote_admin_tool.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_pentest_eggshell_remote_admin_tool.json index eb453205441a7..77537be3f1cbb 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_pentest_eggshell_remote_admin_tool.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_pentest_eggshell_remote_admin_tool.json @@ -11,7 +11,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "EggShell Backdoor Execution", - "query": "event.category:process and event.type:(start or process_started) and process.name:espl and process.args:eyJkZWJ1ZyI6*", + "query": "event.category:process and event.type:(start or process_started) and process.name:espl and process.args:eyJkZWJ1ZyI6*\n", "references": [ "https://github.com/neoneggplant/EggShell" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_perl_tty_shell.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_perl_tty_shell.json index b2d71b355e3a1..0f1e857232e40 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_perl_tty_shell.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_perl_tty_shell.json @@ -11,7 +11,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Interactive Terminal Spawned via Perl", - "query": "event.category:process and event.type:(start or process_started) and process.name:perl and process.args:(\"exec \\\"/bin/sh\\\";\" or \"exec \\\"/bin/dash\\\";\" or \"exec \\\"/bin/bash\\\";\")", + "query": "event.category:process and event.type:(start or process_started) and process.name:perl and\n process.args:(\"exec \\\"/bin/sh\\\";\" or \"exec \\\"/bin/dash\\\";\" or \"exec \\\"/bin/bash\\\";\")\n", "risk_score": 73, "rule_id": "05e5a668-7b51-4a67-93ab-e9af405c9ef3", "severity": "high", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_python_tty_shell.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_python_tty_shell.json index 402f5b0ab33ff..2c40ff28bfa11 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_python_tty_shell.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_python_tty_shell.json @@ -11,7 +11,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Interactive Terminal Spawned via Python", - "query": "event.category:process and event.type:(start or process_started) and process.name:python and process.args:(\"import pty; pty.spawn(\\\"/bin/sh\\\")\" or \"import pty; pty.spawn(\\\"/bin/dash\\\")\" or \"import pty; pty.spawn(\\\"/bin/bash\\\")\")", + "query": "event.category:process and event.type:(start or process_started) and process.name:python and\n process.args:(\"import pty; pty.spawn(\\\"/bin/sh\\\")\" or\n \"import pty; pty.spawn(\\\"/bin/dash\\\")\" or\n \"import pty; pty.spawn(\\\"/bin/bash\\\")\")\n", "risk_score": 73, "rule_id": "d76b02ef-fc95-4001-9297-01cb7412232f", "severity": "high", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_register_server_program_connecting_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_register_server_program_connecting_to_the_internet.json index ca919d06e34a4..2ccc730c3fa01 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_register_server_program_connecting_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_register_server_program_connecting_to_the_internet.json @@ -15,7 +15,10 @@ "language": "eql", "license": "Elastic License v2", "name": "Network Connection via Registration Utility", - "query": "sequence by process.entity_id\n [process where event.type == \"start\" and\n process.name : (\"regsvr32.exe\", \"RegAsm.exe\", \"RegSvcs.exe\") and\n not (\n user.id == \"S-1-5-18\" and\n (process.parent.name : \"msiexec.exe\" or process.parent.executable : (\"C:\\\\Program Files (x86)\\\\*.exe\", \"C:\\\\Program Files\\\\*.exe\"))\n )\n ]\n [network where process.name : (\"regsvr32.exe\", \"RegAsm.exe\", \"RegSvcs.exe\") and\n not cidrmatch(destination.ip, \"10.0.0.0/8\", \"169.254.169.254\", \"172.16.0.0/12\", \"192.168.0.0/16\") and network.protocol != \"dns\"]\n", + "query": "sequence by process.entity_id\n [process where event.type == \"start\" and\n process.name : (\"regsvr32.exe\", \"RegAsm.exe\", \"RegSvcs.exe\") and\n not (\n user.id == \"S-1-5-18\" and\n (process.parent.name : \"msiexec.exe\" or process.parent.executable : (\"C:\\\\Program Files (x86)\\\\*.exe\", \"C:\\\\Program Files\\\\*.exe\"))\n )\n ]\n [network where process.name : (\"regsvr32.exe\", \"RegAsm.exe\", \"RegSvcs.exe\") and\n not cidrmatch(destination.ip, \"10.0.0.0/8\", \"127.0.0.0/8\", \"169.254.0.0/16\", \"172.16.0.0/12\", \"192.0.0.0/24\",\n \"192.0.0.0/29\", \"192.0.0.8/32\", \"192.0.0.9/32\", \"192.0.0.10/32\", \"192.0.0.170/32\", \"192.0.0.171/32\",\n \"192.0.2.0/24\", \"192.31.196.0/24\", \"192.52.193.0/24\", \"192.168.0.0/16\", \"192.88.99.0/24\", \"224.0.0.0/4\",\n \"100.64.0.0/10\", \"192.175.48.0/24\",\"198.18.0.0/15\", \"198.51.100.0/24\", \"203.0.113.0/24\", \"240.0.0.0/4\", \"::1\",\n \"FE80::/10\", \"FF00::/8\") and network.protocol != \"dns\"]\n", + "references": [ + "https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml" + ], "risk_score": 21, "rule_id": "fb02b8d3-71ee-4af1-bacd-215d23f17efa", "severity": "low", @@ -60,5 +63,5 @@ } ], "type": "eql", - "version": 8 + "version": 9 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_scripting_osascript_exec_followed_by_netcon.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_scripting_osascript_exec_followed_by_netcon.json index 1d6fa8507ac43..74a70eb7ebd93 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_scripting_osascript_exec_followed_by_netcon.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_scripting_osascript_exec_followed_by_netcon.json @@ -11,9 +11,10 @@ "language": "eql", "license": "Elastic License v2", "name": "Apple Script Execution followed by Network Connection", - "query": "sequence by host.id, process.entity_id with maxspan=30s\n [process where event.type == \"start\" and process.name == \"osascript\"]\n [network where event.type != \"end\" and process.name == \"osascript\" and destination.ip != \"::1\" and\n not cidrmatch(destination.ip, \"10.0.0.0/8\", \n \"172.16.0.0/12\", \n \"192.168.0.0/16\", \n \"127.0.0.0/8\", \n \"169.254.0.0/16\", \n \"224.0.0.0/4\", \n \"FE80::/10\", \n \"FF00::/8\")\n ]\n", + "query": "sequence by host.id, process.entity_id with maxspan=30s\n [process where event.type == \"start\" and process.name == \"osascript\"]\n [network where event.type != \"end\" and process.name == \"osascript\" and destination.ip != \"::1\" and\n not cidrmatch(destination.ip,\n \"10.0.0.0/8\", \"127.0.0.0/8\", \"169.254.0.0/16\", \"172.16.0.0/12\", \"192.0.0.0/24\", \"192.0.0.0/29\", \"192.0.0.8/32\",\n \"192.0.0.9/32\", \"192.0.0.10/32\", \"192.0.0.170/32\", \"192.0.0.171/32\", \"192.0.2.0/24\", \"192.31.196.0/24\",\n \"192.52.193.0/24\", \"192.168.0.0/16\", \"192.88.99.0/24\", \"224.0.0.0/4\", \"100.64.0.0/10\", \"192.175.48.0/24\",\n \"198.18.0.0/15\", \"198.51.100.0/24\", \"203.0.113.0/24\", \"240.0.0.0/4\", \"::1\", \"FE80::/10\", \"FF00::/8\")]\n", "references": [ - "https://developer.apple.com/library/archive/documentation/LanguagesUtilities/Conceptual/MacAutomationScriptingGuide/index.html" + "https://developer.apple.com/library/archive/documentation/LanguagesUtilities/Conceptual/MacAutomationScriptingGuide/index.html", + "https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml" ], "risk_score": 47, "rule_id": "47f76567-d58a-4fed-b32b-21f571e28910", @@ -59,5 +60,5 @@ } ], "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/exfiltration_ec2_snapshot_change_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/exfiltration_ec2_snapshot_change_activity.json index 7c67a3f037538..f4d0877ca70b7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/exfiltration_ec2_snapshot_change_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/exfiltration_ec2_snapshot_change_activity.json @@ -16,7 +16,7 @@ "license": "Elastic License v2", "name": "AWS EC2 Snapshot Activity", "note": "## Config\n\nThe AWS Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:aws.cloudtrail and event.provider:ec2.amazonaws.com and event.action:ModifySnapshotAttribute", + "query": "event.dataset:aws.cloudtrail and event.provider:ec2.amazonaws.com and event.action:ModifySnapshotAttribute\n", "references": [ "https://awscli.amazonaws.com/v2/documentation/api/latest/reference/ec2/modify-snapshot-attribute.html", "https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_ModifySnapshotAttribute.html" diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/exfiltration_ec2_vm_export_failure.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/exfiltration_ec2_vm_export_failure.json new file mode 100644 index 0000000000000..2bf25435b84de --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/exfiltration_ec2_vm_export_failure.json @@ -0,0 +1,70 @@ +{ + "author": [ + "Elastic", + "Austin Songer" + ], + "description": "Identifies an attempt to export an AWS EC2 instance. A virtual machine (VM) export may indicate an attempt to extract or exfiltrate information.", + "false_positives": [ + "VM exports may be done by a system or network administrator. Verify whether the user identity, user agent, and/or hostname should be making changes in your environment. VM exports from unfamiliar users or hosts should be investigated. If known behavior is causing false positives, it can be exempted from the rule." + ], + "from": "now-60m", + "index": [ + "filebeat-*", + "logs-aws*" + ], + "interval": "10m", + "language": "kuery", + "license": "Elastic License v2", + "name": "AWS EC2 VM Export Failure", + "note": "## Config\nThe AWS Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", + "query": "event.dataset:aws.cloudtrail and event.provider:ec2.amazonaws.com and event.action:CreateInstanceExportTask and event.outcome:failure\n", + "references": [ + "https://docs.aws.amazon.com/vm-import/latest/userguide/vmexport.html#export-instance" + ], + "risk_score": 21, + "rule_id": "e919611d-6b6f-493b-8314-7ed6ac2e413b", + "severity": "low", + "tags": [ + "Elastic", + "Cloud", + "AWS", + "Continuous Monitoring", + "SecOps", + "Asset Visibility" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0010", + "name": "Exfiltration", + "reference": "https://attack.mitre.org/tactics/TA0010/" + }, + "technique": [ + { + "id": "T1537", + "name": "Transfer Data to Cloud Account", + "reference": "https://attack.mitre.org/techniques/T1537/" + } + ] + }, + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0009", + "name": "Collection", + "reference": "https://attack.mitre.org/tactics/TA0009/" + }, + "technique": [ + { + "id": "T1005", + "name": "Data from Local System", + "reference": "https://attack.mitre.org/techniques/T1005/" + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/exfiltration_gcp_logging_sink_modification.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/exfiltration_gcp_logging_sink_modification.json index ddff312a362ee..ac016370fe1c0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/exfiltration_gcp_logging_sink_modification.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/exfiltration_gcp_logging_sink_modification.json @@ -14,7 +14,7 @@ "license": "Elastic License v2", "name": "GCP Logging Sink Modification", "note": "## Config\n\nThe GCP Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:(googlecloud.audit or gcp.audit) and event.action:google.logging.v*.ConfigServiceV*.UpdateSink and event.outcome:success", + "query": "event.dataset:(googlecloud.audit or gcp.audit) and event.action:google.logging.v*.ConfigServiceV*.UpdateSink and event.outcome:success\n", "references": [ "https://cloud.google.com/logging/docs/export#how_sinks_work" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/exfiltration_microsoft_365_exchange_transport_rule_creation.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/exfiltration_microsoft_365_exchange_transport_rule_creation.json index ec9373352254b..a6a9e24ccb63a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/exfiltration_microsoft_365_exchange_transport_rule_creation.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/exfiltration_microsoft_365_exchange_transport_rule_creation.json @@ -15,7 +15,7 @@ "license": "Elastic License v2", "name": "Microsoft 365 Exchange Transport Rule Creation", "note": "## Config\n\nThe Microsoft 365 Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:o365.audit and event.provider:Exchange and event.category:web and event.action:\"New-TransportRule\" and event.outcome:success", + "query": "event.dataset:o365.audit and event.provider:Exchange and event.category:web and event.action:\"New-TransportRule\" and event.outcome:success\n", "references": [ "https://docs.microsoft.com/en-us/powershell/module/exchange/new-transportrule?view=exchange-ps", "https://docs.microsoft.com/en-us/exchange/security-and-compliance/mail-flow-rules/mail-flow-rules" diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/exfiltration_microsoft_365_exchange_transport_rule_mod.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/exfiltration_microsoft_365_exchange_transport_rule_mod.json index 2503f679464f1..836ade6c0b80f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/exfiltration_microsoft_365_exchange_transport_rule_mod.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/exfiltration_microsoft_365_exchange_transport_rule_mod.json @@ -15,7 +15,7 @@ "license": "Elastic License v2", "name": "Microsoft 365 Exchange Transport Rule Modification", "note": "## Config\n\nThe Microsoft 365 Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:o365.audit and event.provider:Exchange and event.category:web and event.action:(\"Remove-TransportRule\" or \"Disable-TransportRule\") and event.outcome:success", + "query": "event.dataset:o365.audit and event.provider:Exchange and event.category:web and event.action:(\"Remove-TransportRule\" or \"Disable-TransportRule\") and event.outcome:success\n", "references": [ "https://docs.microsoft.com/en-us/powershell/module/exchange/remove-transportrule?view=exchange-ps", "https://docs.microsoft.com/en-us/powershell/module/exchange/disable-transportrule?view=exchange-ps", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/external_alerts.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/external_alerts.json index b97e868b21f55..fbcc6e757c66b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/external_alerts.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/external_alerts.json @@ -15,7 +15,7 @@ "license": "Elastic License v2", "max_signals": 10000, "name": "External Alerts", - "query": "event.kind:alert and not event.module:(endgame or endpoint)", + "query": "event.kind:alert and not event.module:(endgame or endpoint)\n", "risk_score": 47, "risk_score_mapping": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/google_workspace_admin_role_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/google_workspace_admin_role_deletion.json index ba84d38d7e4ee..e0a333d92c5aa 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/google_workspace_admin_role_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/google_workspace_admin_role_deletion.json @@ -16,7 +16,7 @@ "license": "Elastic License v2", "name": "Google Workspace Admin Role Deletion", "note": "## Config\n\nThe Google Workspace Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.\n\n### Important Information Regarding Google Workspace Event Lag Times\n- As per Google's documentation, Google Workspace administrators may observe lag times ranging from minutes up to 3 days between the time of an event's occurrence and the event being visible in the Google Workspace admin/audit logs.\n- This rule is configured to run every 10 minutes with a lookback time of 130 minutes.\n- To reduce the risk of false negatives, consider reducing the interval that the Google Workspace (formerly G Suite) Filebeat module polls Google's reporting API for new events.\n- By default, `var.interval` is set to 2 hours (2h). Consider changing this interval to a lower value, such as 10 minutes (10m).\n- See the following references for further information.\n - https://support.google.com/a/answer/7061566\n - https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-module-gsuite.html", - "query": "event.dataset:(gsuite.admin or google_workspace.admin) and event.provider:admin and event.category:iam and event.action:DELETE_ROLE", + "query": "event.dataset:(gsuite.admin or google_workspace.admin) and event.provider:admin and event.category:iam and event.action:DELETE_ROLE\n", "references": [ "https://support.google.com/a/answer/2406043?hl=en" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/google_workspace_mfa_enforcement_disabled.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/google_workspace_mfa_enforcement_disabled.json index 551133ed2a58d..b05edc0566614 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/google_workspace_mfa_enforcement_disabled.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/google_workspace_mfa_enforcement_disabled.json @@ -16,7 +16,7 @@ "license": "Elastic License v2", "name": "Google Workspace MFA Enforcement Disabled", "note": "## Config\n\nThe Google Workspace Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.\n\n### Important Information Regarding Google Workspace Event Lag Times\n- As per Google's documentation, Google Workspace administrators may observe lag times ranging from minutes up to 3 days between the time of an event's occurrence and the event being visible in the Google Workspace admin/audit logs.\n- This rule is configured to run every 10 minutes with a lookback time of 130 minutes.\n- To reduce the risk of false negatives, consider reducing the interval that the Google Workspace (formerly G Suite) Filebeat module polls Google's reporting API for new events.\n- By default, `var.interval` is set to 2 hours (2h). Consider changing this interval to a lower value, such as 10 minutes (10m).\n- See the following references for further information.\n - https://support.google.com/a/answer/7061566\n - https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-module-gsuite.html", - "query": "event.dataset:(gsuite.admin or google_workspace.admin) and event.provider:admin and event.category:iam and event.action:ENFORCE_STRONG_AUTHENTICATION and gsuite.admin.new_value:false", + "query": "event.dataset:(gsuite.admin or google_workspace.admin) and event.provider:admin and event.category:iam and event.action:ENFORCE_STRONG_AUTHENTICATION and gsuite.admin.new_value:false\n", "references": [ "https://support.google.com/a/answer/9176657?hl=en#" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/google_workspace_policy_modified.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/google_workspace_policy_modified.json index 5893782912b35..58a409570f0db 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/google_workspace_policy_modified.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/google_workspace_policy_modified.json @@ -16,7 +16,7 @@ "license": "Elastic License v2", "name": "Google Workspace Password Policy Modified", "note": "## Config\n\nThe Google Workspace Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.\n\n### Important Information Regarding Google Workspace Event Lag Times\n- As per Google's documentation, Google Workspace administrators may observe lag times ranging from minutes up to 3 days between the time of an event's occurrence and the event being visible in the Google Workspace admin/audit logs.\n- This rule is configured to run every 10 minutes with a lookback time of 130 minutes.\n- To reduce the risk of false negatives, consider reducing the interval that the Google Workspace (formerly G Suite) Filebeat module polls Google's reporting API for new events.\n- By default, `var.interval` is set to 2 hours (2h). Consider changing this interval to a lower value, such as 10 minutes (10m).\n- See the following references for further information.\n - https://support.google.com/a/answer/7061566\n - https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-module-gsuite.html", - "query": "event.dataset:(gsuite.admin or google_workspace.admin) and event.provider:admin and event.category:iam and event.action:(CHANGE_APPLICATION_SETTING or CREATE_APPLICATION_SETTING) and gsuite.admin.setting.name:( \"Password Management - Enforce strong password\" or \"Password Management - Password reset frequency\" or \"Password Management - Enable password reuse\" or \"Password Management - Enforce password policy at next login\" or \"Password Management - Minimum password length\" or \"Password Management - Maximum password length\" )", + "query": "event.dataset:(gsuite.admin or google_workspace.admin) and\n event.provider:admin and event.category:iam and\n event.action:(CHANGE_APPLICATION_SETTING or CREATE_APPLICATION_SETTING) and\n gsuite.admin.setting.name:(\n \"Password Management - Enforce strong password\" or\n \"Password Management - Password reset frequency\" or\n \"Password Management - Enable password reuse\" or\n \"Password Management - Enforce password policy at next login\" or\n \"Password Management - Minimum password length\" or\n \"Password Management - Maximum password length\"\n )\n", "risk_score": 47, "rule_id": "a99f82f5-8e77-4f8b-b3ce-10c0f6afbc73", "severity": "medium", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_attempt_to_revoke_okta_api_token.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_attempt_to_revoke_okta_api_token.json index aebda3bb268d1..ba55b3fc7a9bb 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_attempt_to_revoke_okta_api_token.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_attempt_to_revoke_okta_api_token.json @@ -14,7 +14,7 @@ "license": "Elastic License v2", "name": "Attempt to Revoke Okta API Token", "note": "## Config\n\nThe Okta Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:okta.system and event.action:system.api_token.revoke", + "query": "event.dataset:okta.system and event.action:system.api_token.revoke\n", "references": [ "https://developer.okta.com/docs/reference/api/system-log/", "https://developer.okta.com/docs/reference/api/event-types/" diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_azure_automation_runbook_deleted.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_azure_automation_runbook_deleted.json index fb468ff8f6d7d..83fd544c51f13 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_azure_automation_runbook_deleted.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_azure_automation_runbook_deleted.json @@ -12,7 +12,7 @@ "license": "Elastic License v2", "name": "Azure Automation Runbook Deleted", "note": "## Config\n\nThe Azure Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:azure.activitylogs and azure.activitylogs.operation_name:\"MICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/RUNBOOKS/DELETE\" and event.outcome:(Success or success)", + "query": "event.dataset:azure.activitylogs and azure.activitylogs.operation_name:\"MICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/RUNBOOKS/DELETE\" and event.outcome:(Success or success)\n", "references": [ "https://powerzure.readthedocs.io/en/latest/Functions/operational.html#create-backdoor", "https://github.com/hausec/PowerZure", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_cloudtrail_logging_updated.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_cloudtrail_logging_updated.json index 9d1c0d3758a9d..196f54bfc174d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_cloudtrail_logging_updated.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_cloudtrail_logging_updated.json @@ -16,7 +16,7 @@ "license": "Elastic License v2", "name": "AWS CloudTrail Log Updated", "note": "## Config\n\nThe AWS Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:aws.cloudtrail and event.provider:cloudtrail.amazonaws.com and event.action:UpdateTrail and event.outcome:success", + "query": "event.dataset:aws.cloudtrail and event.provider:cloudtrail.amazonaws.com and event.action:UpdateTrail and event.outcome:success\n", "references": [ "https://docs.aws.amazon.com/awscloudtrail/latest/APIReference/API_UpdateTrail.html", "https://awscli.amazonaws.com/v2/documentation/api/latest/reference/cloudtrail/update-trail.html" diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_cloudwatch_log_group_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_cloudwatch_log_group_deletion.json index 14f0419cb4073..f92d6ec44d655 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_cloudwatch_log_group_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_cloudwatch_log_group_deletion.json @@ -16,7 +16,7 @@ "license": "Elastic License v2", "name": "AWS CloudWatch Log Group Deletion", "note": "## Config\n\nThe AWS Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:aws.cloudtrail and event.provider:logs.amazonaws.com and event.action:DeleteLogGroup and event.outcome:success", + "query": "event.dataset:aws.cloudtrail and event.provider:logs.amazonaws.com and event.action:DeleteLogGroup and event.outcome:success\n", "references": [ "https://awscli.amazonaws.com/v2/documentation/api/latest/reference/logs/delete-log-group.html", "https://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_DeleteLogGroup.html" diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_cloudwatch_log_stream_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_cloudwatch_log_stream_deletion.json index 6d37a106aa9ab..46ac2d19889f2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_cloudwatch_log_stream_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_cloudwatch_log_stream_deletion.json @@ -16,7 +16,7 @@ "license": "Elastic License v2", "name": "AWS CloudWatch Log Stream Deletion", "note": "## Config\n\nThe AWS Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:aws.cloudtrail and event.provider:logs.amazonaws.com and event.action:DeleteLogStream and event.outcome:success", + "query": "event.dataset:aws.cloudtrail and event.provider:logs.amazonaws.com and event.action:DeleteLogStream and event.outcome:success\n", "references": [ "https://awscli.amazonaws.com/v2/documentation/api/latest/reference/logs/delete-log-stream.html", "https://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_DeleteLogStream.html" diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_ec2_disable_ebs_encryption.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_ec2_disable_ebs_encryption.json index 94b1839dfc5dd..04f1adaee3edb 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_ec2_disable_ebs_encryption.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_ec2_disable_ebs_encryption.json @@ -16,7 +16,7 @@ "license": "Elastic License v2", "name": "AWS EC2 Encryption Disabled", "note": "## Config\n\nThe AWS Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:aws.cloudtrail and event.provider:ec2.amazonaws.com and event.action:DisableEbsEncryptionByDefault and event.outcome:success", + "query": "event.dataset:aws.cloudtrail and event.provider:ec2.amazonaws.com and event.action:DisableEbsEncryptionByDefault and event.outcome:success\n", "references": [ "https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSEncryption.html", "https://awscli.amazonaws.com/v2/documentation/api/latest/reference/ec2/disable-ebs-encryption-by-default.html", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_gcp_iam_role_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_gcp_iam_role_deletion.json index 5ed1aa4386de4..76901da74ce96 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_gcp_iam_role_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_gcp_iam_role_deletion.json @@ -14,7 +14,7 @@ "license": "Elastic License v2", "name": "GCP IAM Role Deletion", "note": "## Config\n\nThe GCP Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:(googlecloud.audit or gcp.audit) and event.action:google.iam.admin.v*.DeleteRole and event.outcome:success", + "query": "event.dataset:(googlecloud.audit or gcp.audit) and event.action:google.iam.admin.v*.DeleteRole and event.outcome:success\n", "references": [ "https://cloud.google.com/iam/docs/understanding-roles" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_gcp_service_account_deleted.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_gcp_service_account_deleted.json index 134278f30abb6..e928a60c132f6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_gcp_service_account_deleted.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_gcp_service_account_deleted.json @@ -14,7 +14,7 @@ "license": "Elastic License v2", "name": "GCP Service Account Deletion", "note": "## Config\n\nThe GCP Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:(googlecloud.audit or gcp.audit) and event.action:google.iam.admin.v*.DeleteServiceAccount and event.outcome:success", + "query": "event.dataset:(googlecloud.audit or gcp.audit) and event.action:google.iam.admin.v*.DeleteServiceAccount and event.outcome:success\n", "references": [ "https://cloud.google.com/iam/docs/service-accounts" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_gcp_service_account_disabled.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_gcp_service_account_disabled.json index a7750fc586729..6c205987eaf17 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_gcp_service_account_disabled.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_gcp_service_account_disabled.json @@ -14,7 +14,7 @@ "license": "Elastic License v2", "name": "GCP Service Account Disabled", "note": "## Config\n\nThe GCP Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:(googlecloud.audit or gcp.audit) and event.action:google.iam.admin.v*.DisableServiceAccount and event.outcome:success", + "query": "event.dataset:(googlecloud.audit or gcp.audit) and event.action:google.iam.admin.v*.DisableServiceAccount and event.outcome:success\n", "references": [ "https://cloud.google.com/iam/docs/service-accounts" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_gcp_storage_bucket_deleted.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_gcp_storage_bucket_deleted.json index c534dae963970..c3aa1523e4d1e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_gcp_storage_bucket_deleted.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_gcp_storage_bucket_deleted.json @@ -14,7 +14,7 @@ "license": "Elastic License v2", "name": "GCP Storage Bucket Deletion", "note": "## Config\n\nThe GCP Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:(googlecloud.audit or gcp.audit) and event.action:\"storage.buckets.delete\"", + "query": "event.dataset:(googlecloud.audit or gcp.audit) and event.action:\"storage.buckets.delete\"\n", "references": [ "https://cloud.google.com/storage/docs/key-terms#buckets" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_gcp_virtual_private_cloud_network_deleted.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_gcp_virtual_private_cloud_network_deleted.json index caad64c558863..2748a1f573e9f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_gcp_virtual_private_cloud_network_deleted.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_gcp_virtual_private_cloud_network_deleted.json @@ -14,7 +14,7 @@ "license": "Elastic License v2", "name": "GCP Virtual Private Cloud Network Deletion", "note": "## Config\n\nThe GCP Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:(googlecloud.audit or gcp.audit) and event.action:v*.compute.networks.delete and event.outcome:success", + "query": "event.dataset:(googlecloud.audit or gcp.audit) and event.action:v*.compute.networks.delete and event.outcome:success\n", "references": [ "https://cloud.google.com/vpc/docs/vpc" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_gcp_virtual_private_cloud_route_created.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_gcp_virtual_private_cloud_route_created.json index 7e2090de29c62..ef4ee82058d32 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_gcp_virtual_private_cloud_route_created.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_gcp_virtual_private_cloud_route_created.json @@ -14,7 +14,7 @@ "license": "Elastic License v2", "name": "GCP Virtual Private Cloud Route Creation", "note": "## Config\n\nThe GCP Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:(googlecloud.audit or gcp.audit) and event.action:(v*.compute.routes.insert or \"beta.compute.routes.insert\")", + "query": "event.dataset:(googlecloud.audit or gcp.audit) and event.action:(v*.compute.routes.insert or \"beta.compute.routes.insert\")\n", "references": [ "https://cloud.google.com/vpc/docs/routes", "https://cloud.google.com/vpc/docs/using-routes" diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_gcp_virtual_private_cloud_route_deleted.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_gcp_virtual_private_cloud_route_deleted.json index a8b8b12b60960..0d4f5591c9688 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_gcp_virtual_private_cloud_route_deleted.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_gcp_virtual_private_cloud_route_deleted.json @@ -14,7 +14,7 @@ "license": "Elastic License v2", "name": "GCP Virtual Private Cloud Route Deletion", "note": "## Config\n\nThe GCP Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:(googlecloud.audit or gcp.audit) and event.action:v*.compute.routes.delete and event.outcome:success", + "query": "event.dataset:(googlecloud.audit or gcp.audit) and event.action:v*.compute.routes.delete and event.outcome:success\n", "references": [ "https://cloud.google.com/vpc/docs/routes", "https://cloud.google.com/vpc/docs/using-routes" diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_iam_deactivate_mfa_device.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_iam_deactivate_mfa_device.json index 7b77a414dd1e0..cb4f1a15cd337 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_iam_deactivate_mfa_device.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_iam_deactivate_mfa_device.json @@ -17,7 +17,7 @@ "license": "Elastic License v2", "name": "AWS IAM Deactivation of MFA Device", "note": "## Config\n\nThe AWS Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:aws.cloudtrail and event.provider:iam.amazonaws.com and event.action:(DeactivateMFADevice or DeleteVirtualMFADevice) and event.outcome:success", + "query": "event.dataset:aws.cloudtrail and event.provider:iam.amazonaws.com and event.action:(DeactivateMFADevice or DeleteVirtualMFADevice) and event.outcome:success\n", "references": [ "https://awscli.amazonaws.com/v2/documentation/api/latest/reference/iam/deactivate-mfa-device.html", "https://docs.aws.amazon.com/IAM/latest/APIReference/API_DeactivateMFADevice.html" diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_iam_group_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_iam_group_deletion.json index 2c43b0560e660..e1f5fcbf4836e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_iam_group_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_iam_group_deletion.json @@ -16,7 +16,7 @@ "license": "Elastic License v2", "name": "AWS IAM Group Deletion", "note": "## Config\n\nThe AWS Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:aws.cloudtrail and event.provider:iam.amazonaws.com and event.action:DeleteGroup and event.outcome:success", + "query": "event.dataset:aws.cloudtrail and event.provider:iam.amazonaws.com and event.action:DeleteGroup and event.outcome:success\n", "references": [ "https://awscli.amazonaws.com/v2/documentation/api/latest/reference/iam/delete-group.html", "https://docs.aws.amazon.com/IAM/latest/APIReference/API_DeleteGroup.html" diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_possible_okta_dos_attack.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_possible_okta_dos_attack.json index 649c859de8184..7372eb0e0286b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_possible_okta_dos_attack.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_possible_okta_dos_attack.json @@ -11,7 +11,7 @@ "license": "Elastic License v2", "name": "Possible Okta DoS Attack", "note": "## Config\n\nThe Okta Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:okta.system and event.action:(application.integration.rate_limit_exceeded or system.org.rate_limit.warning or system.org.rate_limit.violation or core.concurrency.org.limit.violation)", + "query": "event.dataset:okta.system and event.action:(application.integration.rate_limit_exceeded or system.org.rate_limit.warning or system.org.rate_limit.violation or core.concurrency.org.limit.violation)\n", "references": [ "https://developer.okta.com/docs/reference/api/system-log/", "https://developer.okta.com/docs/reference/api/event-types/" diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_rds_cluster_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_rds_cluster_deletion.json index 94837f665af75..08ae6ce11bbb5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_rds_cluster_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_rds_cluster_deletion.json @@ -16,7 +16,7 @@ "license": "Elastic License v2", "name": "AWS RDS Cluster Deletion", "note": "## Config\n\nThe AWS Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:aws.cloudtrail and event.provider:rds.amazonaws.com and event.action:(DeleteDBCluster or DeleteGlobalCluster) and event.outcome:success", + "query": "event.dataset:aws.cloudtrail and event.provider:rds.amazonaws.com and event.action:(DeleteDBCluster or DeleteGlobalCluster) and event.outcome:success\n", "references": [ "https://awscli.amazonaws.com/v2/documentation/api/latest/reference/rds/delete-db-cluster.html", "https://docs.aws.amazon.com/AmazonRDS/latest/APIReference/API_DeleteDBCluster.html", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_rds_instance_cluster_stoppage.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_rds_instance_cluster_stoppage.json index 065c39f8f676b..e35a0c145ac59 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_rds_instance_cluster_stoppage.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_rds_instance_cluster_stoppage.json @@ -16,7 +16,7 @@ "license": "Elastic License v2", "name": "AWS RDS Instance/Cluster Stoppage", "note": "## Config\n\nThe AWS Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:aws.cloudtrail and event.provider:rds.amazonaws.com and event.action:(StopDBCluster or StopDBInstance) and event.outcome:success", + "query": "event.dataset:aws.cloudtrail and event.provider:rds.amazonaws.com and event.action:(StopDBCluster or StopDBInstance) and event.outcome:success\n", "references": [ "https://awscli.amazonaws.com/v2/documentation/api/latest/reference/rds/stop-db-cluster.html", "https://docs.aws.amazon.com/AmazonRDS/latest/APIReference/API_StopDBCluster.html", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_resource_group_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_resource_group_deletion.json index 221f1b1e46eb2..16153a832dcb9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_resource_group_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_resource_group_deletion.json @@ -15,7 +15,7 @@ "license": "Elastic License v2", "name": "Azure Resource Group Deletion", "note": "## Config\n\nThe Azure Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:azure.activitylogs and azure.activitylogs.operation_name:\"MICROSOFT.RESOURCES/SUBSCRIPTIONS/RESOURCEGROUPS/DELETE\" and event.outcome:(Success or success)", + "query": "event.dataset:azure.activitylogs and azure.activitylogs.operation_name:\"MICROSOFT.RESOURCES/SUBSCRIPTIONS/RESOURCEGROUPS/DELETE\" and event.outcome:(Success or success)\n", "references": [ "https://docs.microsoft.com/en-us/azure/azure-resource-manager/management/manage-resource-groups-portal" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/index.ts index e910b1a10f586..dc3ca4ceed4c8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/index.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/index.ts @@ -544,7 +544,11 @@ import rule531 from './ml_spike_in_traffic_to_a_country.json'; import rule532 from './command_and_control_tunneling_via_earthworm.json'; import rule533 from './lateral_movement_evasion_rdp_shadowing.json'; import rule534 from './threat_intel_module_match.json'; -import rule535 from './persistence_via_bits_job_notify_command.json'; +import rule535 from './exfiltration_ec2_vm_export_failure.json'; +import rule536 from './defense_evasion_suspicious_execution_from_mounted_device.json'; +import rule537 from './defense_evasion_unusual_network_connection_via_dllhost.json'; +import rule538 from './defense_evasion_amsienable_key_mod.json'; +import rule539 from './persistence_via_bits_job_notify_command.json'; export const rawRules = [ rule1, @@ -1082,4 +1086,8 @@ export const rawRules = [ rule533, rule534, rule535, + rule536, + rule537, + rule538, + rule539, ]; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_azure_active_directory_high_risk_signin.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_azure_active_directory_high_risk_signin.json index b18ab4090b1f8..008f6ac7b874c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_azure_active_directory_high_risk_signin.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_azure_active_directory_high_risk_signin.json @@ -13,7 +13,7 @@ "license": "Elastic License v2", "name": "Azure Active Directory High Risk Sign-in", "note": "## Config\n\nThe Azure Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:azure.signinlogs and azure.signinlogs.properties.risk_level_during_signin:high and event.outcome:(success or Success)", + "query": "event.dataset:azure.signinlogs and\n azure.signinlogs.properties.risk_level_during_signin:high and\n event.outcome:(success or Success)\n", "references": [ "https://docs.microsoft.com/en-us/azure/active-directory/conditional-access/howto-conditional-access-policy-risk", "https://docs.microsoft.com/en-us/azure/active-directory/identity-protection/overview-identity-protection", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_azure_active_directory_powershell_signin.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_azure_active_directory_powershell_signin.json index 7e77d851eac00..207d5534efd79 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_azure_active_directory_powershell_signin.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_azure_active_directory_powershell_signin.json @@ -15,7 +15,7 @@ "license": "Elastic License v2", "name": "Azure Active Directory PowerShell Sign-in", "note": "## Config\n\nThe Azure Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:azure.signinlogs and azure.signinlogs.properties.app_display_name:\"Azure Active Directory PowerShell\" and azure.signinlogs.properties.token_issuer_type:AzureAD and event.outcome:(success or Success)", + "query": "event.dataset:azure.signinlogs and\n azure.signinlogs.properties.app_display_name:\"Azure Active Directory PowerShell\" and\n azure.signinlogs.properties.token_issuer_type:AzureAD and event.outcome:(success or Success)\n", "references": [ "https://msrc-blog.microsoft.com/2020/12/13/customer-guidance-on-recent-nation-state-cyber-attacks/", "https://docs.microsoft.com/en-us/microsoft-365/enterprise/connect-to-microsoft-365-powershell?view=o365-worldwide" diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_consent_grant_attack_via_azure_registered_application.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_consent_grant_attack_via_azure_registered_application.json index f73c30f55eb3a..45acc46fcd49d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_consent_grant_attack_via_azure_registered_application.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_consent_grant_attack_via_azure_registered_application.json @@ -12,7 +12,7 @@ "license": "Elastic License v2", "name": "Possible Consent Grant Attack via Azure-Registered Application", "note": "## Triage and analysis\n\n- In a consent grant attack, an attacker tricks an end user into granting a malicious application consent to access their data, usually via a phishing attack. After the malicious application has been granted consent, it has account-level access to data without the need for an organizational account.\n- Normal remediation steps, like resetting passwords for breached accounts or requiring Multi-Factor Authentication (MFA) on accounts, are not effective against this type of attack, since these are third-party applications and are external to the organization.\n- Security analysts should review the list of trusted applications for any suspicious items.\n\n\n## Config\n\nThe Azure Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:(azure.activitylogs or azure.auditlogs or o365.audit) and ( azure.activitylogs.operation_name:\"Consent to application\" or azure.auditlogs.operation_name:\"Consent to application\" or o365.audit.Operation:\"Consent to application.\" ) and event.outcome:(Success or success)", + "query": "event.dataset:(azure.activitylogs or azure.auditlogs or o365.audit) and \n (\n azure.activitylogs.operation_name:\"Consent to application\" or\n azure.auditlogs.operation_name:\"Consent to application\" or\n o365.audit.Operation:\"Consent to application.\"\n ) and\n event.outcome:(Success or success)\n", "references": [ "https://docs.microsoft.com/en-us/microsoft-365/security/office-365-security/detect-and-remediate-illicit-consent-grants?view=o365-worldwide" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_console_login_root.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_console_login_root.json index 7d199f72a22bd..346f679b6c570 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_console_login_root.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_console_login_root.json @@ -16,7 +16,7 @@ "license": "Elastic License v2", "name": "AWS Management Console Root Login", "note": "## Config\n\nThe AWS Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:aws.cloudtrail and event.provider:signin.amazonaws.com and event.action:ConsoleLogin and aws.cloudtrail.user_identity.type:Root and event.outcome:success", + "query": "event.dataset:aws.cloudtrail and event.provider:signin.amazonaws.com and event.action:ConsoleLogin and aws.cloudtrail.user_identity.type:Root and event.outcome:success\n", "references": [ "https://docs.aws.amazon.com/IAM/latest/UserGuide/id_root-user.html" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_external_guest_user_invite.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_external_guest_user_invite.json index fa04b1f724ef7..70c3ea5de73f9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_external_guest_user_invite.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_external_guest_user_invite.json @@ -15,7 +15,7 @@ "license": "Elastic License v2", "name": "Azure External Guest User Invitation", "note": "## Config\n\nThe Azure Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:azure.auditlogs and azure.auditlogs.operation_name:\"Invite external user\" and azure.auditlogs.properties.target_resources.*.display_name:guest and event.outcome:(Success or success)", + "query": "event.dataset:azure.auditlogs and azure.auditlogs.operation_name:\"Invite external user\" and azure.auditlogs.properties.target_resources.*.display_name:guest and event.outcome:(Success or success)\n", "references": [ "https://docs.microsoft.com/en-us/azure/governance/policy/samples/cis-azure-1-1-0" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_gcp_iam_custom_role_creation.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_gcp_iam_custom_role_creation.json index a823f162d405a..7cd94103c6395 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_gcp_iam_custom_role_creation.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_gcp_iam_custom_role_creation.json @@ -14,7 +14,7 @@ "license": "Elastic License v2", "name": "GCP IAM Custom Role Creation", "note": "## Config\n\nThe GCP Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:(googlecloud.audit or gcp.audit) and event.action:google.iam.admin.v*.CreateRole and event.outcome:success", + "query": "event.dataset:(googlecloud.audit or gcp.audit) and event.action:google.iam.admin.v*.CreateRole and event.outcome:success\n", "references": [ "https://cloud.google.com/iam/docs/understanding-custom-roles" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_login_failures.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_login_failures.json index 92b9bf7dc8090..18a201e7638ef 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_login_failures.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_login_failures.json @@ -9,7 +9,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Auditd Max Failed Login Attempts", - "query": "event.module:auditd and event.action:\"failed-log-in-too-many-times-to\"", + "query": "event.module:auditd and event.action:\"failed-log-in-too-many-times-to\"\n", "references": [ "https://github.com/linux-pam/linux-pam/blob/0adbaeb273da1d45213134aa271e95987103281c/modules/pam_faillock/pam_faillock.c#L574" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_login_location.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_login_location.json index f762779103ee1..3224836eb927a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_login_location.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_login_location.json @@ -9,7 +9,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Auditd Login from Forbidden Location", - "query": "event.module:auditd and event.action:\"attempted-log-in-from-unusual-place-to\"", + "query": "event.module:auditd and event.action:\"attempted-log-in-from-unusual-place-to\"\n", "references": [ "https://github.com/linux-pam/linux-pam/blob/aac5a8fdc4aa3f7e56335a6343774cc1b63b408d/modules/pam_access/pam_access.c#L412" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_login_sessions.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_login_sessions.json index 09c389033ca07..b7a51feb122a1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_login_sessions.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_login_sessions.json @@ -9,7 +9,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Auditd Max Login Sessions", - "query": "event.module:auditd and event.action:\"opened-too-many-sessions-to\"", + "query": "event.module:auditd and event.action:\"opened-too-many-sessions-to\"\n", "references": [ "https://github.com/linux-pam/linux-pam/blob/70c32cc6fca51338f92afa58eb75b1107a5c2430/modules/pam_limits/pam_limits.c#L1007" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_login_time.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_login_time.json index 9eacbca79708a..f5ab15fb442f2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_login_time.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_login_time.json @@ -9,7 +9,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Auditd Login Attempt at Forbidden Time", - "query": "event.module:auditd and event.action:\"attempted-log-in-during-unusual-hour-to\"", + "query": "event.module:auditd and event.action:\"attempted-log-in-during-unusual-hour-to\"\n", "references": [ "https://github.com/linux-pam/linux-pam/blob/aac5a8fdc4aa3f7e56335a6343774cc1b63b408d/modules/pam_time/pam_time.c#L666" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_microsoft_365_exchange_anti_phish_policy_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_microsoft_365_exchange_anti_phish_policy_deletion.json index 46c771e5b5ede..d98c6d1fb2260 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_microsoft_365_exchange_anti_phish_policy_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_microsoft_365_exchange_anti_phish_policy_deletion.json @@ -15,7 +15,7 @@ "license": "Elastic License v2", "name": "Microsoft 365 Exchange Anti-Phish Policy Deletion", "note": "## Config\n\nThe Microsoft 365 Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:o365.audit and event.provider:Exchange and event.category:web and event.action:\"Remove-AntiPhishPolicy\" and event.outcome:success", + "query": "event.dataset:o365.audit and event.provider:Exchange and event.category:web and event.action:\"Remove-AntiPhishPolicy\" and event.outcome:success\n", "references": [ "https://docs.microsoft.com/en-us/powershell/module/exchange/remove-antiphishpolicy?view=exchange-ps", "https://docs.microsoft.com/en-us/microsoft-365/security/office-365-security/set-up-anti-phishing-policies?view=o365-worldwide" diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_microsoft_365_exchange_anti_phish_rule_mod.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_microsoft_365_exchange_anti_phish_rule_mod.json index 471a010338085..554f5f80be246 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_microsoft_365_exchange_anti_phish_rule_mod.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_microsoft_365_exchange_anti_phish_rule_mod.json @@ -15,7 +15,7 @@ "license": "Elastic License v2", "name": "Microsoft 365 Exchange Anti-Phish Rule Modification", "note": "## Config\n\nThe Microsoft 365 Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:o365.audit and event.provider:Exchange and event.category:web and event.action:(\"Remove-AntiPhishRule\" or \"Disable-AntiPhishRule\") and event.outcome:success", + "query": "event.dataset:o365.audit and event.provider:Exchange and event.category:web and event.action:(\"Remove-AntiPhishRule\" or \"Disable-AntiPhishRule\") and event.outcome:success\n", "references": [ "https://docs.microsoft.com/en-us/powershell/module/exchange/remove-antiphishrule?view=exchange-ps", "https://docs.microsoft.com/en-us/powershell/module/exchange/disable-antiphishrule?view=exchange-ps" diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_microsoft_365_exchange_safelinks_disabled.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_microsoft_365_exchange_safelinks_disabled.json index d348d790f0c4e..0f936a91023f2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_microsoft_365_exchange_safelinks_disabled.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_microsoft_365_exchange_safelinks_disabled.json @@ -15,7 +15,7 @@ "license": "Elastic License v2", "name": "Microsoft 365 Exchange Safe Link Policy Disabled", "note": "## Config\n\nThe Microsoft 365 Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:o365.audit and event.provider:Exchange and event.category:web and event.action:\"Disable-SafeLinksRule\" and event.outcome:success", + "query": "event.dataset:o365.audit and event.provider:Exchange and event.category:web and event.action:\"Disable-SafeLinksRule\" and event.outcome:success\n", "references": [ "https://docs.microsoft.com/en-us/powershell/module/exchange/disable-safelinksrule?view=exchange-ps", "https://docs.microsoft.com/en-us/microsoft-365/security/office-365-security/atp-safe-links?view=o365-worldwide" diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_password_recovery.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_password_recovery.json index fcbbcec5f06ac..9d0315ea692fb 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_password_recovery.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_password_recovery.json @@ -16,7 +16,7 @@ "license": "Elastic License v2", "name": "AWS IAM Password Recovery Requested", "note": "## Config\n\nThe AWS Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:aws.cloudtrail and event.provider:signin.amazonaws.com and event.action:PasswordRecoveryRequested and event.outcome:success", + "query": "event.dataset:aws.cloudtrail and event.provider:signin.amazonaws.com and event.action:PasswordRecoveryRequested and event.outcome:success\n", "references": [ "https://www.cadosecurity.com/2020/06/11/an-ongoing-aws-phishing-campaign/" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_rpc_remote_procedure_call_from_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_rpc_remote_procedure_call_from_the_internet.json index 81c011cda898a..8e7f8ea6a842b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_rpc_remote_procedure_call_from_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_rpc_remote_procedure_call_from_the_internet.json @@ -12,7 +12,10 @@ "language": "kuery", "license": "Elastic License v2", "name": "RPC (Remote Procedure Call) from the Internet", - "query": "event.category:(network or network_traffic) and network.transport:tcp and (destination.port:135 or event.dataset:zeek.dce_rpc) and not source.ip:( 10.0.0.0/8 or 127.0.0.0/8 or 169.254.0.0/16 or 172.16.0.0/12 or 192.168.0.0/16 or 224.0.0.0/4 or \"::1\" or \"FE80::/10\" or \"FF00::/8\" ) and destination.ip:( 10.0.0.0/8 or 172.16.0.0/12 or 192.168.0.0/16 )", + "query": "event.category:(network or network_traffic) and network.transport:tcp and (destination.port:135 or event.dataset:zeek.dce_rpc) and\n not source.ip:(\n 10.0.0.0/8 or\n 127.0.0.0/8 or\n 169.254.0.0/16 or\n 172.16.0.0/12 or\n 192.0.0.0/24 or\n 192.0.0.0/29 or\n 192.0.0.8/32 or\n 192.0.0.9/32 or\n 192.0.0.10/32 or\n 192.0.0.170/32 or\n 192.0.0.171/32 or\n 192.0.2.0/24 or\n 192.31.196.0/24 or\n 192.52.193.0/24 or\n 192.168.0.0/16 or\n 192.88.99.0/24 or\n 224.0.0.0/4 or\n 100.64.0.0/10 or\n 192.175.48.0/24 or\n 198.18.0.0/15 or\n 198.51.100.0/24 or\n 203.0.113.0/24 or\n 240.0.0.0/4 or\n \"::1\" or\n \"FE80::/10\" or\n \"FF00::/8\"\n ) and\n destination.ip:(\n 10.0.0.0/8 or\n 172.16.0.0/12 or\n 192.168.0.0/16\n )\n", + "references": [ + "https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml" + ], "risk_score": 73, "rule_id": "143cb236-0956-4f42-a706-814bcaa0cf5a", "severity": "high", @@ -42,5 +45,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 8 + "version": 9 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_rpc_remote_procedure_call_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_rpc_remote_procedure_call_to_the_internet.json index 7d1a59886164a..5892bca71d5c9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_rpc_remote_procedure_call_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_rpc_remote_procedure_call_to_the_internet.json @@ -12,7 +12,10 @@ "language": "kuery", "license": "Elastic License v2", "name": "RPC (Remote Procedure Call) to the Internet", - "query": "event.category:(network or network_traffic) and network.transport:tcp and (destination.port:135 or event.dataset:zeek.dce_rpc) and source.ip:( 10.0.0.0/8 or 172.16.0.0/12 or 192.168.0.0/16 ) and not destination.ip:( 10.0.0.0/8 or 127.0.0.0/8 or 169.254.0.0/16 or 172.16.0.0/12 or 192.168.0.0/16 or 224.0.0.0/4 or \"::1\" or \"FE80::/10\" or \"FF00::/8\" )", + "query": "event.category:(network or network_traffic) and network.transport:tcp and (destination.port:135 or event.dataset:zeek.dce_rpc) and\n source.ip:(\n 10.0.0.0/8 or\n 172.16.0.0/12 or\n 192.168.0.0/16\n ) and\n not destination.ip:(\n 10.0.0.0/8 or\n 127.0.0.0/8 or\n 169.254.0.0/16 or\n 172.16.0.0/12 or\n 192.0.0.0/24 or\n 192.0.0.0/29 or\n 192.0.0.8/32 or\n 192.0.0.9/32 or\n 192.0.0.10/32 or\n 192.0.0.170/32 or\n 192.0.0.171/32 or\n 192.0.2.0/24 or\n 192.31.196.0/24 or\n 192.52.193.0/24 or\n 192.168.0.0/16 or\n 192.88.99.0/24 or\n 224.0.0.0/4 or\n 100.64.0.0/10 or\n 192.175.48.0/24 or\n 198.18.0.0/15 or\n 198.51.100.0/24 or\n 203.0.113.0/24 or\n 240.0.0.0/4 or\n \"::1\" or\n \"FE80::/10\" or\n \"FF00::/8\"\n )\n", + "references": [ + "https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml" + ], "risk_score": 73, "rule_id": "32923416-763a-4531-bb35-f33b9232ecdb", "severity": "high", @@ -42,5 +45,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 8 + "version": 9 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_smb_windows_file_sharing_activity_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_smb_windows_file_sharing_activity_to_the_internet.json index 60f488fe9d05f..b119dc0a4f211 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_smb_windows_file_sharing_activity_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_smb_windows_file_sharing_activity_to_the_internet.json @@ -12,7 +12,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "SMB (Windows File Sharing) Activity to the Internet", - "query": "event.category:(network or network_traffic) and network.transport:tcp and (destination.port:(139 or 445) or event.dataset:zeek.smb) and source.ip:( 10.0.0.0/8 or 172.16.0.0/12 or 192.168.0.0/16 ) and not destination.ip:( 10.0.0.0/8 or 127.0.0.0/8 or 169.254.0.0/16 or 172.16.0.0/12 or 192.168.0.0/16 or 224.0.0.0/4 or \"::1\" or \"FE80::/10\" or \"FF00::/8\" )", + "query": "event.category:(network or network_traffic) and network.transport:tcp and (destination.port:(139 or 445) or event.dataset:zeek.smb) and\n source.ip:(\n 10.0.0.0/8 or\n 172.16.0.0/12 or\n 192.168.0.0/16\n ) and\n not destination.ip:(\n 10.0.0.0/8 or\n 127.0.0.0/8 or\n 169.254.0.0/16 or\n 172.16.0.0/12 or\n 192.168.0.0/16 or\n 224.0.0.0/4 or\n \"::1\" or\n \"FE80::/10\" or\n \"FF00::/8\"\n )\n", "risk_score": 73, "rule_id": "c82b2bd8-d701-420c-ba43-f11a155b681a", "severity": "high", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_suspicious_activity_reported_by_okta_user.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_suspicious_activity_reported_by_okta_user.json index 03fb7e44b200d..abcc8d06444e0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_suspicious_activity_reported_by_okta_user.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_suspicious_activity_reported_by_okta_user.json @@ -14,7 +14,7 @@ "license": "Elastic License v2", "name": "Suspicious Activity Reported by Okta User", "note": "## Config\n\nThe Okta Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:okta.system and event.action:user.account.report_suspicious_activity_by_enduser", + "query": "event.dataset:okta.system and event.action:user.account.report_suspicious_activity_by_enduser\n", "references": [ "https://developer.okta.com/docs/reference/api/system-log/", "https://developer.okta.com/docs/reference/api/event-types/" diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_unsecure_elasticsearch_node.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_unsecure_elasticsearch_node.json index 41ab4386c6817..965d4a7f62369 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_unsecure_elasticsearch_node.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_unsecure_elasticsearch_node.json @@ -13,7 +13,7 @@ "license": "Elastic License v2", "name": "Inbound Connection to an Unsecure Elasticsearch Node", "note": "## Config\n\nThis rule requires the addition of port `9200` and `send_all_headers` to the `HTTP` protocol configuration in `packetbeat.yml`. See the References section for additional configuration documentation.", - "query": "event.category:network_traffic AND network.protocol:http AND status:OK AND destination.port:9200 AND network.direction:inbound AND NOT http.response.headers.content-type:\"image/x-icon\" AND NOT _exists_:http.request.headers.authorization", + "query": "event.category:network_traffic AND network.protocol:http AND status:OK AND destination.port:9200 AND network.direction:inbound AND NOT http.response.headers.content-type:\"image/x-icon\" AND NOT _exists_:http.request.headers.authorization\n", "references": [ "https://www.elastic.co/guide/en/elasticsearch/reference/current/configuring-security.html", "https://www.elastic.co/guide/en/beats/packetbeat/current/packetbeat-http-options.html#_send_all_headers" diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_via_system_manager.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_via_system_manager.json index 73370f48253a6..70fadcc29f648 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_via_system_manager.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_via_system_manager.json @@ -16,7 +16,7 @@ "license": "Elastic License v2", "name": "AWS Execution via System Manager", "note": "## Config\n\nThe AWS Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:aws.cloudtrail and event.provider:ssm.amazonaws.com and event.action:SendCommand and event.outcome:success", + "query": "event.dataset:aws.cloudtrail and event.provider:ssm.amazonaws.com and event.action:SendCommand and event.outcome:success\n", "references": [ "https://docs.aws.amazon.com/systems-manager/latest/userguide/ssm-plugins.html" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_zoom_meeting_with_no_passcode.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_zoom_meeting_with_no_passcode.json index 9ceb2d7143713..8c3ae88116a79 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_zoom_meeting_with_no_passcode.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_zoom_meeting_with_no_passcode.json @@ -10,7 +10,7 @@ "license": "Elastic License v2", "name": "Zoom Meeting with no Passcode", "note": "## Config\n\nThe Zoom Filebeat module or similarly structured data is required to be compatible with this rule.", - "query": "event.type:creation and event.module:zoom and event.dataset:zoom.webhook and event.action:meeting.created and not zoom.meeting.password:*", + "query": "event.type:creation and event.module:zoom and event.dataset:zoom.webhook and\n event.action:meeting.created and not zoom.meeting.password:*\n", "references": [ "https://blog.zoom.us/a-message-to-our-users/", "https://www.fbi.gov/contact-us/field-offices/boston/news/press-releases/fbi-warns-of-teleconferencing-and-online-classroom-hijacking-during-covid-19-pandemic" diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_credential_access_kerberos_bifrostconsole.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_credential_access_kerberos_bifrostconsole.json index e8f28ffd5e4c6..82fa9d8d72a92 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_credential_access_kerberos_bifrostconsole.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_credential_access_kerberos_bifrostconsole.json @@ -11,7 +11,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Potential Kerberos Attack via Bifrost", - "query": "event.category:process and event.type:start and process.args:(\"-action\" and (\"-kerberoast\" or askhash or asktgs or asktgt or s4u or (\"-ticket\" and ptt) or (dump and (tickets or keytab))))", + "query": "event.category:process and event.type:start and \n process.args:(\"-action\" and (\"-kerberoast\" or askhash or asktgs or asktgt or s4u or (\"-ticket\" and ptt) or (dump and (tickets or keytab))))\n", "references": [ "https://github.com/its-a-feature/bifrost" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_dns_server_overflow.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_dns_server_overflow.json index 51bf90ff1eaf0..b34badc7c8611 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_dns_server_overflow.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_dns_server_overflow.json @@ -14,7 +14,7 @@ "license": "Elastic License v2", "name": "Abnormally Large DNS Response", "note": "## Triage and analysis\n\n### Investigating Large DNS Responses\nDetection alerts from this rule indicate an attempt was made to exploit CVE-2020-1350 (SigRed) through the use of large DNS responses on a Windows DNS server. Here are some possible avenues of investigation:\n- Investigate any corresponding Intrusion Detection Signatures (IDS) alerts that can validate this detection alert.\n- Examine the `dns.question_type` network fieldset with a protocol analyzer, such as Zeek, Packetbeat, or Suricata, for `SIG` or `RRSIG` data.\n- Validate the patch level and OS of the targeted DNS server to validate the observed activity was not large-scale Internet vulnerability scanning.\n- Validate that the source of the network activity was not from an authorized vulnerability scan or compromise assessment.", - "query": "event.category:(network or network_traffic) and destination.port:53 and (event.dataset:zeek.dns or type:dns or event.type:connection) and network.bytes > 60000", + "query": "event.category:(network or network_traffic) and destination.port:53 and\n (event.dataset:zeek.dns or type:dns or event.type:connection) and network.bytes > 60000\n", "references": [ "https://research.checkpoint.com/2020/resolving-your-way-into-domain-admin-exploiting-a-17-year-old-bug-in-windows-dns-servers/", "https://msrc-blog.microsoft.com/2020/07/14/july-2020-security-update-cve-2020-1350-vulnerability-in-windows-domain-name-system-dns-server/", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_remote_ssh_login_enabled.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_remote_ssh_login_enabled.json index 37fe1a6a99791..dc4d84d497097 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_remote_ssh_login_enabled.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_remote_ssh_login_enabled.json @@ -11,7 +11,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Remote SSH Login Enabled via systemsetup Command", - "query": "event.category:process and event.type:(start or process_started) and process.name:systemsetup and process.args:(\"-setremotelogin\" and on)", + "query": "event.category:process and event.type:(start or process_started) and\n process.name:systemsetup and\n process.args:(\"-setremotelogin\" and on)\n", "references": [ "https://documents.trendmicro.com/assets/pdf/XCSSET_Technical_Brief.pdf", "https://ss64.com/osx/systemsetup.html", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_telnet_network_activity_external.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_telnet_network_activity_external.json index 1c31fa440238a..73dca7c566421 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_telnet_network_activity_external.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_telnet_network_activity_external.json @@ -14,7 +14,10 @@ "language": "eql", "license": "Elastic License v2", "name": "Connection to External Network via Telnet", - "query": "sequence by process.entity_id\n [process where process.name == \"telnet\" and event.type == \"start\"]\n [network where process.name == \"telnet\" and\n not cidrmatch(destination.ip, \"127.0.0.0/8\", \"10.0.0.0/8\", \"172.16.0.0/12\",\n \"192.168.0.0/16\", \"FE80::/10\", \"::1/128\")]\n", + "query": "sequence by process.entity_id\n [process where process.name == \"telnet\" and event.type == \"start\"]\n [network where process.name == \"telnet\" and\n not cidrmatch(destination.ip, \"10.0.0.0/8\", \"127.0.0.0/8\", \"169.254.0.0/16\", \"172.16.0.0/12\", \"192.0.0.0/24\",\n \"192.0.0.0/29\", \"192.0.0.8/32\", \"192.0.0.9/32\", \"192.0.0.10/32\", \"192.0.0.170/32\",\n \"192.0.0.171/32\", \"192.0.2.0/24\", \"192.31.196.0/24\", \"192.52.193.0/24\",\n \"192.168.0.0/16\", \"192.88.99.0/24\", \"224.0.0.0/4\", \"100.64.0.0/10\", \"192.175.48.0/24\",\n \"198.18.0.0/15\", \"198.51.100.0/24\", \"203.0.113.0/24\", \"240.0.0.0/4\", \"::1\",\n \"FE80::/10\", \"FF00::/8\")]\n", + "references": [ + "https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml" + ], "risk_score": 47, "rule_id": "e19e64ee-130e-4c07-961f-8a339f0b8362", "severity": "medium", @@ -43,5 +46,5 @@ } ], "type": "eql", - "version": 5 + "version": 6 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_telnet_network_activity_internal.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_telnet_network_activity_internal.json index 9b4a031aa1609..31ca15c44a8bb 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_telnet_network_activity_internal.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_telnet_network_activity_internal.json @@ -14,7 +14,10 @@ "language": "eql", "license": "Elastic License v2", "name": "Connection to Internal Network via Telnet", - "query": "sequence by process.entity_id\n [process where process.name == \"telnet\" and event.type == \"start\"]\n [network where process.name == \"telnet\" and\n cidrmatch(destination.ip, \"10.0.0.0/8\", \"172.16.0.0/12\", \"192.168.0.0/16\", \"FE80::/10\") and\n not cidrmatch(destination.ip, \"127.0.0.0/8\", \"::1/128\")]\n", + "query": "sequence by process.entity_id\n [process where process.name == \"telnet\" and event.type == \"start\"]\n [network where process.name == \"telnet\" and\n cidrmatch(destination.ip, \"10.0.0.0/8\", \"127.0.0.0/8\", \"169.254.0.0/16\", \"172.16.0.0/12\", \"192.0.0.0/24\",\n \"192.0.0.0/29\", \"192.0.0.8/32\", \"192.0.0.9/32\", \"192.0.0.10/32\", \"192.0.0.170/32\",\n \"192.0.0.171/32\", \"192.0.2.0/24\", \"192.31.196.0/24\", \"192.52.193.0/24\",\n \"192.168.0.0/16\", \"192.88.99.0/24\", \"224.0.0.0/4\", \"100.64.0.0/10\", \"192.175.48.0/24\",\n \"198.18.0.0/15\", \"198.51.100.0/24\", \"203.0.113.0/24\", \"240.0.0.0/4\", \"::1\",\n \"FE80::/10\", \"FF00::/8\")]\n", + "references": [ + "https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml" + ], "risk_score": 47, "rule_id": "1b21abcc-4d9f-4b08-a7f5-316f5f94b973", "severity": "medium", @@ -43,5 +46,5 @@ } ], "type": "eql", - "version": 5 + "version": 6 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_hping_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_hping_activity.json index a706d2a117e7b..3a91b205e9da3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_hping_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_hping_activity.json @@ -14,7 +14,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Hping Process Activity", - "query": "event.category:process and event.type:(start or process_started) and process.name:(hping or hping2 or hping3)", + "query": "event.category:process and event.type:(start or process_started) and process.name:(hping or hping2 or hping3)\n", "references": [ "https://en.wikipedia.org/wiki/Hping" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_iodine_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_iodine_activity.json index cbd8c14052fee..d092c64098648 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_iodine_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_iodine_activity.json @@ -14,7 +14,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Potential DNS Tunneling via Iodine", - "query": "event.category:process and event.type:(start or process_started) and process.name:(iodine or iodined)", + "query": "event.category:process and event.type:(start or process_started) and process.name:(iodine or iodined)\n", "references": [ "https://code.kryo.se/iodine/" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_nping_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_nping_activity.json index 94c37610abf2e..c9898b7414c09 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_nping_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_nping_activity.json @@ -14,7 +14,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Nping Process Activity", - "query": "event.category:process and event.type:(start or process_started) and process.name:nping", + "query": "event.category:process and event.type:(start or process_started) and process.name:nping\n", "references": [ "https://en.wikipedia.org/wiki/Nmap" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_process_started_in_temp_directory.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_process_started_in_temp_directory.json index 7b3c8af056f18..8b71c053fc6d6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_process_started_in_temp_directory.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_process_started_in_temp_directory.json @@ -14,7 +14,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Unusual Process Execution - Temp", - "query": "event.category:process and event.type:(start or process_started) and process.working_directory:/tmp", + "query": "event.category:process and event.type:(start or process_started) and process.working_directory:/tmp\n", "risk_score": 47, "rule_id": "df959768-b0c9-4d45-988c-5606a2be8e5a", "severity": "medium", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_strace_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_strace_activity.json index d14db4869a8f7..210a71ced9222 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_strace_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_strace_activity.json @@ -14,7 +14,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Strace Process Activity", - "query": "event.category:process and event.type:(start or process_started) and process.name:strace", + "query": "event.category:process and event.type:(start or process_started) and process.name:strace\n", "references": [ "https://en.wikipedia.org/wiki/Strace" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/mfa_disabled_for_google_workspace_organization.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/mfa_disabled_for_google_workspace_organization.json index b393fa13db8c7..db8ea7369d456 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/mfa_disabled_for_google_workspace_organization.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/mfa_disabled_for_google_workspace_organization.json @@ -16,7 +16,7 @@ "license": "Elastic License v2", "name": "MFA Disabled for Google Workspace Organization", "note": "## Config\n\nThe Google Workspace Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.\n\n### Important Information Regarding Google Workspace Event Lag Times\n- As per Google's documentation, Google Workspace administrators may observe lag times ranging from minutes up to 3 days between the time of an event's occurrence and the event being visible in the Google Workspace admin/audit logs.\n- This rule is configured to run every 10 minutes with a lookback time of 130 minutes.\n- To reduce the risk of false negatives, consider reducing the interval that the Google Workspace (formerly G Suite) Filebeat module polls Google's reporting API for new events.\n- By default, `var.interval` is set to 2 hours (2h). Consider changing this interval to a lower value, such as 10 minutes (10m).\n- See the following references for further information.\n - https://support.google.com/a/answer/7061566\n - https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-module-gsuite.html", - "query": "event.dataset:(gsuite.admin or google_workspace.admin) and event.provider:admin and event.category:iam and event.action:(ENFORCE_STRONG_AUTHENTICATION or ALLOW_STRONG_AUTHENTICATION) and gsuite.admin.new_value:false", + "query": "event.dataset:(gsuite.admin or google_workspace.admin) and event.provider:admin and event.category:iam and event.action:(ENFORCE_STRONG_AUTHENTICATION or ALLOW_STRONG_AUTHENTICATION) and gsuite.admin.new_value:false\n", "risk_score": 47, "rule_id": "e555105c-ba6d-481f-82bb-9b633e7b4827", "severity": "medium", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/microsoft_365_exchange_dkim_signing_config_disabled.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/microsoft_365_exchange_dkim_signing_config_disabled.json index 280d40537e448..6933a81a22944 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/microsoft_365_exchange_dkim_signing_config_disabled.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/microsoft_365_exchange_dkim_signing_config_disabled.json @@ -15,7 +15,7 @@ "license": "Elastic License v2", "name": "Microsoft 365 Exchange DKIM Signing Configuration Disabled", "note": "## Config\n\nThe Microsoft 365 Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:o365.audit and event.provider:Exchange and event.category:web and event.action:\"Set-DkimSigningConfig\" and o365.audit.Parameters.Enabled:False and event.outcome:success", + "query": "event.dataset:o365.audit and event.provider:Exchange and event.category:web and event.action:\"Set-DkimSigningConfig\" and o365.audit.Parameters.Enabled:False and event.outcome:success\n", "references": [ "https://docs.microsoft.com/en-us/powershell/module/exchange/set-dkimsigningconfig?view=exchange-ps" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/microsoft_365_teams_custom_app_interaction_allowed.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/microsoft_365_teams_custom_app_interaction_allowed.json index cb87e9f7cb257..add3495d03271 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/microsoft_365_teams_custom_app_interaction_allowed.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/microsoft_365_teams_custom_app_interaction_allowed.json @@ -15,7 +15,7 @@ "license": "Elastic License v2", "name": "Microsoft 365 Teams Custom Application Interaction Allowed", "note": "## Config\n\nThe Microsoft 365 Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:o365.audit and event.provider:MicrosoftTeams and event.category:web and event.action:TeamsTenantSettingChanged and o365.audit.Name:\"Allow sideloading and interaction of custom apps\" and o365.audit.NewValue:True and event.outcome:success", + "query": "event.dataset:o365.audit and event.provider:MicrosoftTeams and\nevent.category:web and event.action:TeamsTenantSettingChanged and\no365.audit.Name:\"Allow sideloading and interaction of custom apps\" and\no365.audit.NewValue:True and event.outcome:success\n", "references": [ "https://docs.microsoft.com/en-us/microsoftteams/platform/concepts/deploy-and-publish/apps-upload" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_high_count_network_denies.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_high_count_network_denies.json index 7856d13b8d66f..f0d999476bb42 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_high_count_network_denies.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_high_count_network_denies.json @@ -9,8 +9,8 @@ ], "from": "now-30m", "interval": "15m", - "license": "Elastic License", - "machine_learning_job_id": "high-count-network-denies", + "license": "Elastic License v2", + "machine_learning_job_id": "high_count_network_denies", "name": "Spike in Firewall Denies", "references": [ "https://www.elastic.co/guide/en/security/current/prebuilt-ml-jobs.html" @@ -25,5 +25,5 @@ "ML" ], "type": "machine_learning", - "version": 1 + "version": 2 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_high_count_network_events.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_high_count_network_events.json index 14aec268cc13b..39ded30776bad 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_high_count_network_events.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_high_count_network_events.json @@ -9,8 +9,8 @@ ], "from": "now-30m", "interval": "15m", - "license": "Elastic License", - "machine_learning_job_id": "high-count-network-events", + "license": "Elastic License v2", + "machine_learning_job_id": "high_count_network_events", "name": "Spike in Network Traffic", "references": [ "https://www.elastic.co/guide/en/security/current/prebuilt-ml-jobs.html" @@ -25,5 +25,5 @@ "ML" ], "type": "machine_learning", - "version": 1 + "version": 2 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_metadata_process.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_metadata_process.json index 1c886d0457afc..a2638f60d7495 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_metadata_process.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_metadata_process.json @@ -10,7 +10,10 @@ "from": "now-45m", "interval": "15m", "license": "Elastic License v2", - "machine_learning_job_id": "linux_rare_metadata_process", + "machine_learning_job_id": [ + "linux_rare_metadata_process", + "v2_linux_rare_metadata_process" + ], "name": "Unusual Linux Process Calling the Metadata Service", "risk_score": 21, "rule_id": "9d302377-d226-4e12-b54c-1906b5aec4f6", @@ -23,5 +26,5 @@ "ML" ], "type": "machine_learning", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_metadata_user.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_metadata_user.json index 984060951b3c5..c176bf0d7ad68 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_metadata_user.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_metadata_user.json @@ -10,7 +10,10 @@ "from": "now-45m", "interval": "15m", "license": "Elastic License v2", - "machine_learning_job_id": "linux_rare_metadata_user", + "machine_learning_job_id": [ + "linux_rare_metadata_user", + "v2_linux_rare_metadata_user" + ], "name": "Unusual Linux User Calling the Metadata Service", "risk_score": 21, "rule_id": "1faec04b-d902-4f89-8aff-92cd9043c16f", @@ -23,5 +26,5 @@ "ML" ], "type": "machine_learning", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_network_port_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_network_port_activity.json index c56235774c077..eb6a960702ac7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_network_port_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_network_port_activity.json @@ -10,7 +10,10 @@ "from": "now-45m", "interval": "15m", "license": "Elastic License v2", - "machine_learning_job_id": "linux_anomalous_network_port_activity_ecs", + "machine_learning_job_id": [ + "linux_anomalous_network_port_activity_ecs", + "v2_linux_anomalous_network_port_activity_ecs" + ], "name": "Unusual Linux Network Port Activity", "references": [ "https://www.elastic.co/guide/en/security/current/prebuilt-ml-jobs.html" @@ -26,5 +29,5 @@ "ML" ], "type": "machine_learning", - "version": 4 + "version": 5 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_process_all_hosts.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_process_all_hosts.json index 9243389de0d97..bab02f0a6aa24 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_process_all_hosts.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_process_all_hosts.json @@ -10,7 +10,10 @@ "from": "now-45m", "interval": "15m", "license": "Elastic License v2", - "machine_learning_job_id": "linux_anomalous_process_all_hosts_ecs", + "machine_learning_job_id": [ + "linux_anomalous_process_all_hosts_ecs", + "v2_linux_anomalous_process_all_hosts_ecs" + ], "name": "Anomalous Process For a Linux Population", "note": "## Triage and analysis\n\n### Investigating an Unusual Linux Process\nDetection alerts from this rule indicate the presence of a Linux process that is rare and unusual for all of the monitored Linux hosts for which Auditbeat data is available. Here are some possible avenues of investigation:\n- Consider the user as identified by the username field. Is this program part of an expected workflow for the user who ran this program on this host?\n- Examine the history of execution. If this process manifested only very recently, it might be part of a new software package. If it has a consistent cadence - for example if it runs monthly or quarterly - it might be part of a monthly or quarterly business process.\n- Examine the process arguments, title and working directory. These may provide indications as to the source of the program or the nature of the tasks it is performing.", "references": [ @@ -27,5 +30,5 @@ "ML" ], "type": "machine_learning", - "version": 5 + "version": 6 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_user_name.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_user_name.json index e77247dc0e931..4eb10707e0eb2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_user_name.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_user_name.json @@ -10,7 +10,10 @@ "from": "now-45m", "interval": "15m", "license": "Elastic License v2", - "machine_learning_job_id": "linux_anomalous_user_name_ecs", + "machine_learning_job_id": [ + "linux_anomalous_user_name_ecs", + "v2_linux_anomalous_user_name_ecs" + ], "name": "Unusual Linux Username", "note": "## Triage and analysis\n\n### Investigating an Unusual Linux User\nDetection alerts from this rule indicate activity for a Linux user name that is rare and unusual. Here are some possible avenues of investigation:\n- Consider the user as identified by the username field. Is this program part of an expected workflow for the user who ran this program on this host? Could this be related to troubleshooting or debugging activity by a developer or site reliability engineer?\n- Examine the history of user activity. If this user manifested only very recently, it might be a service account for a new software package. If it has a consistent cadence - for example if it runs monthly or quarterly - it might be part of a monthly or quarterly business process.\n- Examine the process arguments, title and working directory. These may provide indications as to the source of the program or the nature of the tasks that the user is performing.", "references": [ @@ -27,5 +30,5 @@ "ML" ], "type": "machine_learning", - "version": 5 + "version": 6 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_rare_destination_country.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_rare_destination_country.json index 571c7e0d0d32c..8c8b7ffbd12ff 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_rare_destination_country.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_rare_destination_country.json @@ -9,8 +9,8 @@ ], "from": "now-30m", "interval": "15m", - "license": "Elastic License", - "machine_learning_job_id": "rare-destination-country", + "license": "Elastic License v2", + "machine_learning_job_id": "rare_destination_country", "name": "Network Traffic to Rare Destination Country", "references": [ "https://www.elastic.co/guide/en/security/current/prebuilt-ml-jobs.html" @@ -25,5 +25,5 @@ "ML" ], "type": "machine_learning", - "version": 1 + "version": 2 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_rare_process_by_host_linux.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_rare_process_by_host_linux.json index 11f6c2605f6ad..934a5e598629b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_rare_process_by_host_linux.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_rare_process_by_host_linux.json @@ -10,7 +10,10 @@ "from": "now-45m", "interval": "15m", "license": "Elastic License v2", - "machine_learning_job_id": "rare_process_by_host_linux_ecs", + "machine_learning_job_id": [ + "rare_process_by_host_linux_ecs", + "v2_rare_process_by_host_linux_ecs" + ], "name": "Unusual Process For a Linux Host", "note": "## Triage and analysis\n\n### Investigating an Unusual Linux Process\nDetection alerts from this rule indicate the presence of a Linux process that is rare and unusual for the host it ran on. Here are some possible avenues of investigation:\n- Consider the user as identified by the username field. Is this program part of an expected workflow for the user who ran this program on this host?\n- Examine the history of execution. If this process manifested only very recently, it might be part of a new software package. If it has a consistent cadence - for example if it runs monthly or quarterly - it might be part of a monthly or quarterly business process.\n- Examine the process arguments, title and working directory. These may provide indications as to the source of the program or the nature of the tasks it is performing.", "references": [ @@ -27,5 +30,5 @@ "ML" ], "type": "machine_learning", - "version": 5 + "version": 6 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_rare_process_by_host_windows.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_rare_process_by_host_windows.json index 72db8ff93d4d4..3373f51b69db0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_rare_process_by_host_windows.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_rare_process_by_host_windows.json @@ -10,7 +10,10 @@ "from": "now-45m", "interval": "15m", "license": "Elastic License v2", - "machine_learning_job_id": "rare_process_by_host_windows_ecs", + "machine_learning_job_id": [ + "rare_process_by_host_windows_ecs", + "v2_rare_process_by_host_windows_ecs" + ], "name": "Unusual Process For a Windows Host", "note": "## Triage and analysis\n\n### Investigating an Unusual Windows Process\nDetection alerts from this rule indicate the presence of a Windows process that is rare and unusual for the host it ran on. Here are some possible avenues of investigation:\n- Consider the user as identified by the username field. Is this program part of an expected workflow for the user who ran this program on this host?\n- Examine the history of execution. If this process manifested only very recently, it might be part of a new software package. If it has a consistent cadence - for example if it runs monthly or quarterly - it might be part of a monthly or quarterly business process.\n- Examine the process metadata like the values of the Company, Description and Product fields which may indicate whether the program is associated with an expected software vendor or package.\n- Examine arguments and working directory. These may provide indications as to the source of the program or the nature of the tasks it is performing.\n- Consider the same for the parent process. If the parent process is a legitimate system utility or service, this could be related to software updates or system management. If the parent process is something user-facing like an Office application, this process could be more suspicious.\n- If you have file hash values in the event data, and you suspect malware, you can optionally run a search for the file hash to see if the file is identified as malware by anti-malware tools. ", "references": [ @@ -27,5 +30,5 @@ "ML" ], "type": "machine_learning", - "version": 5 + "version": 6 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_spike_in_traffic_to_a_country.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_spike_in_traffic_to_a_country.json index e1e571bbd1c99..1b6c71ed0b0bf 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_spike_in_traffic_to_a_country.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_spike_in_traffic_to_a_country.json @@ -9,8 +9,8 @@ ], "from": "now-30m", "interval": "15m", - "license": "Elastic License", - "machine_learning_job_id": "high-count-by-destination-country", + "license": "Elastic License v2", + "machine_learning_job_id": "high_count_by_destination_country", "name": "Spike in Network Traffic To a Country", "references": [ "https://www.elastic.co/guide/en/security/current/prebuilt-ml-jobs.html" @@ -25,5 +25,5 @@ "ML" ], "type": "machine_learning", - "version": 1 + "version": 2 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_metadata_process.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_metadata_process.json index d19897057497c..deaf07a77c7be 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_metadata_process.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_metadata_process.json @@ -10,7 +10,10 @@ "from": "now-45m", "interval": "15m", "license": "Elastic License v2", - "machine_learning_job_id": "windows_rare_metadata_process", + "machine_learning_job_id": [ + "windows_rare_metadata_process", + "v2_windows_rare_metadata_process" + ], "name": "Unusual Windows Process Calling the Metadata Service", "risk_score": 21, "rule_id": "abae61a8-c560-4dbd-acca-1e1438bff36b", @@ -23,5 +26,5 @@ "ML" ], "type": "machine_learning", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_metadata_user.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_metadata_user.json index 27f100c4e65b8..31e078b795256 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_metadata_user.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_metadata_user.json @@ -10,7 +10,10 @@ "from": "now-45m", "interval": "15m", "license": "Elastic License v2", - "machine_learning_job_id": "windows_rare_metadata_user", + "machine_learning_job_id": [ + "windows_rare_metadata_user", + "v2_windows_rare_metadata_user" + ], "name": "Unusual Windows User Calling the Metadata Service", "risk_score": 21, "rule_id": "df197323-72a8-46a9-a08e-3f5b04a4a97a", @@ -23,5 +26,5 @@ "ML" ], "type": "machine_learning", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_network_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_network_activity.json index 5e3d5757b160d..dbee5dd256873 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_network_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_network_activity.json @@ -10,7 +10,10 @@ "from": "now-45m", "interval": "15m", "license": "Elastic License v2", - "machine_learning_job_id": "windows_anomalous_network_activity_ecs", + "machine_learning_job_id": [ + "windows_anomalous_network_activity_ecs", + "v2_windows_anomalous_network_activity_ecs" + ], "name": "Unusual Windows Network Activity", "note": "## Triage and analysis\n\n### Investigating Unusual Network Activity\nDetection alerts from this rule indicate the presence of network activity from a Windows process for which network activity is very unusual. Here are some possible avenues of investigation:\n- Consider the IP addresses, protocol and ports. Are these used by normal but infrequent network workflows? Are they expected or unexpected?\n- If the destination IP address is remote or external, does it associate with an expected domain, organization or geography? Note: avoid interacting directly with suspected malicious IP addresses.\n- Consider the user as identified by the username field. Is this network activity part of an expected workflow for the user who ran the program?\n- Examine the history of execution. If this process manifested only very recently, it might be part of a new software package. If it has a consistent cadence - for example if it runs monthly or quarterly - it might be part of a monthly or quarterly business process.\n- Examine the process arguments, title and working directory. These may provide indications as to the source of the program or the nature of the tasks it is performing.\n- Consider the same for the parent process. If the parent process is a legitimate system utility or service, this could be related to software updates or system management. If the parent process is something user-facing like an Office application, this process could be more suspicious.\n- If you have file hash values in the event data, and you suspect malware, you can optionally run a search for the file hash to see if the file is identified as malware by anti-malware tools.", "references": [ @@ -27,5 +30,5 @@ "ML" ], "type": "machine_learning", - "version": 5 + "version": 6 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_path_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_path_activity.json index 93a33e41e6ce9..782a80c53f9b3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_path_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_path_activity.json @@ -10,7 +10,10 @@ "from": "now-45m", "interval": "15m", "license": "Elastic License v2", - "machine_learning_job_id": "windows_anomalous_path_activity_ecs", + "machine_learning_job_id": [ + "windows_anomalous_path_activity_ecs", + "v2_windows_anomalous_path_activity_ecs" + ], "name": "Unusual Windows Path Activity", "references": [ "https://www.elastic.co/guide/en/security/current/prebuilt-ml-jobs.html" @@ -26,5 +29,5 @@ "ML" ], "type": "machine_learning", - "version": 4 + "version": 5 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_process_all_hosts.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_process_all_hosts.json index b73637c1a7cdf..09acb2121fd5c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_process_all_hosts.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_process_all_hosts.json @@ -10,7 +10,10 @@ "from": "now-45m", "interval": "15m", "license": "Elastic License v2", - "machine_learning_job_id": "windows_anomalous_process_all_hosts_ecs", + "machine_learning_job_id": [ + "windows_anomalous_process_all_hosts_ecs", + "v2_windows_anomalous_process_all_hosts_ecs" + ], "name": "Anomalous Process For a Windows Population", "note": "## Triage and analysis\n\n### Investigating an Unusual Windows Process\nDetection alerts from this rule indicate the presence of a Windows process that is rare and unusual for all of the Windows hosts for which Winlogbeat data is available. Here are some possible avenues of investigation:\n- Consider the user as identified by the username field. Is this program part of an expected workflow for the user who ran this program on this host?\n- Examine the history of execution. If this process manifested only very recently, it might be part of a new software package. If it has a consistent cadence - for example if it runs monthly or quarterly - it might be part of a monthly or quarterly business process.\n- Examine the process metadata like the values of the Company, Description and Product fields which may indicate whether the program is associated with an expected software vendor or package.\n- Examine arguments and working directory. These may provide indications as to the source of the program or the nature of the tasks it is performing.\n- Consider the same for the parent process. If the parent process is a legitimate system utility or service, this could be related to software updates or system management. If the parent process is something user-facing like an Office application, this process could be more suspicious.\n- If you have file hash values in the event data, and you suspect malware, you can optionally run a search for the file hash to see if the file is identified as malware by anti-malware tools. ", "references": [ @@ -27,5 +30,5 @@ "ML" ], "type": "machine_learning", - "version": 5 + "version": 6 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_process_creation.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_process_creation.json index 7b3a028f1b371..cfe9e7ff1eaa3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_process_creation.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_process_creation.json @@ -10,7 +10,10 @@ "from": "now-45m", "interval": "15m", "license": "Elastic License v2", - "machine_learning_job_id": "windows_anomalous_process_creation", + "machine_learning_job_id": [ + "windows_anomalous_process_creation", + "v2_windows_anomalous_process_creation" + ], "name": "Anomalous Windows Process Creation", "references": [ "https://www.elastic.co/guide/en/security/current/prebuilt-ml-jobs.html" @@ -26,5 +29,5 @@ "ML" ], "type": "machine_learning", - "version": 4 + "version": 5 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_user_name.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_user_name.json index d3cbc34bd1a3d..b2183c8ff66c3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_user_name.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_user_name.json @@ -10,7 +10,10 @@ "from": "now-45m", "interval": "15m", "license": "Elastic License v2", - "machine_learning_job_id": "windows_anomalous_user_name_ecs", + "machine_learning_job_id": [ + "windows_anomalous_user_name_ecs", + "v2_windows_anomalous_user_name_ecs" + ], "name": "Unusual Windows Username", "note": "## Triage and analysis\n\n### Investigating an Unusual Windows User\nDetection alerts from this rule indicate activity for a Windows user name that is rare and unusual. Here are some possible avenues of investigation:\n- Consider the user as identified by the username field. Is this program part of an expected workflow for the user who ran this program on this host? Could this be related to occasional troubleshooting or support activity?\n- Examine the history of user activity. If this user manifested only very recently, it might be a service account for a new software package. If it has a consistent cadence - for example if it runs monthly or quarterly - it might be part of a monthly or quarterly business process.\n- Examine the process arguments, title and working directory. These may provide indications as to the source of the program or the nature of the tasks that the user is performing.\n- Consider the same for the parent process. If the parent process is a legitimate system utility or service, this could be related to software updates or system management. If the parent process is something user-facing like an Office application, this process could be more suspicious.", "references": [ @@ -27,5 +30,5 @@ "ML" ], "type": "machine_learning", - "version": 5 + "version": 6 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/notice.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/notice.ts index cad41391e2b42..86d95c1e021b4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/notice.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/notice.ts @@ -2,17 +2,17 @@ /* @notice * Detection Rules - * Copyright 2020 Elasticsearch B.V. + * Copyright 2021 Elasticsearch B.V. * * --- * This product bundles rules based on https://github.com/BlueTeamLabs/sentinel-attack - * which is available under a "MIT" license. The files based on this license are: + * which is available under a "MIT" license. The rules based on this license are: * - * - defense_evasion_via_filter_manager - * - discovery_process_discovery_via_tasklist_command - * - persistence_priv_escalation_via_accessibility_features - * - persistence_via_application_shimming - * - defense_evasion_execution_via_trusted_developer_utilities + * - "Potential Evasion via Filter Manager" (06dceabf-adca-48af-ac79-ffdf4c3b1e9a) + * - "Process Discovery via Tasklist" (cc16f774-59f9-462d-8b98-d27ccd4519ec) + * - "Potential Modification of Accessibility Binaries" (7405ddf1-6c8e-41ce-818f-48bea6bcaed8) + * - "Potential Application Shimming via Sdbinst" (fd4a992d-6130-4802-9ff8-829b89ae801f) + * - "Trusted Developer Application Usage" (9d110cb3-5f4b-4c9a-b9f5-53f0a1707ae1) * * MIT License * @@ -38,9 +38,9 @@ * * --- * This product bundles rules based on https://github.com/FSecureLABS/leonidas - * which is available under a "MIT" license. The files based on this license are: + * which is available under a "MIT" license. The rules based on this license are: * - * - credential_access_secretsmanager_getsecretvalue.toml + * - "AWS Access Secret in Secrets Manager" (a00681e3-9ed6-447c-ab2c-be648821c622) * * MIT License * diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_deactivate_okta_application.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_deactivate_okta_application.json index bb4cdc6ded201..2c3512fb2ad77 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_deactivate_okta_application.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_deactivate_okta_application.json @@ -14,7 +14,7 @@ "license": "Elastic License v2", "name": "Attempt to Deactivate an Okta Application", "note": "## Config\n\nThe Okta Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:okta.system and event.action:application.lifecycle.deactivate", + "query": "event.dataset:okta.system and event.action:application.lifecycle.deactivate\n", "references": [ "https://help.okta.com/en/prod/Content/Topics/Apps/Apps_Apps.htm", "https://developer.okta.com/docs/reference/api/system-log/", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_deactivate_okta_policy.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_deactivate_okta_policy.json index 54dd1f0741488..45605a297987b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_deactivate_okta_policy.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_deactivate_okta_policy.json @@ -14,7 +14,7 @@ "license": "Elastic License v2", "name": "Attempt to Deactivate an Okta Policy", "note": "## Config\n\nThe Okta Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:okta.system and event.action:policy.lifecycle.deactivate", + "query": "event.dataset:okta.system and event.action:policy.lifecycle.deactivate\n", "references": [ "https://help.okta.com/en/prod/Content/Topics/Security/Security_Policies.htm", "https://developer.okta.com/docs/reference/api/system-log/", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_deactivate_okta_policy_rule.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_deactivate_okta_policy_rule.json index 495404ac9e108..ba25dde279426 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_deactivate_okta_policy_rule.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_deactivate_okta_policy_rule.json @@ -14,7 +14,7 @@ "license": "Elastic License v2", "name": "Attempt to Deactivate an Okta Policy Rule", "note": "## Config\n\nThe Okta Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:okta.system and event.action:policy.rule.deactivate", + "query": "event.dataset:okta.system and event.action:policy.rule.deactivate\n", "references": [ "https://help.okta.com/en/prod/Content/Topics/Security/Security_Policies.htm", "https://developer.okta.com/docs/reference/api/system-log/", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_delete_okta_application.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_delete_okta_application.json index 950964c81458a..4f18d2496e874 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_delete_okta_application.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_delete_okta_application.json @@ -14,7 +14,7 @@ "license": "Elastic License v2", "name": "Attempt to Delete an Okta Application", "note": "## Config\n\nThe Okta Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:okta.system and event.action:application.lifecycle.delete", + "query": "event.dataset:okta.system and event.action:application.lifecycle.delete\n", "references": [ "https://developer.okta.com/docs/reference/api/system-log/", "https://developer.okta.com/docs/reference/api/event-types/" diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_delete_okta_policy.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_delete_okta_policy.json index 43b146b1377b7..47660e2d5d73e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_delete_okta_policy.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_delete_okta_policy.json @@ -14,7 +14,7 @@ "license": "Elastic License v2", "name": "Attempt to Delete an Okta Policy", "note": "## Config\n\nThe Okta Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:okta.system and event.action:policy.lifecycle.delete", + "query": "event.dataset:okta.system and event.action:policy.lifecycle.delete\n", "references": [ "https://help.okta.com/en/prod/Content/Topics/Security/Security_Policies.htm", "https://developer.okta.com/docs/reference/api/system-log/", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_delete_okta_policy_rule.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_delete_okta_policy_rule.json index 3e32480d958ea..d865bda9c3a6f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_delete_okta_policy_rule.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_delete_okta_policy_rule.json @@ -14,7 +14,7 @@ "license": "Elastic License v2", "name": "Attempt to Delete an Okta Policy Rule", "note": "## Config\n\nThe Okta Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:okta.system and event.action:policy.rule.delete", + "query": "event.dataset:okta.system and event.action:policy.rule.delete\n", "references": [ "https://help.okta.com/en/prod/Content/Topics/Security/Security_Policies.htm", "https://developer.okta.com/docs/reference/api/system-log/", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_application.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_application.json index 47c0c836348d6..3c0524749eed9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_application.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_application.json @@ -14,7 +14,7 @@ "license": "Elastic License v2", "name": "Attempt to Modify an Okta Application", "note": "## Config\n\nThe Okta Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:okta.system and event.action:application.lifecycle.update", + "query": "event.dataset:okta.system and event.action:application.lifecycle.update\n", "references": [ "https://help.okta.com/en/prod/Content/Topics/Apps/Apps_Apps.htm", "https://developer.okta.com/docs/reference/api/system-log/", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_network_zone.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_network_zone.json index 05e48c9175daf..de94a52a4caa4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_network_zone.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_network_zone.json @@ -14,7 +14,7 @@ "license": "Elastic License v2", "name": "Attempt to Modify an Okta Network Zone", "note": "## Config\n\nThe Okta Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:okta.system and event.action:(zone.update or network_zone.rule.disabled or zone.remove_blacklist)", + "query": "event.dataset:okta.system and event.action:(zone.update or network_zone.rule.disabled or zone.remove_blacklist)\n", "references": [ "https://help.okta.com/en/prod/Content/Topics/Security/network/network-zones.htm", "https://developer.okta.com/docs/reference/api/system-log/", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_policy.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_policy.json index 78451913b110a..768d69341e71a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_policy.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_policy.json @@ -14,7 +14,7 @@ "license": "Elastic License v2", "name": "Attempt to Modify an Okta Policy", "note": "## Config\n\nThe Okta Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:okta.system and event.action:policy.lifecycle.update", + "query": "event.dataset:okta.system and event.action:policy.lifecycle.update\n", "references": [ "https://developer.okta.com/docs/reference/api/system-log/", "https://developer.okta.com/docs/reference/api/event-types/" diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_policy_rule.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_policy_rule.json index 3d89183e4a921..cae49f5789b29 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_policy_rule.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_policy_rule.json @@ -14,7 +14,7 @@ "license": "Elastic License v2", "name": "Attempt to Modify an Okta Policy Rule", "note": "## Config\n\nThe Okta Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:okta.system and event.action:policy.rule.update", + "query": "event.dataset:okta.system and event.action:policy.rule.update\n", "references": [ "https://help.okta.com/en/prod/Content/Topics/Security/Security_Policies.htm", "https://developer.okta.com/docs/reference/api/system-log/", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_or_delete_application_sign_on_policy.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_or_delete_application_sign_on_policy.json index ce68423ffa4a4..20e52ad1aca7b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_or_delete_application_sign_on_policy.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_or_delete_application_sign_on_policy.json @@ -14,7 +14,7 @@ "license": "Elastic License v2", "name": "Modification or Removal of an Okta Application Sign-On Policy", "note": "## Config\n\nThe Okta Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:okta.system and event.action:(application.policy.sign_on.update or application.policy.sign_on.rule.delete)", + "query": "event.dataset:okta.system and event.action:(application.policy.sign_on.update or application.policy.sign_on.rule.delete)\n", "references": [ "https://help.okta.com/en/prod/Content/Topics/Security/App_Based_Signon.htm", "https://developer.okta.com/docs/reference/api/system-log/", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_threat_detected_by_okta_threatinsight.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_threat_detected_by_okta_threatinsight.json index 51be6cc2aba45..c2225b53e30b6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_threat_detected_by_okta_threatinsight.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_threat_detected_by_okta_threatinsight.json @@ -11,7 +11,7 @@ "license": "Elastic License v2", "name": "Threat Detected by Okta ThreatInsight", "note": "## Config\n\nThe Okta Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:okta.system and event.action:security.threat.detected", + "query": "event.dataset:okta.system and event.action:security.threat.detected\n", "references": [ "https://developer.okta.com/docs/reference/api/system-log/", "https://developer.okta.com/docs/reference/api/event-types/" diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_account_creation_hide_at_logon.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_account_creation_hide_at_logon.json index faf309fac1365..dc8a5ba10fa00 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_account_creation_hide_at_logon.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_account_creation_hide_at_logon.json @@ -11,7 +11,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Potential Hidden Local User Account Creation", - "query": "event.category:process and event.type:(start or process_started) and process.name:dscl and process.args:(IsHidden and create and (true or 1 or yes))", + "query": "event.category:process and event.type:(start or process_started) and\n process.name:dscl and process.args:(IsHidden and create and (true or 1 or yes))\n", "references": [ "https://support.apple.com/en-us/HT203998" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_administrator_privileges_assigned_to_okta_group.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_administrator_privileges_assigned_to_okta_group.json index 2284e08ba0caa..071fb643083a1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_administrator_privileges_assigned_to_okta_group.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_administrator_privileges_assigned_to_okta_group.json @@ -14,7 +14,7 @@ "license": "Elastic License v2", "name": "Administrator Privileges Assigned to an Okta Group", "note": "## Config\n\nThe Okta Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:okta.system and event.action:group.privilege.grant", + "query": "event.dataset:okta.system and event.action:group.privilege.grant\n", "references": [ "https://help.okta.com/en/prod/Content/Topics/Security/administrators-admin-comparison.htm", "https://developer.okta.com/docs/reference/api/system-log/", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_administrator_role_assigned_to_okta_user.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_administrator_role_assigned_to_okta_user.json index 8aa425e4faac6..eccd892913023 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_administrator_role_assigned_to_okta_user.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_administrator_role_assigned_to_okta_user.json @@ -14,7 +14,7 @@ "license": "Elastic License v2", "name": "Administrator Role Assigned to an Okta User", "note": "## Config\n\nThe Okta Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:okta.system and event.action:user.account.privilege.grant", + "query": "event.dataset:okta.system and event.action:user.account.privilege.grant\n", "references": [ "https://help.okta.com/en/prod/Content/Topics/Security/administrators-admin-comparison.htm", "https://developer.okta.com/docs/reference/api/system-log/", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_create_okta_api_token.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_create_okta_api_token.json index df234db46fed2..02cc638a7ce0d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_create_okta_api_token.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_create_okta_api_token.json @@ -14,7 +14,7 @@ "license": "Elastic License v2", "name": "Attempt to Create Okta API Token", "note": "## Config\n\nThe Okta Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:okta.system and event.action:system.api_token.create", + "query": "event.dataset:okta.system and event.action:system.api_token.create\n", "references": [ "https://developer.okta.com/docs/reference/api/system-log/", "https://developer.okta.com/docs/reference/api/event-types/" diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_deactivate_mfa_for_okta_user_account.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_deactivate_mfa_for_okta_user_account.json index df70b91b8c8a0..29d604a2953ba 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_deactivate_mfa_for_okta_user_account.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_deactivate_mfa_for_okta_user_account.json @@ -14,7 +14,7 @@ "license": "Elastic License v2", "name": "Attempt to Deactivate MFA for an Okta User Account", "note": "## Config\n\nThe Okta Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:okta.system and event.action:user.mfa.factor.deactivate", + "query": "event.dataset:okta.system and event.action:user.mfa.factor.deactivate\n", "references": [ "https://developer.okta.com/docs/reference/api/system-log/", "https://developer.okta.com/docs/reference/api/event-types/" diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_reset_mfa_factors_for_okta_user_account.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_reset_mfa_factors_for_okta_user_account.json index a9bbbca91e2b7..2a2f5acfcc114 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_reset_mfa_factors_for_okta_user_account.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_reset_mfa_factors_for_okta_user_account.json @@ -14,7 +14,7 @@ "license": "Elastic License v2", "name": "Attempt to Reset MFA Factors for an Okta User Account", "note": "## Config\n\nThe Okta Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:okta.system and event.action:user.mfa.factor.reset_all", + "query": "event.dataset:okta.system and event.action:user.mfa.factor.reset_all\n", "references": [ "https://developer.okta.com/docs/reference/api/system-log/", "https://developer.okta.com/docs/reference/api/event-types/" diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_azure_automation_account_created.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_azure_automation_account_created.json index 5ec70f2970d28..bdae2f42bd04e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_azure_automation_account_created.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_azure_automation_account_created.json @@ -12,7 +12,7 @@ "license": "Elastic License v2", "name": "Azure Automation Account Created", "note": "## Config\n\nThe Azure Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:azure.activitylogs and azure.activitylogs.operation_name:\"MICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/WRITE\" and event.outcome:(Success or success)", + "query": "event.dataset:azure.activitylogs and azure.activitylogs.operation_name:\"MICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/WRITE\" and event.outcome:(Success or success)\n", "references": [ "https://powerzure.readthedocs.io/en/latest/Functions/operational.html#create-backdoor", "https://github.com/hausec/PowerZure", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_azure_automation_runbook_created_or_modified.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_azure_automation_runbook_created_or_modified.json index f7ef529895222..0c6a82d37701b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_azure_automation_runbook_created_or_modified.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_azure_automation_runbook_created_or_modified.json @@ -12,7 +12,7 @@ "license": "Elastic License v2", "name": "Azure Automation Runbook Created or Modified", "note": "## Config\n\nThe Azure Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:azure.activitylogs and azure.activitylogs.operation_name: ( \"MICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/RUNBOOKS/DRAFT/WRITE\" or \"MICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/RUNBOOKS/WRITE\" or \"MICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/RUNBOOKS/PUBLISH/ACTION\" ) and event.outcome:(Success or success)", + "query": "event.dataset:azure.activitylogs and\n azure.activitylogs.operation_name:\n (\n \"MICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/RUNBOOKS/DRAFT/WRITE\" or\n \"MICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/RUNBOOKS/WRITE\" or\n \"MICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/RUNBOOKS/PUBLISH/ACTION\"\n ) and\n event.outcome:(Success or success)\n", "references": [ "https://powerzure.readthedocs.io/en/latest/Functions/operational.html#create-backdoor", "https://github.com/hausec/PowerZure", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_azure_automation_webhook_created.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_azure_automation_webhook_created.json index e77d1f91747e7..2b8110b8e7d54 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_azure_automation_webhook_created.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_azure_automation_webhook_created.json @@ -12,7 +12,7 @@ "license": "Elastic License v2", "name": "Azure Automation Webhook Created", "note": "## Config\n\nThe Azure Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:azure.activitylogs and azure.activitylogs.operation_name: ( \"MICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/WEBHOOKS/ACTION\" or \"MICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/WEBHOOKS/WRITE\" ) and event.outcome:(Success or success)", + "query": "event.dataset:azure.activitylogs and\n azure.activitylogs.operation_name:\n (\n \"MICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/WEBHOOKS/ACTION\" or\n \"MICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/WEBHOOKS/WRITE\"\n ) and\n event.outcome:(Success or success)\n", "references": [ "https://powerzure.readthedocs.io/en/latest/Functions/operational.html#create-backdoor", "https://github.com/hausec/PowerZure", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_azure_conditional_access_policy_modified.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_azure_conditional_access_policy_modified.json index 00826c3d6162e..700ae78685257 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_azure_conditional_access_policy_modified.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_azure_conditional_access_policy_modified.json @@ -12,7 +12,7 @@ "license": "Elastic License v2", "name": "Azure Conditional Access Policy Modified", "note": "## Config\n\nThe Azure Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:(azure.activitylogs or azure.auditlogs) and ( azure.activitylogs.operation_name:\"Update policy\" or azure.auditlogs.operation_name:\"Update policy\" ) and event.outcome:(Success or success)", + "query": "event.dataset:(azure.activitylogs or azure.auditlogs) and\n (\n azure.activitylogs.operation_name:\"Update policy\" or\n azure.auditlogs.operation_name:\"Update policy\"\n ) and\n event.outcome:(Success or success)\n", "references": [ "https://docs.microsoft.com/en-us/azure/active-directory/conditional-access/overview" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_azure_pim_user_added_global_admin.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_azure_pim_user_added_global_admin.json index 1d5332beed4f6..4ee9f8074985b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_azure_pim_user_added_global_admin.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_azure_pim_user_added_global_admin.json @@ -14,7 +14,7 @@ "license": "Elastic License v2", "name": "Azure Global Administrator Role Addition to PIM User", "note": "## Config\n\nThe Azure Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:azure.auditlogs and azure.auditlogs.properties.category:RoleManagement and azure.auditlogs.operation_name:(\"Add eligible member to role in PIM completed (permanent)\" or \"Add member to role in PIM completed (timebound)\") and azure.auditlogs.properties.target_resources.*.display_name:\"Global Administrator\" and event.outcome:(Success or success)", + "query": "event.dataset:azure.auditlogs and azure.auditlogs.properties.category:RoleManagement and\n azure.auditlogs.operation_name:(\"Add eligible member to role in PIM completed (permanent)\" or\n \"Add member to role in PIM completed (timebound)\") and\n azure.auditlogs.properties.target_resources.*.display_name:\"Global Administrator\" and\n event.outcome:(Success or success)\n", "references": [ "https://docs.microsoft.com/en-us/azure/active-directory/users-groups-roles/directory-assign-admin-roles" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_azure_privileged_identity_management_role_modified.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_azure_privileged_identity_management_role_modified.json index 0a75c0feaa516..214100ffced5c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_azure_privileged_identity_management_role_modified.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_azure_privileged_identity_management_role_modified.json @@ -12,7 +12,7 @@ "license": "Elastic License v2", "name": "Azure Privilege Identity Management Role Modified", "note": "## Config\n\nThe Azure Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:azure.auditlogs and azure.auditlogs.operation_name:\"Update role setting in PIM\" and event.outcome:(Success or success)", + "query": "event.dataset:azure.auditlogs and azure.auditlogs.operation_name:\"Update role setting in PIM\" and event.outcome:(Success or success)\n", "references": [ "https://docs.microsoft.com/en-us/azure/active-directory/privileged-identity-management/pim-resource-roles-assign-roles", "https://docs.microsoft.com/en-us/azure/active-directory/privileged-identity-management/pim-configure" diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_credential_access_authorization_plugin_creation.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_credential_access_authorization_plugin_creation.json index 3df0e48eb5ee3..d66066bca644e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_credential_access_authorization_plugin_creation.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_credential_access_authorization_plugin_creation.json @@ -11,7 +11,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Authorization Plugin Modification", - "query": "event.category:file and not event.type:deletion and file.path:(/Library/Security/SecurityAgentPlugins/* and not /Library/Security/SecurityAgentPlugins/TeamViewerAuthPlugin.bundle/Contents/*)", + "query": "event.category:file and not event.type:deletion and\n file.path:(/Library/Security/SecurityAgentPlugins/* and\n not /Library/Security/SecurityAgentPlugins/TeamViewerAuthPlugin.bundle/Contents/*)\n", "references": [ "https://developer.apple.com/documentation/security/authorization_plug-ins", "https://www.xorrior.com/persistent-credential-theft/" diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_credential_access_modify_auth_module_or_config.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_credential_access_modify_auth_module_or_config.json index e1d8c05438b81..31e52590e22e0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_credential_access_modify_auth_module_or_config.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_credential_access_modify_auth_module_or_config.json @@ -14,7 +14,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Modification of Standard Authentication Module or Configuration", - "query": "event.category:file and event.type:change and (file.name:pam_*.so or file.path:(/etc/pam.d/* or /private/etc/pam.d/*)) and process.executable: (* and not ( /bin/yum or \"/usr/sbin/pam-auth-update\" or /usr/libexec/packagekitd or /usr/bin/dpkg or /usr/bin/vim or /usr/libexec/xpcproxy or /usr/bin/bsdtar or /usr/local/bin/brew or /usr/bin/rsync or /usr/bin/yum or /var/lib/docker/*/bin/yum or /var/lib/docker/*/bin/dpkg or ./merged/var/lib/docker/*/bin/dpkg or \"/System/Library/PrivateFrameworks/PackageKit.framework/Versions/A/XPCServices/package_script_service.xpc/Contents/MacOS/package_script_service\" ) ) and not file.path: ( /tmp/snap.rootfs_*/pam_*.so or /tmp/newroot/lib/*/pam_*.so or /private/var/folders/*/T/com.apple.fileprovider.ArchiveService/TemporaryItems/*/lib/security/pam_*.so or /tmp/newroot/usr/lib64/security/pam_*.so )", + "query": "event.category:file and event.type:change and \n (file.name:pam_*.so or file.path:(/etc/pam.d/* or /private/etc/pam.d/*)) and \n process.executable:\n (* and \n not \n (\n /bin/yum or \n \"/usr/sbin/pam-auth-update\" or \n /usr/libexec/packagekitd or \n /usr/bin/dpkg or \n /usr/bin/vim or \n /usr/libexec/xpcproxy or \n /usr/bin/bsdtar or \n /usr/local/bin/brew or\n /usr/bin/rsync or\n /usr/bin/yum or\n /var/lib/docker/*/bin/yum or\n /var/lib/docker/*/bin/dpkg or\n ./merged/var/lib/docker/*/bin/dpkg or\n \"/System/Library/PrivateFrameworks/PackageKit.framework/Versions/A/XPCServices/package_script_service.xpc/Contents/MacOS/package_script_service\"\n )\n ) and\n not file.path:\n (\n /tmp/snap.rootfs_*/pam_*.so or\n /tmp/newroot/lib/*/pam_*.so or\n /private/var/folders/*/T/com.apple.fileprovider.ArchiveService/TemporaryItems/*/lib/security/pam_*.so or\n /tmp/newroot/usr/lib64/security/pam_*.so\n )\n", "references": [ "https://github.com/zephrax/linux-pam-backdoor", "https://github.com/eurialo/pambd", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_credential_access_modify_ssh_binaries.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_credential_access_modify_ssh_binaries.json index e9f4aeea01129..46414c400ab8d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_credential_access_modify_ssh_binaries.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_credential_access_modify_ssh_binaries.json @@ -14,7 +14,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Modification of OpenSSH Binaries", - "query": "event.category:file and event.type:change and process.name:* and (file.path:(/usr/sbin/sshd or /usr/bin/ssh or /usr/bin/sftp or /usr/bin/scp) or file.name:libkeyutils.so) and not process.executable:/usr/bin/dpkg", + "query": "event.category:file and event.type:change and \n process.name:* and\n (file.path:(/usr/sbin/sshd or /usr/bin/ssh or /usr/bin/sftp or /usr/bin/scp) or file.name:libkeyutils.so) and\n not process.executable:/usr/bin/dpkg\n", "references": [ "https://blog.angelalonso.es/2016/09/anatomy-of-real-linux-intrusion-part-ii.html" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_defense_evasion_hidden_launch_agent_deamon_logonitem_process.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_defense_evasion_hidden_launch_agent_deamon_logonitem_process.json index 9778691f1d2f3..7ff8f12e60011 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_defense_evasion_hidden_launch_agent_deamon_logonitem_process.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_defense_evasion_hidden_launch_agent_deamon_logonitem_process.json @@ -11,7 +11,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Suspicious Hidden Child Process of Launchd", - "query": "event.category:process and event.type:(start or process_started) and process.name:.* and process.parent.executable:/sbin/launchd", + "query": "event.category:process and event.type:(start or process_started) and\n process.name:.* and process.parent.executable:/sbin/launchd\n", "references": [ "https://objective-see.com/blog/blog_0x61.html", "https://www.intezer.com/blog/research/operation-electrorat-attacker-creates-fake-companies-to-drain-your-crypto-wallets/", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_directory_services_plugins_modification.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_directory_services_plugins_modification.json index 18595dfe18048..0fce2b7647df4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_directory_services_plugins_modification.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_directory_services_plugins_modification.json @@ -11,7 +11,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Persistence via DirectoryService Plugin Modification", - "query": "event.category:file and not event.type:deletion and file.path:/Library/DirectoryServices/PlugIns/*.dsplug", + "query": "event.category:file and not event.type:deletion and\n file.path:/Library/DirectoryServices/PlugIns/*.dsplug\n", "references": [ "https://blog.chichou.me/2019/11/21/two-macos-persistence-tricks-abusing-plugins/" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_docker_shortcuts_plist_modification.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_docker_shortcuts_plist_modification.json index 6ccc3dbe509f2..941fe5cbf5484 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_docker_shortcuts_plist_modification.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_docker_shortcuts_plist_modification.json @@ -11,7 +11,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Persistence via Docker Shortcut Modification", - "query": "event.category : file and event.action : modification and file.path : /Users/*/Library/Preferences/com.apple.dock.plist and not process.name : (xpcproxy or cfprefsd or plutil or jamf or PlistBuddy or InstallerRemotePluginService)", + "query": "event.category : file and event.action : modification and \n file.path : /Users/*/Library/Preferences/com.apple.dock.plist and \n not process.name : (xpcproxy or cfprefsd or plutil or jamf or PlistBuddy or InstallerRemotePluginService)\n", "references": [ "https://github.com/specterops/presentations/raw/master/Leo Pitt/Hey_Im_Still_in_Here_Modern_macOS_Persistence_SO-CON2020.pdf" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_ec2_network_acl_creation.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_ec2_network_acl_creation.json index 79ff9080c8f23..a545ae77d03ea 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_ec2_network_acl_creation.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_ec2_network_acl_creation.json @@ -16,7 +16,7 @@ "license": "Elastic License v2", "name": "AWS EC2 Network Access Control List Creation", "note": "## Config\n\nThe AWS Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:aws.cloudtrail and event.provider:ec2.amazonaws.com and event.action:(CreateNetworkAcl or CreateNetworkAclEntry) and event.outcome:success", + "query": "event.dataset:aws.cloudtrail and event.provider:ec2.amazonaws.com and event.action:(CreateNetworkAcl or CreateNetworkAclEntry) and event.outcome:success\n", "references": [ "https://awscli.amazonaws.com/v2/documentation/api/latest/reference/ec2/create-network-acl.html", "https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_CreateNetworkAcl.html", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_enable_root_account.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_enable_root_account.json index 5d608649abe21..b81496a79f960 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_enable_root_account.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_enable_root_account.json @@ -11,7 +11,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Attempt to Enable the Root Account", - "query": "event.category:process and event.type:(start or process_started) and process.name:dsenableroot and not process.args:\"-d\"", + "query": "event.category:process and event.type:(start or process_started) and\n process.name:dsenableroot and not process.args:\"-d\"\n", "references": [ "https://ss64.com/osx/dsenableroot.html" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_gcp_iam_service_account_key_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_gcp_iam_service_account_key_deletion.json index 34efe6e0b3d0e..07372389f48c3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_gcp_iam_service_account_key_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_gcp_iam_service_account_key_deletion.json @@ -14,7 +14,7 @@ "license": "Elastic License v2", "name": "GCP IAM Service Account Key Deletion", "note": "## Config\n\nThe GCP Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:(googlecloud.audit or gcp.audit) and event.action:google.iam.admin.v*.DeleteServiceAccountKey and event.outcome:success", + "query": "event.dataset:(googlecloud.audit or gcp.audit) and event.action:google.iam.admin.v*.DeleteServiceAccountKey and event.outcome:success\n", "references": [ "https://cloud.google.com/iam/docs/service-accounts", "https://cloud.google.com/iam/docs/creating-managing-service-account-keys" diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_gcp_key_created_for_service_account.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_gcp_key_created_for_service_account.json index 2c84a68b7603c..88249ad30adb9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_gcp_key_created_for_service_account.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_gcp_key_created_for_service_account.json @@ -14,7 +14,7 @@ "license": "Elastic License v2", "name": "GCP Service Account Key Creation", "note": "## Config\n\nThe GCP Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:(googlecloud.audit or gcp.audit) and event.action:google.iam.admin.v*.CreateServiceAccountKey and event.outcome:success", + "query": "event.dataset:(googlecloud.audit or gcp.audit) and event.action:google.iam.admin.v*.CreateServiceAccountKey and event.outcome:success\n", "references": [ "https://cloud.google.com/iam/docs/service-accounts", "https://cloud.google.com/iam/docs/creating-managing-service-account-keys" diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_gcp_service_account_created.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_gcp_service_account_created.json index 54d4c01c66ea4..d37cc80d562cd 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_gcp_service_account_created.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_gcp_service_account_created.json @@ -14,7 +14,7 @@ "license": "Elastic License v2", "name": "GCP Service Account Creation", "note": "## Config\n\nThe GCP Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:(googlecloud.audit or gcp.audit) and event.action:google.iam.admin.v*.CreateServiceAccount and event.outcome:success", + "query": "event.dataset:(googlecloud.audit or gcp.audit) and event.action:google.iam.admin.v*.CreateServiceAccount and event.outcome:success\n", "references": [ "https://cloud.google.com/iam/docs/service-accounts" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_google_workspace_admin_role_assigned_to_user.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_google_workspace_admin_role_assigned_to_user.json index 681d9f4f7d4c8..1ad3e0afeed52 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_google_workspace_admin_role_assigned_to_user.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_google_workspace_admin_role_assigned_to_user.json @@ -16,7 +16,7 @@ "license": "Elastic License v2", "name": "Google Workspace Admin Role Assigned to a User", "note": "## Config\n\nThe Google Workspace Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.\n\n### Important Information Regarding Google Workspace Event Lag Times\n- As per Google's documentation, Google Workspace administrators may observe lag times ranging from minutes up to 3 days between the time of an event's occurrence and the event being visible in the Google Workspace admin/audit logs.\n- This rule is configured to run every 10 minutes with a lookback time of 130 minutes.\n- To reduce the risk of false negatives, consider reducing the interval that the Google Workspace (formerly G Suite) Filebeat module polls Google's reporting API for new events.\n- By default, `var.interval` is set to 2 hours (2h). Consider changing this interval to a lower value, such as 10 minutes (10m).\n- See the following references for further information.\n - https://support.google.com/a/answer/7061566\n - https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-module-gsuite.html", - "query": "event.dataset:(gsuite.admin or google_workspace.admin) and event.provider:admin and event.category:iam and event.action:ASSIGN_ROLE", + "query": "event.dataset:(gsuite.admin or google_workspace.admin) and event.provider:admin and event.category:iam and event.action:ASSIGN_ROLE\n", "references": [ "https://support.google.com/a/answer/172176?hl=en" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_google_workspace_api_access_granted_via_domain_wide_delegation_of_authority.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_google_workspace_api_access_granted_via_domain_wide_delegation_of_authority.json index 819c047a2c64f..19dd54c6ccb35 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_google_workspace_api_access_granted_via_domain_wide_delegation_of_authority.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_google_workspace_api_access_granted_via_domain_wide_delegation_of_authority.json @@ -16,7 +16,7 @@ "license": "Elastic License v2", "name": "Google Workspace API Access Granted via Domain-Wide Delegation of Authority", "note": "## Config\n\nThe Google Workspace Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.\n\n### Important Information Regarding Google Workspace Event Lag Times\n- As per Google's documentation, Google Workspace administrators may observe lag times ranging from minutes up to 3 days between the time of an event's occurrence and the event being visible in the Google Workspace admin/audit logs.\n- This rule is configured to run every 10 minutes with a lookback time of 130 minutes.\n- To reduce the risk of false negatives, consider reducing the interval that the Google Workspace (formerly G Suite) Filebeat module polls Google's reporting API for new events.\n- By default, `var.interval` is set to 2 hours (2h). Consider changing this interval to a lower value, such as 10 minutes (10m).\n- See the following references for further information.\n - https://support.google.com/a/answer/7061566\n - https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-module-gsuite.html", - "query": "event.dataset:(gsuite.admin or google_workspace.admin) and event.provider:admin and event.category:iam and event.action:AUTHORIZE_API_CLIENT_ACCESS", + "query": "event.dataset:(gsuite.admin or google_workspace.admin) and event.provider:admin and event.category:iam and event.action:AUTHORIZE_API_CLIENT_ACCESS\n", "references": [ "https://developers.google.com/admin-sdk/directory/v1/guides/delegation" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_google_workspace_custom_admin_role_created.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_google_workspace_custom_admin_role_created.json index 2a68fc2604f8b..ae03288800adc 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_google_workspace_custom_admin_role_created.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_google_workspace_custom_admin_role_created.json @@ -16,7 +16,7 @@ "license": "Elastic License v2", "name": "Google Workspace Custom Admin Role Created", "note": "## Config\n\nThe Google Workspace Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.\n\n### Important Information Regarding Google Workspace Event Lag Times\n- As per Google's documentation, Google Workspace administrators may observe lag times ranging from minutes up to 3 days between the time of an event's occurrence and the event being visible in the Google Workspace admin/audit logs.\n- This rule is configured to run every 10 minutes with a lookback time of 130 minutes.\n- To reduce the risk of false negatives, consider reducing the interval that the Google Workspace (formerly G Suite) Filebeat module polls Google's reporting API for new events.\n- By default, `var.interval` is set to 2 hours (2h). Consider changing this interval to a lower value, such as 10 minutes (10m).\n- See the following references for further information.\n - https://support.google.com/a/answer/7061566\n - https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-module-gsuite.html", - "query": "event.dataset:(gsuite.admin or google_workspace.admin) and event.provider:admin and event.category:iam and event.action:CREATE_ROLE", + "query": "event.dataset:(gsuite.admin or google_workspace.admin) and event.provider:admin and event.category:iam and event.action:CREATE_ROLE\n", "references": [ "https://support.google.com/a/answer/2406043?hl=en" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_google_workspace_role_modified.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_google_workspace_role_modified.json index daf5b9b97a522..75bd229efa31c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_google_workspace_role_modified.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_google_workspace_role_modified.json @@ -16,7 +16,7 @@ "license": "Elastic License v2", "name": "Google Workspace Role Modified", "note": "## Config\n\nThe Google Workspace Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.\n\n### Important Information Regarding Google Workspace Event Lag Times\n- As per Google's documentation, Google Workspace administrators may observe lag times ranging from minutes up to 3 days between the time of an event's occurrence and the event being visible in the Google Workspace admin/audit logs.\n- This rule is configured to run every 10 minutes with a lookback time of 130 minutes.\n- To reduce the risk of false negatives, consider reducing the interval that the Google Workspace (formerly G Suite) Filebeat module polls Google's reporting API for new events.\n- By default, `var.interval` is set to 2 hours (2h). Consider changing this interval to a lower value, such as 10 minutes (10m).\n- See the following references for further information.\n - https://support.google.com/a/answer/7061566\n - https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-module-gsuite.html", - "query": "event.dataset:(gsuite.admin or google_workspace.admin) and event.provider:admin and event.category:iam and event.action:(ADD_PRIVILEGE or UPDATE_ROLE)", + "query": "event.dataset:(gsuite.admin or google_workspace.admin) and event.provider:admin and event.category:iam and event.action:(ADD_PRIVILEGE or UPDATE_ROLE)\n", "references": [ "https://support.google.com/a/answer/2406043?hl=en" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_iam_group_creation.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_iam_group_creation.json index 0930201f0422e..084ccb4da74f2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_iam_group_creation.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_iam_group_creation.json @@ -16,7 +16,7 @@ "license": "Elastic License v2", "name": "AWS IAM Group Creation", "note": "## Config\n\nThe AWS Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:aws.cloudtrail and event.provider:iam.amazonaws.com and event.action:CreateGroup and event.outcome:success", + "query": "event.dataset:aws.cloudtrail and event.provider:iam.amazonaws.com and event.action:CreateGroup and event.outcome:success\n", "references": [ "https://awscli.amazonaws.com/v2/documentation/api/latest/reference/iam/create-group.html", "https://docs.aws.amazon.com/IAM/latest/APIReference/API_CreateGroup.html" diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_loginwindow_plist_modification.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_loginwindow_plist_modification.json index d6ffaa532a530..0ec8607f4d71d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_loginwindow_plist_modification.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_loginwindow_plist_modification.json @@ -12,7 +12,7 @@ "license": "Elastic License v2", "name": "Potential Persistence via Login Hook", "note": "## Triage and analysis\n\nStarting in Mac OS X 10.7 (Lion), users can specify certain applications to be re-opened when a user reboots their machine. This can be abused to establish or maintain persistence on a compromised system.", - "query": "event.category:\"file\" and not event.type:\"deletion\" and file.name:\"com.apple.loginwindow.plist\" and process.name:(* and not (systemmigrationd or DesktopServicesHelper or diskmanagementd or rsync or launchd or cfprefsd or xpcproxy or ManagedClient or MCXCompositor))", + "query": "event.category:\"file\" and not event.type:\"deletion\" and\n file.name:\"com.apple.loginwindow.plist\" and\n process.name:(* and not (systemmigrationd or DesktopServicesHelper or diskmanagementd or rsync or launchd or cfprefsd or xpcproxy or ManagedClient or MCXCompositor))\n", "references": [ "https://github.com/D00MFist/PersistentJXA/blob/master/LoginScript.js" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_mfa_disabled_for_azure_user.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_mfa_disabled_for_azure_user.json index e13975684a722..2514b5790d023 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_mfa_disabled_for_azure_user.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_mfa_disabled_for_azure_user.json @@ -12,7 +12,7 @@ "license": "Elastic License v2", "name": "Multi-Factor Authentication Disabled for an Azure User", "note": "## Config\n\nThe Azure Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:azure.auditlogs and azure.auditlogs.operation_name:\"Disable Strong Authentication\" and event.outcome:(Success or success)", + "query": "event.dataset:azure.auditlogs and azure.auditlogs.operation_name:\"Disable Strong Authentication\" and event.outcome:(Success or success)\n", "risk_score": 47, "rule_id": "dafa3235-76dc-40e2-9f71-1773b96d24cf", "severity": "medium", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_microsoft_365_exchange_management_role_assignment.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_microsoft_365_exchange_management_role_assignment.json index a0e51fa3b8eed..b145642a01968 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_microsoft_365_exchange_management_role_assignment.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_microsoft_365_exchange_management_role_assignment.json @@ -15,7 +15,7 @@ "license": "Elastic License v2", "name": "Microsoft 365 Exchange Management Group Role Assignment", "note": "## Config\n\nThe Microsoft 365 Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:o365.audit and event.provider:Exchange and event.category:web and event.action:\"New-ManagementRoleAssignment\" and event.outcome:success", + "query": "event.dataset:o365.audit and event.provider:Exchange and event.category:web and event.action:\"New-ManagementRoleAssignment\" and event.outcome:success\n", "references": [ "https://docs.microsoft.com/en-us/powershell/module/exchange/new-managementroleassignment?view=exchange-ps", "https://docs.microsoft.com/en-us/microsoft-365/admin/add-users/about-admin-roles?view=o365-worldwide" diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_microsoft_365_teams_external_access_enabled.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_microsoft_365_teams_external_access_enabled.json index 570ac0323dfd1..f3ad4d22cf14c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_microsoft_365_teams_external_access_enabled.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_microsoft_365_teams_external_access_enabled.json @@ -15,7 +15,7 @@ "license": "Elastic License v2", "name": "Microsoft 365 Teams External Access Enabled", "note": "## Config\n\nThe Microsoft 365 Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:o365.audit and event.provider:(SkypeForBusiness or MicrosoftTeams) and event.category:web and event.action:\"Set-CsTenantFederationConfiguration\" and o365.audit.Parameters.AllowFederatedUsers:True and event.outcome:success", + "query": "event.dataset:o365.audit and event.provider:(SkypeForBusiness or MicrosoftTeams) and\nevent.category:web and event.action:\"Set-CsTenantFederationConfiguration\" and\no365.audit.Parameters.AllowFederatedUsers:True and event.outcome:success\n", "references": [ "https://docs.microsoft.com/en-us/microsoftteams/manage-external-access" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_microsoft_365_teams_guest_access_enabled.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_microsoft_365_teams_guest_access_enabled.json index 492c3dcd5bc9d..93f72b401d51e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_microsoft_365_teams_guest_access_enabled.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_microsoft_365_teams_guest_access_enabled.json @@ -15,7 +15,7 @@ "license": "Elastic License v2", "name": "Microsoft 365 Teams Guest Access Enabled", "note": "## Config\n\nThe Microsoft 365 Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:o365.audit and event.provider:(SkypeForBusiness or MicrosoftTeams) and event.category:web and event.action:\"Set-CsTeamsClientConfiguration\" and o365.audit.Parameters.AllowGuestUser:True and event.outcome:success", + "query": "event.dataset:o365.audit and event.provider:(SkypeForBusiness or MicrosoftTeams) and\nevent.category:web and event.action:\"Set-CsTeamsClientConfiguration\" and\no365.audit.Parameters.AllowGuestUser:True and event.outcome:success\n", "references": [ "https://docs.microsoft.com/en-us/powershell/module/skype/get-csteamsclientconfiguration?view=skype-ps" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_periodic_tasks_file_mdofiy.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_periodic_tasks_file_mdofiy.json index f48ea64022ecb..2df59206645b4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_periodic_tasks_file_mdofiy.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_periodic_tasks_file_mdofiy.json @@ -11,7 +11,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Potential Persistence via Periodic Tasks", - "query": "event.category:\"file\" and not event.type:\"deletion\" and file.path:(/private/etc/periodic/* or /private/etc/defaults/periodic.conf or /private/etc/periodic.conf)", + "query": "event.category:\"file\" and not event.type:\"deletion\" and\n file.path:(/private/etc/periodic/* or /private/etc/defaults/periodic.conf or /private/etc/periodic.conf)\n", "references": [ "https://opensource.apple.com/source/crontabs/crontabs-13/private/etc/defaults/periodic.conf.auto.html", "https://www.oreilly.com/library/view/mac-os-x/0596003706/re328.html", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_rds_cluster_creation.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_rds_cluster_creation.json index 3a13c02a368ad..4f28f277d21e1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_rds_cluster_creation.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_rds_cluster_creation.json @@ -16,7 +16,7 @@ "license": "Elastic License v2", "name": "AWS RDS Cluster Creation", "note": "## Config\n\nThe AWS Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:aws.cloudtrail and event.provider:rds.amazonaws.com and event.action:(CreateDBCluster or CreateGlobalCluster) and event.outcome:success", + "query": "event.dataset:aws.cloudtrail and event.provider:rds.amazonaws.com and event.action:(CreateDBCluster or CreateGlobalCluster) and event.outcome:success\n", "references": [ "https://awscli.amazonaws.com/v2/documentation/api/latest/reference/rds/create-db-cluster.html", "https://docs.aws.amazon.com/AmazonRDS/latest/APIReference/API_CreateDBCluster.html", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_shell_activity_by_web_server.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_shell_activity_by_web_server.json index 545787d7ec999..eec7157c74148 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_shell_activity_by_web_server.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_shell_activity_by_web_server.json @@ -14,7 +14,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Potential Shell via Web Server", - "query": "event.category:process and event.type:(start or process_started) and process.name:(bash or dash) and user.name:(apache or nginx or www or \"www-data\")", + "query": "event.category:process and event.type:(start or process_started) and process.name:(bash or dash) and\n user.name:(apache or nginx or www or \"www-data\")\n", "references": [ "https://pentestlab.blog/tag/web-shell/" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_shell_profile_modification.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_shell_profile_modification.json index fa56d07b190cd..41ca64fb6c162 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_shell_profile_modification.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_shell_profile_modification.json @@ -14,7 +14,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Bash Shell Profile Modification", - "query": "event.category:file and event.type:change and process.name:(* and not (sudo or vim or zsh or env or nano or bash or Terminal or xpcproxy or login or cat or cp or launchctl or java)) and not process.executable:(/Applications/* or /private/var/folders/* or /usr/local/*) and file.path:(/private/etc/rc.local or /etc/rc.local or /home/*/.profile or /home/*/.profile1 or /home/*/.bash_profile or /home/*/.bash_profile1 or /home/*/.bashrc or /Users/*/.bash_profile or /Users/*/.zshenv)", + "query": "event.category:file and event.type:change and\n process.name:(* and not (sudo or\n vim or\n zsh or\n env or\n nano or\n bash or\n Terminal or\n xpcproxy or\n login or\n cat or\n cp or\n launchctl or\n java)) and\n not process.executable:(/Applications/* or /private/var/folders/* or /usr/local/*) and\n file.path:(/private/etc/rc.local or\n /etc/rc.local or\n /home/*/.profile or\n /home/*/.profile1 or\n /home/*/.bash_profile or\n /home/*/.bash_profile1 or\n /home/*/.bashrc or\n /Users/*/.bash_profile or\n /Users/*/.zshenv)\n", "references": [ "https://www.anomali.com/blog/pulling-linux-rabbit-rabbot-malware-out-of-a-hat" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_ssh_authorized_keys_modification.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_ssh_authorized_keys_modification.json index 46358ec7f47fe..23d16f121921b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_ssh_authorized_keys_modification.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_ssh_authorized_keys_modification.json @@ -11,7 +11,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "SSH Authorized Keys File Modification", - "query": "event.category:file and event.type:(change or creation) and file.name:(\"authorized_keys\" or \"authorized_keys2\") and not process.executable: (/Library/Developer/CommandLineTools/usr/bin/git or /usr/local/Cellar/maven/*/libexec/bin/mvn or /Library/Java/JavaVirtualMachines/jdk*.jdk/Contents/Home/bin/java or /usr/bin/vim or /usr/local/Cellar/coreutils/*/bin/gcat or /usr/bin/bsdtar or /usr/bin/nautilus or /usr/bin/scp or /usr/bin/touch or /var/lib/docker/*)", + "query": "event.category:file and event.type:(change or creation) and \n file.name:(\"authorized_keys\" or \"authorized_keys2\") and \n not process.executable:\n (/Library/Developer/CommandLineTools/usr/bin/git or \n /usr/local/Cellar/maven/*/libexec/bin/mvn or \n /Library/Java/JavaVirtualMachines/jdk*.jdk/Contents/Home/bin/java or \n /usr/bin/vim or \n /usr/local/Cellar/coreutils/*/bin/gcat or \n /usr/bin/bsdtar or\n /usr/bin/nautilus or \n /usr/bin/scp or\n /usr/bin/touch or \n /var/lib/docker/*)\n", "risk_score": 47, "rule_id": "2215b8bd-1759-4ffa-8ab8-55c8e6b32e7f", "severity": "medium", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_suspicious_calendar_modification.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_suspicious_calendar_modification.json index 1f0186c39ad26..97a3de8f4060d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_suspicious_calendar_modification.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_suspicious_calendar_modification.json @@ -14,7 +14,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Suspicious Calendar File Modification", - "query": "event.category:file and event.action:modification and file.path:/Users/*/Library/Calendars/*.calendar/Events/*.ics and process.executable: (* and not ( /System/Library/* or /System/Applications/Calendar.app/Contents/MacOS/* or /usr/libexec/xpcproxy or /sbin/launchd or /Applications/* ) )", + "query": "event.category:file and event.action:modification and\n file.path:/Users/*/Library/Calendars/*.calendar/Events/*.ics and\n process.executable:\n (* and not \n (\n /System/Library/* or \n /System/Applications/Calendar.app/Contents/MacOS/* or \n /usr/libexec/xpcproxy or \n /sbin/launchd or \n /Applications/*\n )\n )\n", "references": [ "https://labs.f-secure.com/blog/operationalising-calendar-alerts-persistence-on-macos", "https://github.com/FSecureLABS/CalendarPersist", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_suspicious_com_hijack_registry.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_suspicious_com_hijack_registry.json index f20b00d057f1a..b0f573730b73f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_suspicious_com_hijack_registry.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_suspicious_com_hijack_registry.json @@ -12,7 +12,7 @@ "language": "eql", "license": "Elastic License v2", "name": "Component Object Model Hijacking", - "query": "registry where\n /* uncomment once length is stable length(bytes_written_string) > 0 and */\n (registry.path : \"HK*}\\\\InprocServer32\\\\\" and registry.data.strings: (\"scrobj.dll\", \"C:\\\\*\\\\scrobj.dll\") and\n not registry.path : \"*\\\\{06290BD*-48AA-11D2-8432-006008C3FBFC}\\\\*\") \n or\n /* in general COM Registry changes on Users Hive is less noisy and worth alerting */\n (registry.path : (\"HKEY_USERS\\\\*Classes\\\\*\\\\InprocXServer32\\\\\", \n \"HKEY_USERS\\\\*Classes\\\\*\\\\LocalServer32\\\\\", \n \"HKEY_USERS\\\\*Classes\\\\*\\\\DelegateExecute\\\\\", \n \"HKEY_USERS\\\\*Classes\\\\*\\\\TreatAs\\\\\", \n \"HKEY_USERS\\\\*Classes\\\\CLSID\\\\*\\\\ScriptletURL\\\\\") and\n /* not necessary but good for filtering privileged installations */\n user.domain != \"NT AUTHORITY\")\n", + "query": "registry where\n /* uncomment once length is stable length(bytes_written_string) > 0 and */\n (registry.path : \"HK*}\\\\InprocServer32\\\\\" and registry.data.strings: (\"scrobj.dll\", \"C:\\\\*\\\\scrobj.dll\") and\n not registry.path : \"*\\\\{06290BD*-48AA-11D2-8432-006008C3FBFC}\\\\*\") \n or\n /* in general COM Registry changes on Users Hive is less noisy and worth alerting */\n (registry.path : (\"HKEY_USERS\\\\*Classes\\\\*\\\\InprocServer32\\\\\",\n \"HKEY_USERS\\\\*Classes\\\\*\\\\LocalServer32\\\\\", \n \"HKEY_USERS\\\\*Classes\\\\*\\\\DelegateExecute\\\\\", \n \"HKEY_USERS\\\\*Classes\\\\*\\\\TreatAs\\\\\", \n \"HKEY_USERS\\\\*Classes\\\\CLSID\\\\*\\\\ScriptletURL\\\\\") and\n /* not necessary but good for filtering privileged installations */\n user.domain != \"NT AUTHORITY\")\n", "references": [ "https://bohops.com/2018/08/18/abusing-the-com-registry-structure-part-2-loading-techniques-for-evasion-and-persistence/" ], @@ -52,5 +52,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_user_added_as_owner_for_azure_application.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_user_added_as_owner_for_azure_application.json index ee2fdb5b75eac..1d0990e951925 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_user_added_as_owner_for_azure_application.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_user_added_as_owner_for_azure_application.json @@ -12,7 +12,7 @@ "license": "Elastic License v2", "name": "User Added as Owner for Azure Application", "note": "## Config\n\nThe Azure Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:azure.auditlogs and azure.auditlogs.operation_name:\"Add owner to application\" and event.outcome:(Success or success)", + "query": "event.dataset:azure.auditlogs and azure.auditlogs.operation_name:\"Add owner to application\" and event.outcome:(Success or success)\n", "risk_score": 21, "rule_id": "774f5e28-7b75-4a58-b94e-41bf060fdd86", "severity": "low", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_user_added_as_owner_for_azure_service_principal.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_user_added_as_owner_for_azure_service_principal.json index aacc17bb6be53..b68b4826bc4cf 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_user_added_as_owner_for_azure_service_principal.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_user_added_as_owner_for_azure_service_principal.json @@ -12,7 +12,7 @@ "license": "Elastic License v2", "name": "User Added as Owner for Azure Service Principal", "note": "## Config\n\nThe Azure Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:azure.auditlogs and azure.auditlogs.operation_name:\"Add owner to service principal\" and event.outcome:(Success or success)", + "query": "event.dataset:azure.auditlogs and azure.auditlogs.operation_name:\"Add owner to service principal\" and event.outcome:(Success or success)\n", "references": [ "https://docs.microsoft.com/en-us/azure/active-directory/develop/app-objects-and-service-principals" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_via_atom_init_file_modification.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_via_atom_init_file_modification.json index 30c2d3a9b4ab7..0977d8310cf6b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_via_atom_init_file_modification.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_via_atom_init_file_modification.json @@ -11,7 +11,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Potential Persistence via Atom Init Script Modification", - "query": "event.category:\"file\" and not event.type:\"deletion\" and file.path:/Users/*/.atom/init.coffee and not process.name:(Atom or xpcproxy) and not user.name:root", + "query": "event.category:\"file\" and not event.type:\"deletion\" and\n file.path:/Users/*/.atom/init.coffee and not process.name:(Atom or xpcproxy) and not user.name:root\n", "references": [ "https://github.com/D00MFist/PersistentJXA/blob/master/AtomPersist.js", "https://flight-manual.atom.io/hacking-atom/sections/the-init-file/" diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_echo_nopasswd_sudoers.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_echo_nopasswd_sudoers.json index e6bd7784006ee..6023854cd6641 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_echo_nopasswd_sudoers.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_echo_nopasswd_sudoers.json @@ -11,7 +11,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Potential Privilege Escalation via Sudoers File Modification", - "query": "event.category:process and event.type:start and process.args:(echo and *NOPASSWD*ALL*)", + "query": "event.category:process and event.type:start and process.args:(echo and *NOPASSWD*ALL*)\n", "risk_score": 73, "rule_id": "76152ca1-71d0-4003-9e37-0983e12832da", "severity": "high", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_explicit_creds_via_scripting.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_explicit_creds_via_scripting.json index c8ec829d3ae8a..32c47afd93019 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_explicit_creds_via_scripting.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_explicit_creds_via_scripting.json @@ -11,7 +11,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Execution with Explicit Credentials via Scripting", - "query": "event.category:process and event.type:(start or process_started) and process.name:\"security_authtrampoline\" and process.parent.name:(osascript or com.apple.automator.runner or sh or bash or dash or zsh or python* or perl* or php* or ruby or pwsh)", + "query": "event.category:process and event.type:(start or process_started) and\n process.name:\"security_authtrampoline\" and\n process.parent.name:(osascript or com.apple.automator.runner or sh or bash or dash or zsh or python* or perl* or php* or ruby or pwsh)\n", "references": [ "https://objectivebythesea.com/v2/talks/OBTS_v2_Thomas.pdf", "https://www.manpagez.com/man/8/security_authtrampoline/" diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_exploit_adobe_acrobat_updater.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_exploit_adobe_acrobat_updater.json index 0fb4285ea2a98..0c30cbb23b56b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_exploit_adobe_acrobat_updater.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_exploit_adobe_acrobat_updater.json @@ -14,7 +14,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Suspicious Child Process of Adobe Acrobat Reader Update Service", - "query": "event.category:process and event.type:(start or process_started) and process.parent.name:com.adobe.ARMDC.SMJobBlessHelper and user.name:root and not process.executable: (/Library/PrivilegedHelperTools/com.adobe.ARMDC.SMJobBlessHelper or /usr/bin/codesign or /private/var/folders/zz/*/T/download/ARMDCHammer or /usr/sbin/pkgutil or /usr/bin/shasum or /usr/bin/perl* or /usr/sbin/spctl or /usr/sbin/installer)", + "query": "event.category:process and event.type:(start or process_started) and\n process.parent.name:com.adobe.ARMDC.SMJobBlessHelper and\n user.name:root and\n not process.executable: (/Library/PrivilegedHelperTools/com.adobe.ARMDC.SMJobBlessHelper or\n /usr/bin/codesign or\n /private/var/folders/zz/*/T/download/ARMDCHammer or\n /usr/sbin/pkgutil or\n /usr/bin/shasum or\n /usr/bin/perl* or\n /usr/sbin/spctl or\n /usr/sbin/installer)\n", "references": [ "https://rekken.github.io/2020/05/14/Security-Flaws-in-Adobe-Acrobat-Reader-Allow-Malicious-Program-to-Gain-Root-on-macOS-Silently/" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_ld_preload_shared_object_modif.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_ld_preload_shared_object_modif.json index 7000e119f8881..4b1b367a5ea35 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_ld_preload_shared_object_modif.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_ld_preload_shared_object_modif.json @@ -11,7 +11,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Modification of Dynamic Linker Preload Shared Object", - "query": "event.category:file and not event.type:deletion and file.path:/etc/ld.so.preload", + "query": "event.category:file and not event.type:deletion and file.path:/etc/ld.so.preload\n", "references": [ "https://www.anomali.com/blog/rocke-evolves-its-arsenal-with-a-new-malware-family-written-in-golang" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_local_user_added_to_admin.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_local_user_added_to_admin.json index 58c151d6de0c4..72c3bfc9a520c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_local_user_added_to_admin.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_local_user_added_to_admin.json @@ -11,7 +11,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Potential Admin Group Account Addition", - "query": "event.category:process and event.type:(start or process_started) and process.name:(dscl or dseditgroup) and process.args:((\"/Groups/admin\" or admin) and (\"-a\" or \"-append\"))", + "query": "event.category:process and event.type:(start or process_started) and\n process.name:(dscl or dseditgroup) and process.args:((\"/Groups/admin\" or admin) and (\"-a\" or \"-append\"))\n", "references": [ "https://managingosx.wordpress.com/2010/01/14/add-a-user-to-the-admin-group-via-command-line-3-0/" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_persistence_phantom_dll.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_persistence_phantom_dll.json index a85558df6f388..e454a387b3883 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_persistence_phantom_dll.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_persistence_phantom_dll.json @@ -12,7 +12,7 @@ "language": "eql", "license": "Elastic License v2", "name": "Suspicious DLL Loaded for Persistence or Privilege Escalation", - "query": "library where dll.name :\n (\n \"wlbsctrl.dll\",\n \"wbemcomn.dll\",\n \"WptsExtensions.dll\",\n \"Tsmsisrv.dll\",\n \"TSVIPSrv.dll\",\n \"Msfte.dll\",\n \"wow64log.dll\",\n \"WindowsCoreDeviceInfo.dll\",\n \"Ualapi.dll\",\n \"wlanhlp.dll\",\n \"phoneinfo.dll\",\n \"EdgeGdi.dll\",\n \"cdpsgshims.dll\",\n \"windowsperformancerecordercontrol.dll\",\n \"diagtrack_win.dll\"\n ) and \nnot (dll.code_signature.subject_name : \"Microsoft Windows\" and dll.code_signature.status : \"trusted\")\n", + "query": "library where dll.name :\n (\n \"wlbsctrl.dll\",\n \"wbemcomn.dll\",\n \"WptsExtensions.dll\",\n \"Tsmsisrv.dll\",\n \"TSVIPSrv.dll\",\n \"Msfte.dll\",\n \"wow64log.dll\",\n \"WindowsCoreDeviceInfo.dll\",\n \"Ualapi.dll\",\n \"wlanhlp.dll\",\n \"phoneinfo.dll\",\n \"EdgeGdi.dll\",\n \"cdpsgshims.dll\",\n \"windowsperformancerecordercontrol.dll\",\n \"diagtrack_win.dll\"\n ) and \nnot (dll.code_signature.subject_name : (\"Microsoft Windows\", \"Microsoft Corporation\") and dll.code_signature.status : \"trusted\")\n", "references": [ "https://itm4n.github.io/windows-dll-hijacking-clarified/", "http://remoteawesomethoughts.blogspot.com/2019/05/windows-10-task-schedulerservice.html", @@ -80,5 +80,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 1 + "version": 2 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_root_crontab_filemod.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_root_crontab_filemod.json index f782e429dd29e..57f893168ba57 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_root_crontab_filemod.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_root_crontab_filemod.json @@ -11,7 +11,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Privilege Escalation via Root Crontab File Modification", - "query": "event.category:file and not event.type:deletion and file.path:/private/var/at/tabs/root and not process.executable:/usr/bin/crontab", + "query": "event.category:file and not event.type:deletion and\n file.path:/private/var/at/tabs/root and not process.executable:/usr/bin/crontab\n", "references": [ "https://phoenhex.re/2017-06-09/pwn2own-diskarbitrationd-privesc", "https://www.exploit-db.com/exploits/42146" diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_root_login_without_mfa.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_root_login_without_mfa.json index 5ab6b41582030..33b5cd400fd33 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_root_login_without_mfa.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_root_login_without_mfa.json @@ -16,7 +16,7 @@ "license": "Elastic License v2", "name": "AWS Root Login Without MFA", "note": "## Config\n\nThe AWS Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:aws.cloudtrail and event.provider:signin.amazonaws.com and event.action:ConsoleLogin and aws.cloudtrail.user_identity.type:Root and aws.cloudtrail.console_login.additional_eventdata.mfa_used:false and event.outcome:success", + "query": "event.dataset:aws.cloudtrail and event.provider:signin.amazonaws.com and event.action:ConsoleLogin and\n aws.cloudtrail.user_identity.type:Root and\n aws.cloudtrail.console_login.additional_eventdata.mfa_used:false and\n event.outcome:success\n", "references": [ "https://docs.aws.amazon.com/IAM/latest/UserGuide/id_root-user.html" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_setuid_setgid_bit_set_via_chmod.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_setuid_setgid_bit_set_via_chmod.json index 67633985221e3..29113d078ae5f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_setuid_setgid_bit_set_via_chmod.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_setuid_setgid_bit_set_via_chmod.json @@ -12,7 +12,7 @@ "license": "Elastic License v2", "max_signals": 33, "name": "Setuid / Setgid Bit Set via chmod", - "query": "event.category:process AND event.type:(start OR process_started) AND process.name:chmod AND process.args:(\"+s\" OR \"u+s\" OR /4[0-9]{3}/ OR g+s OR /2[0-9]{3}/) AND NOT process.args: ( /.*\\/Applications\\/VirtualBox.app\\/.+/ OR /\\/usr\\/local\\/lib\\/python.+/ OR /\\/var\\/folders\\/.+\\/FP.*nstallHelper/ OR /\\/Library\\/Filesystems\\/.+/ OR /\\/usr\\/lib\\/virtualbox\\/.+/ OR /\\/Library\\/Application.*/ OR \"/run/postgresql\" OR \"/var/crash\" OR \"/var/run/postgresql\" OR /\\/usr\\/bin\\/.+/ OR /\\/usr\\/local\\/share\\/.+/ OR /\\/Applications\\/.+/ OR /\\/usr\\/libexec\\/.+/ OR \"/var/metrics\" OR /\\/var\\/lib\\/dpkg\\/.+/ OR /\\/run\\/log\\/journal\\/.*/ OR \\/Users\\/*\\/.minikube\\/bin\\/docker-machine-driver-hyperkit ) AND NOT process.parent.executable: ( /\\/var\\/lib\\/docker\\/.+/ OR \"/System/Library/PrivateFrameworks/PackageKit.framework/Versions/A/XPCServices/package_script_service.xpc/Contents/MacOS/package_script_service\" OR \"/var/lib/dpkg/info/whoopsie.postinst\" )", + "query": "event.category:process AND event.type:(start OR process_started) AND\n process.name:chmod AND process.args:(\"+s\" OR \"u+s\" OR /4[0-9]{3}/ OR g+s OR /2[0-9]{3}/) AND\n NOT process.args:\n (\n /.*\\/Applications\\/VirtualBox.app\\/.+/ OR\n /\\/usr\\/local\\/lib\\/python.+/ OR\n /\\/var\\/folders\\/.+\\/FP.*nstallHelper/ OR\n /\\/Library\\/Filesystems\\/.+/ OR\n /\\/usr\\/lib\\/virtualbox\\/.+/ OR\n /\\/Library\\/Application.*/ OR\n \"/run/postgresql\" OR\n \"/var/crash\" OR\n \"/var/run/postgresql\" OR\n /\\/usr\\/bin\\/.+/ OR /\\/usr\\/local\\/share\\/.+/ OR\n /\\/Applications\\/.+/ OR /\\/usr\\/libexec\\/.+/ OR\n \"/var/metrics\" OR /\\/var\\/lib\\/dpkg\\/.+/ OR\n /\\/run\\/log\\/journal\\/.*/ OR\n \\/Users\\/*\\/.minikube\\/bin\\/docker-machine-driver-hyperkit\n ) AND\n NOT process.parent.executable:\n (\n /\\/var\\/lib\\/docker\\/.+/ OR\n \"/System/Library/PrivateFrameworks/PackageKit.framework/Versions/A/XPCServices/package_script_service.xpc/Contents/MacOS/package_script_service\" OR\n \"/var/lib/dpkg/info/whoopsie.postinst\"\n )\n", "risk_score": 21, "rule_id": "8a1b0278-0f9a-487d-96bd-d4833298e87a", "severity": "low", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_sudo_buffer_overflow.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_sudo_buffer_overflow.json index 144cea40e6a4b..1c7a064131171 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_sudo_buffer_overflow.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_sudo_buffer_overflow.json @@ -14,7 +14,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Sudo Heap-Based Buffer Overflow Attempt", - "query": "event.category:process and event.type:start and process.name:(sudo or sudoedit) and process.args:(*\\\\ and (\"-i\" or \"-s\"))", + "query": "event.category:process and event.type:start and\n process.name:(sudo or sudoedit) and\n process.args:(*\\\\ and (\"-i\" or \"-s\"))\n", "references": [ "https://cve.mitre.org/cgi-bin/cvename.cgi?name=2021-3156", "https://blog.qualys.com/vulnerabilities-research/2021/01/26/cve-2021-3156-heap-based-buffer-overflow-in-sudo-baron-samedit", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_sudoers_file_mod.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_sudoers_file_mod.json index 1b480e3d19650..963c16ae4dd61 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_sudoers_file_mod.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_sudoers_file_mod.json @@ -11,7 +11,7 @@ "language": "kuery", "license": "Elastic License v2", "name": "Sudoers File Modification", - "query": "event.category:file and event.type:change and file.path:(/etc/sudoers* or /private/etc/sudoers*)", + "query": "event.category:file and event.type:change and file.path:(/etc/sudoers* or /private/etc/sudoers*)\n", "risk_score": 47, "rule_id": "931e25a5-0f5e-4ae0-ba0d-9e94eff7e3a4", "severity": "medium", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_updateassumerolepolicy.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_updateassumerolepolicy.json index 8f01db818dfb9..e46e59feea6f0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_updateassumerolepolicy.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_updateassumerolepolicy.json @@ -16,7 +16,7 @@ "license": "Elastic License v2", "name": "AWS IAM Assume Role Policy Update", "note": "## Config\n\nThe AWS Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:aws.cloudtrail and event.provider:iam.amazonaws.com and event.action:UpdateAssumeRolePolicy and event.outcome:success", + "query": "event.dataset:aws.cloudtrail and event.provider:iam.amazonaws.com and event.action:UpdateAssumeRolePolicy and event.outcome:success\n", "references": [ "https://labs.bishopfox.com/tech-blog/5-privesc-attack-vectors-in-aws" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/threat_intel_module_match.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/threat_intel_module_match.json index 9a058b50683b6..f582eba053d64 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/threat_intel_module_match.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/threat_intel_module_match.json @@ -17,7 +17,7 @@ "license": "Elastic License v2", "name": "Threat Intel Filebeat Module Indicator Match", "note": "## Triage and Analysis\n\nIf an indicator matches a local observation, the following enriched fields will be generated to identify the indicator, field, and type matched.\n\n- `threatintel.indicator.matched.atomic` - this identifies the atomic indicator that matched the local observation\n- `threatintel.indicator.matched.field` - this identifies the indicator field that matched the local observation\n- `threatintel.indicator.matched.type` - this identifies the indicator type that matched the local observation\n", - "query": "file.hash.*:* or file.pe.imphash:* or source.ip:* or destination.ip:* or url.full:* or registry.path:*", + "query": "file.hash.*:* or file.pe.imphash:* or source.ip:* or destination.ip:* or url.full:* or registry.path:*\n", "references": [ "https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-module-threatintel.html" ], diff --git a/x-pack/test/fleet_api_integration/apis/epm/delete.ts b/x-pack/test/fleet_api_integration/apis/epm/delete.ts index c79a29a858b46..5f90805e5879f 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/delete.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/delete.ts @@ -11,7 +11,7 @@ import { skipIfNoDockerRegistry } from '../../helpers'; export default function (providerContext: FtrProviderContext) { const { getService } = providerContext; const supertest = getService('supertest'); - const requiredPackage = 'system-0.12.6'; + const requiredPackage = 'system-0.13.3'; const installPackage = async (pkgkey: string) => { await supertest diff --git a/x-pack/test/fleet_api_integration/config.ts b/x-pack/test/fleet_api_integration/config.ts index d18ba9c55ca96..d1c6c3c3f6b1e 100644 --- a/x-pack/test/fleet_api_integration/config.ts +++ b/x-pack/test/fleet_api_integration/config.ts @@ -15,8 +15,7 @@ import { defineDockerServersConfig } from '@kbn/test'; // example: https://beats-ci.elastic.co/blue/organizations/jenkins/Ingest-manager%2Fpackage-storage/detail/snapshot/74/pipeline/257#step-302-log-1. // It should be updated any time there is a new Docker image published for the Snapshot Distribution of the Package Registry. export const dockerImage = - process.env.FLEET_PACKAGE_REGISTRY_DOCKER_IMAGE || - 'docker.elastic.co/package-registry/distribution:fc104ac437370d80518e24da6d0b84370edf0c0c'; + 'docker.elastic.co/package-registry/distribution@sha256:35cedaaa6adac547947321fa0c3b60a63eba153ba09524b9c1a21f1247a09bd2'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { const xPackAPITestsConfig = await readConfigFile(require.resolve('../api_integration/config.ts')); From bd2215f587de0da0613bf1c5a71c71b8efbd868f Mon Sep 17 00:00:00 2001 From: Luke Elmers Date: Thu, 24 Jun 2021 16:46:19 -0600 Subject: [PATCH 60/69] [docs][migrations v2] Update SO migration docs to include removal of index write block when handling corrupt SOs. (#103014) --- .../setup/upgrade/upgrade-migrations.asciidoc | 49 ++++++++++++++++--- 1 file changed, 41 insertions(+), 8 deletions(-) diff --git a/docs/setup/upgrade/upgrade-migrations.asciidoc b/docs/setup/upgrade/upgrade-migrations.asciidoc index fdcd71791ad3a..947043b21ef50 100644 --- a/docs/setup/upgrade/upgrade-migrations.asciidoc +++ b/docs/setup/upgrade/upgrade-migrations.asciidoc @@ -55,22 +55,55 @@ This section highlights common causes of {kib} upgrade failures and how to preve There is a known issue in v7.12.0 for users who tried the fleet beta. Upgrade migrations fail because of a large number of documents in the `.kibana` index. This can cause Kibana to log errors like: -> Error: Unable to complete saved object migrations for the [.kibana] index. Please check the health of your Elasticsearch cluster and try again. Error: [receive_timeout_transport_exception]: [instance-0000000002][10.32.1.112:19541][cluster:monitor/task/get] request_id [2648] timed out after [59940ms] -> Error: Unable to complete saved object migrations for the [.kibana] index. Please check the health of your Elasticsearch cluster and try again. Error: [timeout_exception]: Timed out waiting for completion of [org.elasticsearch.index.reindex.BulkByScrollTask@6a74c54] + +[source,sh] +-------------------------------------------- +Error: Unable to complete saved object migrations for the [.kibana] index. Please check the health of your Elasticsearch cluster and try again. Error: [receive_timeout_transport_exception]: [instance-0000000002][10.32.1.112:19541][cluster:monitor/task/get] request_id [2648] timed out after [59940ms] + +Error: Unable to complete saved object migrations for the [.kibana] index. Please check the health of your Elasticsearch cluster and try again. Error: [timeout_exception]: Timed out waiting for completion of [org.elasticsearch.index.reindex.BulkByScrollTask@6a74c54] +-------------------------------------------- See https://github.com/elastic/kibana/issues/95321 for instructions to work around this issue. [float] ===== Corrupt saved objects -We highly recommend testing your {kib} upgrade in a development cluster to discover and remedy problems caused by corrupt documents, especially when there are custom integrations creating saved objects in your environment. Saved objects that were corrupted through manual editing or integrations will cause migration failures with a log message like `Failed to transform document. Transform: index-pattern:7.0.0\n Doc: {...}` or `Unable to migrate the corrupt Saved Object document ...`. Corrupt documents will have to be fixed or deleted before an upgrade migration can succeed. +We highly recommend testing your {kib} upgrade in a development cluster to discover and remedy problems caused by corrupt documents, especially when there are custom integrations creating saved objects in your environment. + +Saved objects that were corrupted through manual editing or integrations will cause migration failures with a log message like `Failed to transform document. Transform: index-pattern:7.0.0\n Doc: {...}` or `Unable to migrate the corrupt Saved Object document ...`. Corrupt documents will have to be fixed or deleted before an upgrade migration can succeed. For example, given the following error message: -> Unable to migrate the corrupt saved object document with _id: 'marketing_space:dashboard:e3c5fc71-ac71-4805-bcab-2bcc9cc93275'. To allow migrations to proceed, please delete this document from the [.kibana_7.12.0_001] index. -The following steps must be followed to allow the upgrade migration to succeed. -Please be aware the Dashboard having ID `e3c5fc71-ac71-4805-bcab-2bcc9cc93275` belonging to the space `marketing_space` will no more be available: -1. Delete the corrupt document with `DELETE .kibana_7.12.0_001/_doc/marketing_space:dashboard:e3c5fc71-ac71-4805-bcab-2bcc9cc93275` -2. Restart {kib} +[source,sh] +-------------------------------------------- +Unable to migrate the corrupt saved object document with _id: 'marketing_space:dashboard:e3c5fc71-ac71-4805-bcab-2bcc9cc93275'. To allow migrations to proceed, please delete this document from the [.kibana_7.12.0_001] index. +-------------------------------------------- + +The following steps must be followed to delete the document that is causing the migration to fail: + +. Remove the write block which the migration system has placed on the previous index: ++ +[source,sh] +-------------------------------------------- +PUT .kibana_7.12.1_001/_settings +{ + "index": { + "blocks.write": false + } +} +-------------------------------------------- + +. Delete the corrupt document: ++ +[source,sh] +-------------------------------------------- +DELETE .kibana_7.12.0_001/_doc/marketing_space:dashboard:e3c5fc71-ac71-4805-bcab-2bcc9cc93275 +-------------------------------------------- + +. Restart {kib}. + +In this example, the Dashboard with ID `e3c5fc71-ac71-4805-bcab-2bcc9cc93275` that belongs to the space `marketing_space` **will no longer be available**. + +Be sure you have a snapshot before you delete the corrupt document. If restoring from a snapshot is not an option, it is recommended to also delete the `temp` and `target` indices the migration created before restarting {kib} and retrying. [float] ===== User defined index templates that causes new `.kibana*` indices to have incompatible settings or mappings From 803d0fa57ba0cb955b846b52c2d8489cb101312c Mon Sep 17 00:00:00 2001 From: Constance Date: Thu, 24 Jun 2021 16:02:48 -0700 Subject: [PATCH 61/69] [Enterprise Search] Final KibanaPageTemplate cleanup (#103355) * [AS] Delete AppSearchNav and EngineNav * [WS] Delete WorkplaceSearchNav * [Shared] Delete custom Layout & SideNav components --- .../components/engine/engine_nav.test.tsx | 184 +------------- .../components/engine/engine_nav.tsx | 235 +----------------- .../app_search/components/engine/index.ts | 1 - .../applications/app_search/index.test.tsx | 55 +--- .../public/applications/app_search/index.tsx | 39 +-- .../applications/shared/layout/index.ts | 4 - .../applications/shared/layout/layout.scss | 96 ------- .../shared/layout/layout.test.tsx | 77 ------ .../applications/shared/layout/layout.tsx | 82 ------ .../applications/shared/layout/side_nav.scss | 87 ------- .../shared/layout/side_nav.test.tsx | 166 ------------- .../applications/shared/layout/side_nav.tsx | 130 ---------- .../shared/layout/side_nav_bg.svg | 25 -- .../components/layout/index.ts | 2 +- .../components/layout/nav.test.tsx | 21 +- .../components/layout/nav.tsx | 41 +-- .../translations/translations/ja-JP.json | 3 - .../translations/translations/zh-CN.json | 3 - 18 files changed, 18 insertions(+), 1233 deletions(-) delete mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.scss delete mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.test.tsx delete mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.tsx delete mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.scss delete mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.test.tsx delete mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.tsx delete mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav_bg.svg diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.test.tsx index 015fb997c29ed..e088678a13562 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.test.tsx @@ -10,7 +10,6 @@ import { mockUseRouteMatch } from '../../../__mocks__/react_router'; import { mockEngineValues } from '../../__mocks__'; jest.mock('../../../shared/layout', () => ({ - ...jest.requireActual('../../../shared/layout'), // TODO: Remove once side nav components are gone generateNavLink: jest.fn(({ to }) => ({ href: to })), })); @@ -20,9 +19,7 @@ import { shallow } from 'enzyme'; import { EuiBadge, EuiIcon } from '@elastic/eui'; -import { rerender } from '../../../test_helpers'; - -import { useEngineNav, EngineNav } from './engine_nav'; +import { useEngineNav } from './engine_nav'; describe('useEngineNav', () => { const values = { ...mockEngineValues, myRole: {}, dataLoading: false }; @@ -321,182 +318,3 @@ describe('useEngineNav', () => { }); }); }); - -describe('EngineNav', () => { - const values = { ...mockEngineValues, myRole: {}, dataLoading: false }; - - beforeEach(() => { - setMockValues(values); - }); - - it('does not render if async data is still loading', () => { - setMockValues({ ...values, dataLoading: true }); - const wrapper = shallow(); - expect(wrapper.isEmptyRender()).toBe(true); - }); - - it('does not render without an engine name', () => { - setMockValues({ ...values, engineName: '' }); - const wrapper = shallow(); - expect(wrapper.isEmptyRender()).toBe(true); - }); - - it('renders an engine label and badges', () => { - setMockValues({ ...values, isSampleEngine: false, isMetaEngine: false }); - const wrapper = shallow(); - const label = wrapper.find('[data-test-subj="EngineLabel"]').find('.eui-textTruncate'); - - expect(label.text()).toEqual('SOME-ENGINE'); - expect(wrapper.find(EuiBadge)).toHaveLength(0); - - setMockValues({ ...values, isSampleEngine: true }); - rerender(wrapper); - expect(wrapper.find(EuiBadge).prop('children')).toEqual('SAMPLE ENGINE'); - - setMockValues({ ...values, isMetaEngine: true }); - rerender(wrapper); - expect(wrapper.find(EuiBadge).prop('children')).toEqual('META ENGINE'); - }); - - it('renders a default engine overview link', () => { - const wrapper = shallow(); - expect(wrapper.find('[data-test-subj="EngineOverviewLink"]')).toHaveLength(1); - }); - - it('renders an analytics link', () => { - setMockValues({ ...values, myRole: { canViewEngineAnalytics: true } }); - const wrapper = shallow(); - expect(wrapper.find('[data-test-subj="EngineAnalyticsLink"]')).toHaveLength(1); - }); - - it('renders a documents link', () => { - setMockValues({ ...values, myRole: { canViewEngineDocuments: true } }); - const wrapper = shallow(); - expect(wrapper.find('[data-test-subj="EngineDocumentsLink"]')).toHaveLength(1); - }); - - it('renders a schema link', () => { - setMockValues({ ...values, myRole: { canViewEngineSchema: true } }); - const wrapper = shallow(); - expect(wrapper.find('[data-test-subj="EngineSchemaLink"]')).toHaveLength(1); - }); - - describe('schema nav icons', () => { - const myRole = { canViewEngineSchema: true }; - - it('renders schema errors alert icon', () => { - setMockValues({ ...values, myRole, hasSchemaErrors: true }); - const wrapper = shallow(); - expect(wrapper.find('[data-test-subj="EngineNavSchemaErrors"]')).toHaveLength(1); - }); - - it('renders unconfirmed schema fields info icon', () => { - setMockValues({ ...values, myRole, hasUnconfirmedSchemaFields: true }); - const wrapper = shallow(); - expect(wrapper.find('[data-test-subj="EngineNavSchemaUnconfirmedFields"]')).toHaveLength(1); - }); - - it('renders schema conflicts alert icon', () => { - setMockValues({ ...values, myRole, hasSchemaConflicts: true }); - const wrapper = shallow(); - expect(wrapper.find('[data-test-subj="EngineNavSchemaConflicts"]')).toHaveLength(1); - }); - }); - - describe('crawler link', () => { - const myRole = { canViewEngineCrawler: true }; - - it('renders', () => { - setMockValues({ ...values, myRole }); - const wrapper = shallow(); - expect(wrapper.find('[data-test-subj="EngineCrawlerLink"]')).toHaveLength(1); - }); - - it('does not render for meta engines', () => { - setMockValues({ ...values, myRole, isMetaEngine: true }); - const wrapper = shallow(); - expect(wrapper.find('[data-test-subj="EngineCrawlerLink"]')).toHaveLength(0); - }); - }); - - describe('meta engine source engines link', () => { - const myRole = { canViewMetaEngineSourceEngines: true }; - - it('renders', () => { - setMockValues({ ...values, myRole, isMetaEngine: true }); - const wrapper = shallow(); - expect(wrapper.find('[data-test-subj="MetaEngineEnginesLink"]')).toHaveLength(1); - }); - - it('does not render for non meta engines', () => { - setMockValues({ ...values, myRole, isMetaEngine: false }); - const wrapper = shallow(); - expect(wrapper.find('[data-test-subj="MetaEngineEnginesLink"]')).toHaveLength(0); - }); - }); - - it('renders a relevance tuning link', () => { - setMockValues({ ...values, myRole: { canManageEngineRelevanceTuning: true } }); - const wrapper = shallow(); - expect(wrapper.find('[data-test-subj="EngineRelevanceTuningLink"]')).toHaveLength(1); - }); - - describe('relevance tuning nav icons', () => { - const myRole = { canManageEngineRelevanceTuning: true }; - - it('renders unconfirmed schema fields info icon', () => { - const engine = { unsearchedUnconfirmedFields: true }; - setMockValues({ ...values, myRole, engine }); - const wrapper = shallow(); - expect( - wrapper.find('[data-test-subj="EngineNavRelevanceTuningUnsearchedFields"]') - ).toHaveLength(1); - }); - - it('renders schema conflicts alert icon', () => { - const engine = { invalidBoosts: true }; - setMockValues({ ...values, myRole, engine }); - const wrapper = shallow(); - expect(wrapper.find('[data-test-subj="EngineNavRelevanceTuningInvalidBoosts"]')).toHaveLength( - 1 - ); - }); - - it('can render multiple icons', () => { - const engine = { invalidBoosts: true, unsearchedUnconfirmedFields: true }; - setMockValues({ ...values, myRole, engine }); - const wrapper = shallow(); - expect(wrapper.find(EuiIcon)).toHaveLength(2); - }); - }); - - it('renders a synonyms link', () => { - setMockValues({ ...values, myRole: { canManageEngineSynonyms: true } }); - const wrapper = shallow(); - expect(wrapper.find('[data-test-subj="EngineSynonymsLink"]')).toHaveLength(1); - }); - - it('renders a curations link', () => { - setMockValues({ ...values, myRole: { canManageEngineCurations: true } }); - const wrapper = shallow(); - expect(wrapper.find('[data-test-subj="EngineCurationsLink"]')).toHaveLength(1); - }); - - it('renders a results settings link', () => { - setMockValues({ ...values, myRole: { canManageEngineResultSettings: true } }); - const wrapper = shallow(); - expect(wrapper.find('[data-test-subj="EngineResultSettingsLink"]')).toHaveLength(1); - }); - - it('renders a Search UI link', () => { - setMockValues({ ...values, myRole: { canManageEngineSearchUi: true } }); - const wrapper = shallow(); - expect(wrapper.find('[data-test-subj="EngineSearchUILink"]')).toHaveLength(1); - }); - - it('renders an API logs link', () => { - setMockValues({ ...values, myRole: { canViewEngineApiLogs: true } }); - const wrapper = shallow(); - expect(wrapper.find('[data-test-subj="EngineAPILogsLink"]')).toHaveLength(1); - }); -}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx index 76e751cf4da5f..70f2d04a5123d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx @@ -10,17 +10,10 @@ import { useRouteMatch } from 'react-router-dom'; import { useValues } from 'kea'; -import { - EuiSideNavItemType, - EuiText, - EuiBadge, - EuiIcon, - EuiFlexGroup, - EuiFlexItem, -} from '@elastic/eui'; +import { EuiSideNavItemType, EuiText, EuiBadge, EuiIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { generateNavLink, SideNavLink, SideNavItem } from '../../../shared/layout'; +import { generateNavLink } from '../../../shared/layout'; import { AppLogic } from '../../app_logic'; import { ENGINE_PATH, @@ -49,8 +42,6 @@ import { SCHEMA_TITLE } from '../schema'; import { SEARCH_UI_TITLE } from '../search_ui'; import { SYNONYMS_TITLE } from '../synonyms'; -import { EngineDetails } from './types'; - import { EngineLogic, generateEnginePath } from './'; import './engine_nav.scss'; @@ -198,7 +189,10 @@ export const useEngineNav = () => { navItems.push({ id: 'crawler', name: CRAWLER_TITLE, - ...generateNavLink({ to: generateEnginePath(ENGINE_CRAWLER_PATH) }), + ...generateNavLink({ + to: generateEnginePath(ENGINE_CRAWLER_PATH), + shouldShowActiveForSubroutes: true, + }), 'data-test-subj': 'EngineCrawlerLink', }); } @@ -301,220 +295,3 @@ export const useEngineNav = () => { return navItems; }; - -// TODO: Delete the below once page template migration is complete - -export const EngineNav: React.FC = () => { - const { - myRole: { - canViewEngineAnalytics, - canViewEngineDocuments, - canViewEngineSchema, - canViewEngineCrawler, - canViewMetaEngineSourceEngines, - canManageEngineSynonyms, - canManageEngineCurations, - canManageEngineRelevanceTuning, - canManageEngineResultSettings, - canManageEngineSearchUi, - canViewEngineApiLogs, - }, - } = useValues(AppLogic); - - const { - engineName, - dataLoading, - isSampleEngine, - isMetaEngine, - hasSchemaErrors, - hasSchemaConflicts, - hasUnconfirmedSchemaFields, - engine, - } = useValues(EngineLogic); - - if (dataLoading) return null; - if (!engineName) return null; - - const { invalidBoosts, unsearchedUnconfirmedFields } = engine as Required; - - return ( - <> - - -
{engineName.toUpperCase()}
- {isSampleEngine && ( - - {i18n.translate('xpack.enterpriseSearch.appSearch.engine.sampleEngineBadge', { - defaultMessage: 'SAMPLE ENGINE', - })} - - )} - {isMetaEngine && ( - - {i18n.translate('xpack.enterpriseSearch.appSearch.engine.metaEngineBadge', { - defaultMessage: 'META ENGINE', - })} - - )} -
-
- - {OVERVIEW_TITLE} - - {canViewEngineAnalytics && ( - - {ANALYTICS_TITLE} - - )} - {canViewEngineDocuments && ( - - {DOCUMENTS_TITLE} - - )} - {canViewEngineSchema && ( - - - {SCHEMA_TITLE} - - {hasSchemaErrors && ( - - )} - {hasUnconfirmedSchemaFields && ( - - )} - {hasSchemaConflicts && ( - - )} - - - - )} - {canViewEngineCrawler && !isMetaEngine && ( - - {CRAWLER_TITLE} - - )} - {canViewMetaEngineSourceEngines && isMetaEngine && ( - - {ENGINES_TITLE} - - )} - {canManageEngineRelevanceTuning && ( - - - {RELEVANCE_TUNING_TITLE} - - {invalidBoosts && ( - - )} - {unsearchedUnconfirmedFields && ( - - )} - - - - )} - {canManageEngineSynonyms && ( - - {SYNONYMS_TITLE} - - )} - {canManageEngineCurations && ( - - {CURATIONS_TITLE} - - )} - {canManageEngineResultSettings && ( - - {RESULT_SETTINGS_TITLE} - - )} - {canManageEngineSearchUi && ( - - {SEARCH_UI_TITLE} - - )} - {canViewEngineApiLogs && ( - - {API_LOGS_TITLE} - - )} - - ); -}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/index.ts index 2a5b3351f41f7..86b4ff21e62d3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/index.ts @@ -6,6 +6,5 @@ */ export { EngineRouter } from './engine_router'; -export { EngineNav } from './engine_nav'; export { EngineLogic } from './engine_logic'; export { generateEnginePath, getEngineBreadcrumbs } from './utils'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx index 46596cc5d6765..6647b4032e4bc 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx @@ -7,7 +7,6 @@ import { DEFAULT_INITIAL_APP_DATA } from '../../../common/__mocks__'; import { setMockValues } from '../__mocks__/kea_logic'; -import { mockUseRouteMatch } from '../__mocks__/react_router'; import '../__mocks__/shallow_useeffect.mock'; import '../__mocks__/enterprise_search_url.mock'; @@ -17,15 +16,13 @@ import { Redirect } from 'react-router-dom'; import { shallow, ShallowWrapper } from 'enzyme'; -import { SideNav, SideNavLink } from '../shared/layout'; - import { rerender } from '../test_helpers'; jest.mock('./app_logic', () => ({ AppLogic: jest.fn() })); import { AppLogic } from './app_logic'; import { Credentials } from './components/credentials'; -import { EngineRouter, EngineNav } from './components/engine'; +import { EngineRouter } from './components/engine'; import { EngineCreation } from './components/engine_creation'; import { EnginesOverview } from './components/engines'; import { ErrorConnecting } from './components/error_connecting'; @@ -35,7 +32,7 @@ import { RoleMappings } from './components/role_mappings'; import { Settings } from './components/settings'; import { SetupGuide } from './components/setup_guide'; -import { AppSearch, AppSearchUnconfigured, AppSearchConfigured, AppSearchNav } from './'; +import { AppSearch, AppSearchUnconfigured, AppSearchConfigured } from './'; describe('AppSearch', () => { it('always renders the Setup Guide', () => { @@ -142,51 +139,3 @@ describe('AppSearchConfigured', () => { }); }); }); - -describe('AppSearchNav', () => { - it('renders with the Engines link', () => { - const wrapper = shallow(); - - expect(wrapper.find(SideNav)).toHaveLength(1); - expect(wrapper.find(SideNavLink).prop('to')).toEqual('/engines'); - }); - - describe('engine subnavigation', () => { - const getEnginesLink = (wrapper: ShallowWrapper) => wrapper.find(SideNavLink).dive(); - - it('does not render the engine subnav on top-level routes', () => { - mockUseRouteMatch.mockReturnValueOnce(false); - const wrapper = shallow(); - - expect(getEnginesLink(wrapper).find(EngineNav)).toHaveLength(0); - }); - - it('renders the engine subnav if currently on an engine route', () => { - mockUseRouteMatch.mockReturnValueOnce(true); - const wrapper = shallow(); - - expect(getEnginesLink(wrapper).find(EngineNav)).toHaveLength(1); - }); - }); - - it('renders the Settings link', () => { - setMockValues({ myRole: { canViewSettings: true } }); - const wrapper = shallow(); - - expect(wrapper.find(SideNavLink).last().prop('to')).toEqual('/settings'); - }); - - it('renders the Credentials link', () => { - setMockValues({ myRole: { canViewAccountCredentials: true } }); - const wrapper = shallow(); - - expect(wrapper.find(SideNavLink).last().prop('to')).toEqual('/credentials'); - }); - - it('renders the Role Mappings link', () => { - setMockValues({ myRole: { canViewRoleMappings: true } }); - const wrapper = shallow(); - - expect(wrapper.find(SideNavLink).last().prop('to')).toEqual('/users_and_roles'); - }); -}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx index 6d049b2015487..11f706bff028f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx @@ -6,30 +6,26 @@ */ import React, { useEffect } from 'react'; -import { Route, Redirect, Switch, useRouteMatch } from 'react-router-dom'; +import { Route, Redirect, Switch } from 'react-router-dom'; import { useValues } from 'kea'; -import { APP_SEARCH_PLUGIN } from '../../../common/constants'; import { InitialAppData } from '../../../common/types'; import { HttpLogic } from '../shared/http'; import { KibanaLogic } from '../shared/kibana'; -import { SideNav, SideNavLink } from '../shared/layout'; - -import { ROLE_MAPPINGS_TITLE } from '../shared/role_mapping/constants'; import { AppLogic } from './app_logic'; -import { Credentials, CREDENTIALS_TITLE } from './components/credentials'; -import { EngineNav, EngineRouter } from './components/engine'; +import { Credentials } from './components/credentials'; +import { EngineRouter } from './components/engine'; import { EngineCreation } from './components/engine_creation'; -import { EnginesOverview, ENGINES_TITLE } from './components/engines'; +import { EnginesOverview } from './components/engines'; import { ErrorConnecting } from './components/error_connecting'; import { KibanaHeaderActions } from './components/layout'; import { Library } from './components/library'; import { MetaEngineCreation } from './components/meta_engine_creation'; import { NotFound } from './components/not_found'; import { RoleMappings } from './components/role_mappings'; -import { Settings, SETTINGS_TITLE } from './components/settings'; +import { Settings } from './components/settings'; import { SetupGuide } from './components/setup_guide'; import { ENGINE_CREATION_PATH, @@ -137,28 +133,3 @@ export const AppSearchConfigured: React.FC> = (props) = ); }; - -export const AppSearchNav: React.FC = () => { - const { - myRole: { canViewSettings, canViewAccountCredentials, canViewRoleMappings }, - } = useValues(AppLogic); - - const isEngineRoute = !!useRouteMatch(ENGINE_PATH); - - return ( - - : null} isRoot> - {ENGINES_TITLE} - - {canViewSettings && {SETTINGS_TITLE}} - {canViewAccountCredentials && ( - {CREDENTIALS_TITLE} - )} - {canViewRoleMappings && ( - - {ROLE_MAPPINGS_TITLE} - - )} - - ); -}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/layout/index.ts index 856d483e174a6..41f8869ad5f61 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/index.ts @@ -7,7 +7,3 @@ export { EnterpriseSearchPageTemplate, PageTemplateProps } from './page_template'; export { generateNavLink } from './nav_link_helpers'; - -// TODO: Delete these once KibanaPageTemplate migration is done -export { Layout } from './layout'; -export { SideNav, SideNavLink, SideNavItem } from './side_nav'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.scss b/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.scss deleted file mode 100644 index 88799406070cd..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.scss +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -.enterpriseSearchLayout { - $sideBarWidth: $euiSize * 15; - $sideBarMobileHeight: $euiSize * 4.75; - $consoleHeaderHeight: 98px; // NOTE: Keep an eye on this for changes - $pageHeight: calc(100vh - #{$consoleHeaderHeight}); - - display: block; - background-color: $euiColorEmptyShade; - min-height: $pageHeight; - position: relative; - left: $sideBarWidth; - width: calc(100% - #{$sideBarWidth}); - padding: 0; - - @include euiBreakpoint('xs', 's', 'm') { - left: auto; - width: 100%; - } - - &__sideBarToggle { - display: none; - - @include euiBreakpoint('xs', 's', 'm') { - display: block; - - position: absolute; - right: $euiSize; - top: $sideBarMobileHeight / 2; - transform: translateY(-50%) scale(.9); - - .euiButton { - min-width: 0; - } - } - } - - &__sideBar { - z-index: $euiZNavigation; - position: fixed; - margin-left: -1 * $sideBarWidth; - margin-right: 0; - overflow-y: auto; - overflow-x: hidden; - - height: $pageHeight; - width: $sideBarWidth; - - background-color: $euiColorLightestShade; - box-shadow: inset (-1 * $euiSizeXS) 0 $euiSizeS (-1 * $euiSizeXS) rgba($euiShadowColor, .25); - - @include euiBreakpoint('xs', 's', 'm') { - position: relative; - width: 100%; - height: $sideBarMobileHeight; - margin-left: 0; - overflow-y: hidden; - - border-bottom: $euiBorderThin; - box-shadow: none; - - &--isOpen { - height: auto; - overflow-y: auto; - } - } - } - - &__body { - padding: $euiSizeXXL; - - @include euiBreakpoint('m') { - padding: $euiSizeL; - } - @include euiBreakpoint('xs', 's') { - padding: $euiSize; - } - } - - &__readOnlyMode { - margin: -$euiSizeM 0 $euiSizeL; - - @include euiBreakpoint('m') { - margin: 0 0 $euiSizeL; - } - @include euiBreakpoint('xs', 's') { - margin: 0; - } - } -} diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.test.tsx deleted file mode 100644 index 28092f75cdede..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.test.tsx +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { EuiPageSideBar, EuiButton, EuiPageBody, EuiCallOut } from '@elastic/eui'; - -import { Layout, INavContext } from './layout'; - -describe('Layout', () => { - it('renders', () => { - const wrapper = shallow(); - - expect(wrapper.find('.enterpriseSearchLayout')).toHaveLength(1); - expect(wrapper.find(EuiPageBody).prop('restrictWidth')).toBeFalsy(); - }); - - it('passes the restrictWidth prop', () => { - const wrapper = shallow(); - - expect(wrapper.find(EuiPageBody).prop('restrictWidth')).toEqual(true); - }); - - it('renders navigation', () => { - const wrapper = shallow(Hello World} />); - - expect(wrapper.find('.enterpriseSearchLayout__sideBar')).toHaveLength(1); - expect(wrapper.find('.nav-test')).toHaveLength(1); - }); - - it('renders navigation toggle state', () => { - const wrapper = shallow(Hello World} />); - expect(wrapper.find(EuiPageSideBar).prop('className')).not.toContain('--isOpen'); - expect(wrapper.find(EuiButton).prop('iconType')).toEqual('arrowRight'); - - const toggle = wrapper.find('[data-test-subj="enterpriseSearchNavToggle"]'); - toggle.simulate('click'); - - expect(wrapper.find(EuiPageSideBar).prop('className')).toContain('--isOpen'); - expect(wrapper.find(EuiButton).prop('iconType')).toEqual('arrowDown'); - }); - - it('passes down NavContext to navigation links', () => { - const wrapper = shallow(} />); - - const toggle = wrapper.find('[data-test-subj="enterpriseSearchNavToggle"]'); - toggle.simulate('click'); - expect(wrapper.find(EuiPageSideBar).prop('className')).toContain('--isOpen'); - - const context = (wrapper.find('ContextProvider').prop('value') as unknown) as INavContext; - context.closeNavigation(); - expect(wrapper.find(EuiPageSideBar).prop('className')).not.toContain('--isOpen'); - }); - - it('renders a read-only mode callout', () => { - const wrapper = shallow(); - - expect(wrapper.find(EuiCallOut)).toHaveLength(1); - }); - - it('renders children', () => { - const wrapper = shallow( - -
Test
-
- ); - - expect(wrapper.find('.enterpriseSearchLayout__body')).toHaveLength(1); - expect(wrapper.find('.testing')).toHaveLength(1); - }); -}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.tsx deleted file mode 100644 index 9cf5fccddbd5b..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.tsx +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useState } from 'react'; - -import classNames from 'classnames'; - -import { EuiPage, EuiPageSideBar, EuiPageBody, EuiButton, EuiCallOut } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; - -import './layout.scss'; - -interface LayoutProps { - navigation: React.ReactNode; - restrictWidth?: boolean; - readOnlyMode?: boolean; -} - -export interface INavContext { - closeNavigation(): void; -} -export const NavContext = React.createContext({}); - -export const Layout: React.FC = ({ - children, - navigation, - restrictWidth, - readOnlyMode, -}) => { - const [isNavOpen, setIsNavOpen] = useState(false); - const toggleNavigation = () => setIsNavOpen(!isNavOpen); - const closeNavigation = () => setIsNavOpen(false); - - const navClasses = classNames('enterpriseSearchLayout__sideBar', { - 'enterpriseSearchLayout__sideBar--isOpen': isNavOpen, // eslint-disable-line @typescript-eslint/naming-convention - }); - - return ( - - -
- - {i18n.translate('xpack.enterpriseSearch.nav.menu', { - defaultMessage: 'Menu', - })} - -
- {navigation} -
- - {readOnlyMode && ( - - )} - {children} - -
- ); -}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.scss b/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.scss deleted file mode 100644 index f6a1ebed7ba93..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.scss +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -$euiSizeML: $euiSize * 1.25; // 20px - between medium and large ¯\_(ツ)_/¯ - -.enterpriseSearchProduct { - display: flex; - align-items: center; - padding: $euiSizeML; - - background-image: url('./side_nav_bg.svg'); - background-repeat: no-repeat; - - @include euiBreakpoint('xs', 's', 'm') { - padding: $euiSize $euiSizeML; - } - - &__icon { - display: flex; - align-items: center; - justify-content: center; - - width: $euiSizeXXL; - height: $euiSizeXXL; - margin-right: $euiSizeS; - - background-color: $euiColorEmptyShade; - border-radius: 50%; - @include euiSlightShadow(); - - .euiIcon { - width: $euiSizeML; - height: $euiSizeML; - } - } - - &__title { - .euiText { - font-weight: $euiFontWeightMedium; - } - } -} - -.enterpriseSearchNavLinks { - &__item { - display: block; - padding: $euiSizeM $euiSizeML; - font-size: $euiFontSizeS; - font-weight: $euiFontWeightMedium; - line-height: $euiFontSizeM; - - $activeBgColor: rgba($euiColorFullShade, .05); - - &--isActive { - background-color: $activeBgColor; - } - - &.euiLink { - color: $euiTextColor; - font-weight: $euiFontWeightMedium; - - &:hover { - color: $euiTextColor; - } - - &:focus { - outline: solid 0 $activeBgColor; - background-color: $activeBgColor; - } - } - } - - &__subNav { - padding-left: $euiSizeML; - - // Extends the click area of links more to the left, so that second tiers - // of subnavigation links still have the same hitbox as first tier links - .enterpriseSearchNavLinks__item { - margin-left: -$euiSizeML; - padding-left: $euiSizeXXL; - } - } -} diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.test.tsx deleted file mode 100644 index 244037d6e1382..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.test.tsx +++ /dev/null @@ -1,166 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { mockLocation } from '../../__mocks__/react_router'; - -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { EuiLink } from '@elastic/eui'; - -import { ENTERPRISE_SEARCH_PLUGIN, APP_SEARCH_PLUGIN } from '../../../../common/constants'; -import { EuiLinkTo } from '../react_router_helpers'; - -import { SideNav, SideNavLink, SideNavItem } from './'; - -describe('SideNav', () => { - it('renders link children', () => { - const wrapper = shallow( - -
Hello World
-
- ); - - expect(wrapper.type()).toEqual('nav'); - expect(wrapper.find('.enterpriseSearchNavLinks')).toHaveLength(1); - expect(wrapper.find('.testing')).toHaveLength(1); - }); - - it('renders a custom product', () => { - const wrapper = shallow(); - - expect(wrapper.find('h3').text()).toEqual('App Search'); - expect(wrapper.find('.enterpriseSearchProduct--appSearch')).toHaveLength(1); - }); -}); - -describe('SideNavLink', () => { - it('renders', () => { - const wrapper = shallow(Link); - - expect(wrapper.type()).toEqual('li'); - expect(wrapper.find(EuiLinkTo)).toHaveLength(1); - expect(wrapper.find('.enterpriseSearchNavLinks__item')).toHaveLength(1); - }); - - it('renders an external link if isExternal is true', () => { - const wrapper = shallow( - - Link - - ); - const externalLink = wrapper.find(EuiLink); - - expect(externalLink).toHaveLength(1); - expect(externalLink.prop('href')).toEqual('http://website.com'); - expect(externalLink.prop('target')).toEqual('_blank'); - }); - - it('sets an active class if the current path matches the nav link', () => { - mockLocation.pathname = '/test/'; - - const wrapper = shallow(Link); - - expect(wrapper.find('.enterpriseSearchNavLinks__item--isActive')).toHaveLength(1); - }); - - it('sets an active class if the current path is / and the link isRoot', () => { - mockLocation.pathname = '/'; - - const wrapper = shallow( - - Link - - ); - - expect(wrapper.find('.enterpriseSearchNavLinks__item--isActive')).toHaveLength(1); - }); - - it('passes down custom classes and props', () => { - const wrapper = shallow( - - Link - - ); - - expect(wrapper.find('.testing')).toHaveLength(1); - expect(wrapper.find('[data-test-subj="testing"]')).toHaveLength(1); - }); - - it('renders nested subnavigation', () => { - const subNav = ( - - Another link! - - ); - const wrapper = shallow( - - Link - - ); - - expect(wrapper.find('.enterpriseSearchNavLinks__subNav')).toHaveLength(1); - expect(wrapper.find('[data-test-subj="subNav"]')).toHaveLength(1); - }); - - describe('shouldShowActiveForSubroutes', () => { - it("won't set an active class when route is a subroute of 'to'", () => { - mockLocation.pathname = '/documents/1234'; - - const wrapper = shallow( - - Link - - ); - - expect(wrapper.find('.enterpriseSearchNavLinks__item--isActive')).toHaveLength(0); - }); - - it('sets an active class if the current path is a subRoute of "to", and shouldShowActiveForSubroutes is true', () => { - mockLocation.pathname = '/documents/1234'; - - const wrapper = shallow( - - Link - - ); - - expect(wrapper.find('.enterpriseSearchNavLinks__item--isActive')).toHaveLength(1); - }); - }); -}); - -describe('SideNavItem', () => { - it('renders', () => { - const wrapper = shallow(Test); - - expect(wrapper.type()).toEqual('li'); - expect(wrapper.find('.enterpriseSearchNavLinks__item')).toHaveLength(1); - }); - - it('renders children', () => { - const wrapper = shallow( - - World - - ); - - expect(wrapper.find('[data-test-subj="hello"]').text()).toEqual('World'); - }); - - it('passes down custom classes and props', () => { - const wrapper = shallow( - - Test - - ); - - expect(wrapper.find('.testing')).toHaveLength(1); - expect(wrapper.find('[data-test-subj="testing"]')).toHaveLength(1); - }); -}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.tsx deleted file mode 100644 index 58a5c7bbb229f..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.tsx +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useContext } from 'react'; -import { useLocation } from 'react-router-dom'; - -import classNames from 'classnames'; - -import { EuiIcon, EuiTitle, EuiText, EuiLink } from '@elastic/eui'; // TODO: Remove EuiLink after full Kibana transition -import { i18n } from '@kbn/i18n'; - -import { ENTERPRISE_SEARCH_PLUGIN } from '../../../../common/constants'; -import { stripTrailingSlash } from '../../../../common/strip_slashes'; -import { EuiLinkTo } from '../react_router_helpers'; - -import { NavContext, INavContext } from './layout'; - -import './side_nav.scss'; - -/** - * Side navigation - product & icon + links wrapper - */ - -interface SideNavProps { - // Expects product plugin constants (@see common/constants.ts) - product: { - NAME: string; - ID: string; - }; -} - -export const SideNav: React.FC = ({ product, children }) => { - return ( - - ); -}; - -/** - * Side navigation link item - */ - -interface SideNavLinkProps { - to: string; - shouldShowActiveForSubroutes?: boolean; - isExternal?: boolean; - className?: string; - isRoot?: boolean; - subNav?: React.ReactNode; -} - -export const SideNavLink: React.FC = ({ - to, - shouldShowActiveForSubroutes = false, - isExternal, - children, - className, - isRoot, - subNav, - ...rest -}) => { - const { closeNavigation } = useContext(NavContext) as INavContext; - - const { pathname } = useLocation(); - const currentPath = stripTrailingSlash(pathname); - const isActive = - currentPath === to || - (shouldShowActiveForSubroutes && currentPath.startsWith(to)) || - (isRoot && currentPath === ''); - - const classes = classNames('enterpriseSearchNavLinks__item', className, { - 'enterpriseSearchNavLinks__item--isActive': !isExternal && isActive, // eslint-disable-line @typescript-eslint/naming-convention - }); - - return ( -
  • - {isExternal ? ( - // eslint-disable-next-line @elastic/eui/href-or-on-click - - {children} - - ) : ( - - {children} - - )} - {subNav &&
      {subNav}
    } -
  • - ); -}; - -/** - * Side navigation non-link item - */ - -interface SideNavItemProps { - className?: string; -} - -export const SideNavItem: React.FC = ({ children, className, ...rest }) => { - const classes = classNames('enterpriseSearchNavLinks__item', className); - return ( -
  • - {children} -
  • - ); -}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav_bg.svg b/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav_bg.svg deleted file mode 100644 index a19227ab7b7eb..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav_bg.svg +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/index.ts index 8cdc133681762..0cc39f66e3d20 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/index.ts @@ -6,7 +6,7 @@ */ export { WorkplaceSearchPageTemplate } from './page_template'; -export { useWorkplaceSearchNav, WorkplaceSearchNav } from './nav'; +export { useWorkplaceSearchNav } from './nav'; export { WorkplaceSearchHeaderActions } from './kibana_header_actions'; export { AccountHeader } from './account_header'; export { PersonalDashboardLayout } from './personal_dashboard_layout'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.test.tsx index 04576e981e104..65a6a798b032a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.test.tsx @@ -6,7 +6,6 @@ */ jest.mock('../../../shared/layout', () => ({ - ...jest.requireActual('../../../shared/layout'), generateNavLink: jest.fn(({ to, items }) => ({ href: to, items })), })); jest.mock('../../views/content_sources/components/source_sub_nav', () => ({ @@ -19,13 +18,7 @@ jest.mock('../../views/settings/components/settings_sub_nav', () => ({ useSettingsSubNav: () => [], })); -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { SideNav, SideNavLink } from '../../../shared/layout'; - -import { useWorkplaceSearchNav, WorkplaceSearchNav } from './'; +import { useWorkplaceSearchNav } from './'; describe('useWorkplaceSearchNav', () => { it('returns an array of top-level Workplace Search nav items', () => { @@ -72,15 +65,3 @@ describe('useWorkplaceSearchNav', () => { ]); }); }); - -// TODO: Delete below once fully migrated to KibanaPageTemplate - -describe('WorkplaceSearchNav', () => { - it('renders', () => { - const wrapper = shallow(); - - expect(wrapper.find(SideNav)).toHaveLength(1); - expect(wrapper.find(SideNavLink).first().prop('to')).toEqual('/'); - expect(wrapper.find(SideNavLink)).toHaveLength(6); - }); -}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx index c8d821dcdae2e..7dc005a56bf10 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx @@ -5,12 +5,9 @@ * 2.0. */ -import React from 'react'; +import { EuiSideNavItemType } from '@elastic/eui'; -import { EuiSideNavItemType, EuiSpacer } from '@elastic/eui'; - -import { WORKPLACE_SEARCH_PLUGIN } from '../../../../../common/constants'; -import { generateNavLink, SideNav, SideNavLink } from '../../../shared/layout'; +import { generateNavLink } from '../../../shared/layout'; import { NAV } from '../../constants'; import { SOURCES_PATH, @@ -68,37 +65,3 @@ export const useWorkplaceSearchNav = () => { // to cause all our navItems to properly render as nav links. return [{ id: '', name: '', items: navItems }]; }; - -// TODO: Delete below once fully migrated to KibanaPageTemplate - -interface Props { - sourcesSubNav?: React.ReactNode; - groupsSubNav?: React.ReactNode; - settingsSubNav?: React.ReactNode; -} - -export const WorkplaceSearchNav: React.FC = ({ - sourcesSubNav, - groupsSubNav, - settingsSubNav, -}) => ( - - - {NAV.OVERVIEW} - - - {NAV.SOURCES} - - - {NAV.GROUPS} - - - {NAV.ROLE_MAPPINGS} - - {NAV.SECURITY} - - {NAV.SETTINGS} - - - -); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 5f86e020a0407..aab80fe308861 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -7884,9 +7884,6 @@ "xpack.enterpriseSearch.featureCatalogueDescription2": "ユーザーを関連するデータにつなげます。", "xpack.enterpriseSearch.featureCatalogueDescription3": "チームの内容を統合します。", "xpack.enterpriseSearch.hiddenText": "非表示のテキスト", - "xpack.enterpriseSearch.nav.hierarchy": "セカンダリ", - "xpack.enterpriseSearch.nav.menu": "メニュー", - "xpack.enterpriseSearch.nav.toggleMenu": "セカンダリナビゲーションを切り替える", "xpack.enterpriseSearch.navTitle": "概要", "xpack.enterpriseSearch.notFound.action1": "ダッシュボードに戻す", "xpack.enterpriseSearch.notFound.action2": "サポートに問い合わせる", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index acad67a0b1c7a..d4f263b58b8ab 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -7952,9 +7952,6 @@ "xpack.enterpriseSearch.featureCatalogueDescription2": "将您的用户连接到相关数据。", "xpack.enterpriseSearch.featureCatalogueDescription3": "统一您的团队内容。", "xpack.enterpriseSearch.hiddenText": "隐藏文本", - "xpack.enterpriseSearch.nav.hierarchy": "次级", - "xpack.enterpriseSearch.nav.menu": "菜单", - "xpack.enterpriseSearch.nav.toggleMenu": "切换次级导航", "xpack.enterpriseSearch.navTitle": "概览", "xpack.enterpriseSearch.notFound.action1": "返回到您的仪表板", "xpack.enterpriseSearch.notFound.action2": "联系支持人员", From 205684540857cee84be923404c7e7b82939e002e Mon Sep 17 00:00:00 2001 From: Clint Andrew Hall Date: Thu, 24 Jun 2021 18:05:11 -0500 Subject: [PATCH 62/69] [canvas] Reduce bundle size by combining SCSS imports (#102822) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../canvas/public/components/var_config/delete_var.tsx | 2 -- .../canvas/public/components/var_config/edit_var.tsx | 3 --- .../canvas/public/components/var_config/var_config.tsx | 2 -- x-pack/plugins/canvas/public/style/index.scss | 7 +++++++ x-pack/plugins/canvas/public/transitions/fade/index.ts | 2 -- x-pack/plugins/canvas/public/transitions/rotate/index.ts | 2 -- x-pack/plugins/canvas/public/transitions/slide/index.ts | 2 -- x-pack/plugins/canvas/public/transitions/zoom/index.ts | 2 -- 8 files changed, 7 insertions(+), 15 deletions(-) diff --git a/x-pack/plugins/canvas/public/components/var_config/delete_var.tsx b/x-pack/plugins/canvas/public/components/var_config/delete_var.tsx index f6ba2d7e28825..1aea08a96784d 100644 --- a/x-pack/plugins/canvas/public/components/var_config/delete_var.tsx +++ b/x-pack/plugins/canvas/public/components/var_config/delete_var.tsx @@ -39,8 +39,6 @@ const strings = { }), }; -import './var_panel.scss'; - interface Props { selectedVar: CanvasVariable; onDelete: (v: CanvasVariable) => void; diff --git a/x-pack/plugins/canvas/public/components/var_config/edit_var.tsx b/x-pack/plugins/canvas/public/components/var_config/edit_var.tsx index 35f9e67745aec..5501aa9aab637 100644 --- a/x-pack/plugins/canvas/public/components/var_config/edit_var.tsx +++ b/x-pack/plugins/canvas/public/components/var_config/edit_var.tsx @@ -76,9 +76,6 @@ const strings = { }), }; -import './edit_var.scss'; -import './var_panel.scss'; - interface Props { selectedVar: CanvasVariable | null; variables: CanvasVariable[]; diff --git a/x-pack/plugins/canvas/public/components/var_config/var_config.tsx b/x-pack/plugins/canvas/public/components/var_config/var_config.tsx index dc8898e2132e7..25c77ab7704bf 100644 --- a/x-pack/plugins/canvas/public/components/var_config/var_config.tsx +++ b/x-pack/plugins/canvas/public/components/var_config/var_config.tsx @@ -25,8 +25,6 @@ import { CanvasVariable } from '../../../types'; import { EditVar } from './edit_var'; import { DeleteVar } from './delete_var'; -import './var_config.scss'; - enum PanelMode { List, Edit, diff --git a/x-pack/plugins/canvas/public/style/index.scss b/x-pack/plugins/canvas/public/style/index.scss index d9592d5c0be5f..e866eada1f85f 100644 --- a/x-pack/plugins/canvas/public/style/index.scss +++ b/x-pack/plugins/canvas/public/style/index.scss @@ -43,6 +43,13 @@ @import '../components/workpad_page/workpad_page'; @import '../components/workpad_page/workpad_interactive_page/workpad_interactive_page'; @import '../components/workpad_page/workpad_static_page/workpad_static_page'; +@import '../components/var_config/edit_var'; +@import '../components/var_config/var_config'; + +@import '../transitions/fade/fade'; +@import '../transitions/rotate/rotate'; +@import '../transitions/slide/slide'; +@import '../transitions/zoom/zoom'; @import '../../canvas_plugin_src/renderers/filters/advanced_filter/component/advanced_filter.scss'; @import '../../canvas_plugin_src/renderers/filters/dropdown_filter/component/dropdown_filter.scss'; diff --git a/x-pack/plugins/canvas/public/transitions/fade/index.ts b/x-pack/plugins/canvas/public/transitions/fade/index.ts index c8fcc574b1872..7ce717a83eeb0 100644 --- a/x-pack/plugins/canvas/public/transitions/fade/index.ts +++ b/x-pack/plugins/canvas/public/transitions/fade/index.ts @@ -5,8 +5,6 @@ * 2.0. */ -import './fade.scss'; - import { TransitionStrings } from '../../../i18n'; const { fade: strings } = TransitionStrings; diff --git a/x-pack/plugins/canvas/public/transitions/rotate/index.ts b/x-pack/plugins/canvas/public/transitions/rotate/index.ts index 217fd26680959..959e1ae248f2a 100644 --- a/x-pack/plugins/canvas/public/transitions/rotate/index.ts +++ b/x-pack/plugins/canvas/public/transitions/rotate/index.ts @@ -5,8 +5,6 @@ * 2.0. */ -import './rotate.scss'; - import { TransitionStrings } from '../../../i18n'; const { rotate: strings } = TransitionStrings; diff --git a/x-pack/plugins/canvas/public/transitions/slide/index.ts b/x-pack/plugins/canvas/public/transitions/slide/index.ts index 0c3f82a09dd02..1cf87acca2963 100644 --- a/x-pack/plugins/canvas/public/transitions/slide/index.ts +++ b/x-pack/plugins/canvas/public/transitions/slide/index.ts @@ -5,8 +5,6 @@ * 2.0. */ -import './slide.scss'; - import { TransitionStrings } from '../../../i18n'; const { slide: strings } = TransitionStrings; diff --git a/x-pack/plugins/canvas/public/transitions/zoom/index.ts b/x-pack/plugins/canvas/public/transitions/zoom/index.ts index c7c1db25bd0d7..c102935b4118b 100644 --- a/x-pack/plugins/canvas/public/transitions/zoom/index.ts +++ b/x-pack/plugins/canvas/public/transitions/zoom/index.ts @@ -5,8 +5,6 @@ * 2.0. */ -import './zoom.scss'; - import { TransitionStrings } from '../../../i18n'; const { zoom: strings } = TransitionStrings; From 3838bfd34a68c6e3ef5757441786b0e19c96773d Mon Sep 17 00:00:00 2001 From: Scotty Bollinger Date: Thu, 24 Jun 2021 18:53:03 -0500 Subject: [PATCH 63/69] [Enterprise Search] Add notices for deactivated users and SMTP callout (#103285) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Port #3904 to Kibana https://github.com/elastic/ent-search/pull/3904 * DRY out logic interfaces Should have done this long ago * Port #3920 to Kibana https://github.com/elastic/ent-search/pull/3920 * Lint fixes * Remove error state from form We already did this for the users flyout. Basically changes the dirty state of the form from an error state to just showing “Required”. i18n had not been translated yet for `ATTRIBUTE_VALUE_ERROR` * Add loading states * Remove manual disabling of button Co-authored-by: Constance * Remove manual disabling of other button * Lint fixes Co-authored-by: Constance --- .../components/role_mappings/role_mapping.tsx | 2 + .../role_mappings/role_mappings_logic.test.ts | 3 + .../role_mappings/role_mappings_logic.ts | 86 ++++++------------- .../components/role_mappings/user.test.tsx | 24 +++++- .../components/role_mappings/user.tsx | 11 +++ .../__mocks__/elasticsearch_users.ts | 1 + .../role_mapping/attribute_selector.test.tsx | 11 ++- .../role_mapping/attribute_selector.tsx | 5 +- .../shared/role_mapping/constants.ts | 40 +++++++-- .../deactivated_user_callout.test.tsx | 75 ++++++++++++++++ .../role_mapping/deactivated_user_callout.tsx | 28 ++++++ .../applications/shared/role_mapping/index.ts | 2 + .../role_mapping/role_mapping_flyout.test.tsx | 1 + .../role_mapping/role_mapping_flyout.tsx | 3 + .../applications/shared/role_mapping/types.ts | 66 ++++++++++++++ .../shared/role_mapping/user_flyout.test.tsx | 1 + .../shared/role_mapping/user_flyout.tsx | 4 +- .../role_mapping/user_selector.test.tsx | 3 +- .../shared/role_mapping/user_selector.tsx | 18 +++- .../shared/role_mapping/users_table.test.tsx | 20 ++++- .../shared/role_mapping/users_table.tsx | 10 ++- .../public/applications/shared/types.ts | 3 +- .../views/role_mappings/role_mapping.tsx | 2 + .../role_mappings/role_mappings_logic.test.ts | 3 + .../role_mappings/role_mappings_logic.ts | 84 ++++++------------ .../views/role_mappings/user.test.tsx | 24 +++++- .../views/role_mappings/user.tsx | 11 +++ 27 files changed, 406 insertions(+), 135 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/deactivated_user_callout.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/deactivated_user_callout.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/types.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mapping.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mapping.tsx index dbebd8e46a219..02ff4c0ba1c3d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mapping.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mapping.tsx @@ -45,6 +45,7 @@ export const RoleMapping: React.FC = () => { selectedEngines, selectedAuthProviders, roleMappingErrors, + formLoading, } = useValues(RoleMappingsLogic); const isNew = !roleMapping; @@ -67,6 +68,7 @@ export const RoleMapping: React.FC = () => { return ( { userCreated: false, userFormIsNewUser: true, userFormUserIsExisting: true, + smtpSettingsPresent: false, + formLoading: false, }; const mappingsServerProps = { @@ -70,6 +72,7 @@ describe('RoleMappingsLogic', () => { hasAdvancedRoles: false, singleUserRoleMappings: [asSingleUserRoleMapping], elasticsearchUsers, + smtpSettingsPresent: false, }; beforeEach(() => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.ts index 0b57e1d08a294..fb574a3588989 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.ts @@ -7,14 +7,17 @@ import { kea, MakeLogicType } from 'kea'; -import { EuiComboBoxOptionOption } from '@elastic/eui'; - import { clearFlashMessages, flashAPIErrors, setSuccessMessage, } from '../../../shared/flash_messages'; import { HttpLogic } from '../../../shared/http'; +import { + RoleMappingsBaseServerDetails, + RoleMappingsBaseActions, + RoleMappingsBaseValues, +} from '../../../shared/role_mapping'; import { ANY_AUTH_PROVIDER } from '../../../shared/role_mapping/constants'; import { AttributeName, SingleUserRoleMapping, ElasticsearchUser } from '../../../shared/types'; import { ASRoleMapping, RoleTypes } from '../../types'; @@ -29,16 +32,11 @@ import { type UserMapping = SingleUserRoleMapping; -interface RoleMappingsServerDetails { +interface RoleMappingsServerDetails extends RoleMappingsBaseServerDetails { roleMappings: ASRoleMapping[]; - attributes: string[]; - authProviders: string[]; availableEngines: Engine[]; - elasticsearchRoles: string[]; - elasticsearchUsers: ElasticsearchUser[]; - hasAdvancedRoles: boolean; - multipleAuthProvidersConfig: boolean; singleUserRoleMappings: UserMapping[]; + hasAdvancedRoles: boolean; } const getFirstAttributeName = (roleMapping: ASRoleMapping) => @@ -47,24 +45,7 @@ const getFirstAttributeValue = (roleMapping: ASRoleMapping) => Object.entries(roleMapping.rules)[0][1] as AttributeName; const emptyUser = { username: '', email: '' } as ElasticsearchUser; -interface RoleMappingsActions { - handleAccessAllEnginesChange(selected: boolean): { selected: boolean }; - handleAuthProviderChange(value: string[]): { value: string[] }; - handleAttributeSelectorChange( - value: AttributeName, - firstElasticsearchRole: string - ): { value: AttributeName; firstElasticsearchRole: string }; - handleAttributeValueChange(value: string): { value: string }; - handleDeleteMapping(roleMappingId: string): { roleMappingId: string }; - handleEngineSelectionChange(engineNames: string[]): { engineNames: string[] }; - handleRoleChange(roleType: RoleTypes): { roleType: RoleTypes }; - handleUsernameSelectChange(username: string): { username: string }; - handleSaveMapping(): void; - handleSaveUser(): void; - initializeRoleMapping(roleMappingId?: string): { roleMappingId?: string }; - initializeSingleUserRoleMapping(roleMappingId?: string): { roleMappingId?: string }; - initializeRoleMappings(): void; - resetState(): void; +interface RoleMappingsActions extends RoleMappingsBaseActions { setRoleMapping(roleMapping: ASRoleMapping): { roleMapping: ASRoleMapping }; setSingleUserRoleMapping(data?: UserMapping): { singleUserRoleMapping: UserMapping }; setRoleMappings({ @@ -73,34 +54,14 @@ interface RoleMappingsActions { roleMappings: ASRoleMapping[]; }): { roleMappings: ASRoleMapping[] }; setRoleMappingsData(data: RoleMappingsServerDetails): RoleMappingsServerDetails; - setElasticsearchUser( - elasticsearchUser?: ElasticsearchUser - ): { elasticsearchUser: ElasticsearchUser }; - openRoleMappingFlyout(): void; - openSingleUserRoleMappingFlyout(): void; - closeUsersAndRolesFlyout(): void; - setRoleMappingErrors(errors: string[]): { errors: string[] }; - enableRoleBasedAccess(): void; - setUserExistingRadioValue(userFormUserIsExisting: boolean): { userFormUserIsExisting: boolean }; - setElasticsearchUsernameValue(username: string): { username: string }; - setElasticsearchEmailValue(email: string): { email: string }; - setUserCreated(): void; - setUserFormIsNewUser(userFormIsNewUser: boolean): { userFormIsNewUser: boolean }; + handleAccessAllEnginesChange(selected: boolean): { selected: boolean }; + handleEngineSelectionChange(engineNames: string[]): { engineNames: string[] }; + handleRoleChange(roleType: RoleTypes): { roleType: RoleTypes }; } -interface RoleMappingsValues { +interface RoleMappingsValues extends RoleMappingsBaseValues { accessAllEngines: boolean; - attributeName: AttributeName; - attributeValue: string; - attributes: string[]; - availableAuthProviders: string[]; availableEngines: Engine[]; - dataLoading: boolean; - elasticsearchRoles: string[]; - elasticsearchUsers: ElasticsearchUser[]; - elasticsearchUser: ElasticsearchUser; - hasAdvancedRoles: boolean; - multipleAuthProvidersConfig: boolean; roleMapping: ASRoleMapping | null; roleMappings: ASRoleMapping[]; singleUserRoleMapping: UserMapping | null; @@ -108,13 +69,7 @@ interface RoleMappingsValues { roleType: RoleTypes; selectedAuthProviders: string[]; selectedEngines: Set; - roleMappingFlyoutOpen: boolean; - singleUserRoleMappingFlyoutOpen: boolean; - selectedOptions: EuiComboBoxOptionOption[]; - roleMappingErrors: string[]; - userFormUserIsExisting: boolean; - userCreated: boolean; - userFormIsNewUser: boolean; + hasAdvancedRoles: boolean; } export const RoleMappingsLogic = kea>({ @@ -369,6 +324,21 @@ export const RoleMappingsLogic = kea userFormIsNewUser, }, ], + smtpSettingsPresent: [ + false, + { + setRoleMappingsData: (_, { smtpSettingsPresent }) => smtpSettingsPresent, + }, + ], + formLoading: [ + false, + { + handleSaveMapping: () => true, + handleSaveUser: () => true, + initializeRoleMappings: () => false, + setRoleMappingErrors: () => false, + }, + ], }, selectors: ({ selectors }) => ({ selectedOptions: [ diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/user.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/user.test.tsx index 88103532bd149..cec7f1541a31a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/user.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/user.test.tsx @@ -14,7 +14,12 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { UserFlyout, UserAddedInfo, UserInvitationCallout } from '../../../shared/role_mapping'; +import { + UserFlyout, + UserAddedInfo, + UserInvitationCallout, + DeactivatedUserCallout, +} from '../../../shared/role_mapping'; import { elasticsearchUsers } from '../../../shared/role_mapping/__mocks__/elasticsearch_users'; import { wsSingleUserRoleMapping } from '../../../shared/role_mapping/__mocks__/roles'; @@ -91,6 +96,23 @@ describe('User', () => { expect(wrapper.find(UserAddedInfo)).toHaveLength(1); }); + it('renders DeactivatedUserCallout', () => { + setMockValues({ + ...mockValues, + singleUserRoleMapping: { + ...wsSingleUserRoleMapping, + invitation: null, + elasticsearchUser: { + ...wsSingleUserRoleMapping.elasticsearchUser, + enabled: false, + }, + }, + }); + const wrapper = shallow(); + + expect(wrapper.find(DeactivatedUserCallout)).toHaveLength(1); + }); + it('disables form when username value not present', () => { setMockValues({ ...mockValues, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/user.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/user.tsx index df231fac64df7..018d29706b05f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/user.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/user.tsx @@ -17,6 +17,7 @@ import { UserSelector, UserAddedInfo, UserInvitationCallout, + DeactivatedUserCallout, } from '../../../shared/role_mapping'; import { RoleTypes } from '../../types'; @@ -48,6 +49,8 @@ export const User: React.FC = () => { roleMappingErrors, userCreated, userFormIsNewUser, + smtpSettingsPresent, + formLoading, } = useValues(RoleMappingsLogic); const roleTypes = hasAdvancedRoles ? [...standardRoles, ...advancedRoles] : standardRoles; @@ -55,6 +58,11 @@ export const User: React.FC = () => { const showEngineAssignmentSelector = hasEngines && hasAdvancedRoles; const flyoutDisabled = !userFormUserIsExisting && (!elasticsearchUser.email || !elasticsearchUser.username); + const userIsDeactivated = !!( + singleUserRoleMapping && + !singleUserRoleMapping.invitation && + !singleUserRoleMapping.elasticsearchUser.enabled + ); const userAddedInfo = singleUserRoleMapping && ( { 0} error={roleMappingErrors}> { return ( { > {userCreated ? userAddedInfo : createUserForm} {userInvitationCallout} + {userIsDeactivated && } ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/__mocks__/elasticsearch_users.ts b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/__mocks__/elasticsearch_users.ts index 500f560675679..6d9365d63c320 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/__mocks__/elasticsearch_users.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/__mocks__/elasticsearch_users.ts @@ -9,5 +9,6 @@ export const elasticsearchUsers = [ { email: 'user1@user.com', username: 'user1', + enabled: true, }, ]; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/attribute_selector.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/attribute_selector.test.tsx index d19d090b6e706..8fed459b0a8dc 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/attribute_selector.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/attribute_selector.test.tsx @@ -9,12 +9,12 @@ import React from 'react'; import { shallow, ShallowWrapper } from 'enzyme'; -import { EuiComboBox, EuiFieldText } from '@elastic/eui'; +import { EuiComboBox, EuiFieldText, EuiFormRow } from '@elastic/eui'; import { AttributeName } from '../types'; import { AttributeSelector } from './attribute_selector'; -import { ANY_AUTH_PROVIDER, ANY_AUTH_PROVIDER_OPTION_LABEL } from './constants'; +import { ANY_AUTH_PROVIDER, ANY_AUTH_PROVIDER_OPTION_LABEL, REQUIRED_LABEL } from './constants'; const handleAttributeSelectorChange = jest.fn(); const handleAttributeValueChange = jest.fn(); @@ -166,5 +166,12 @@ describe('AttributeSelector', () => { baseProps.elasticsearchRoles[0] ); }); + + it('shows helpText when attributeValueInvalid', () => { + const wrapper = shallow(); + const formRow = wrapper.find(EuiFormRow).at(2); + + expect(formRow.prop('helpText')).toEqual(REQUIRED_LABEL); + }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/attribute_selector.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/attribute_selector.tsx index bedb6b3df90f2..e24bc03bea452 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/attribute_selector.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/attribute_selector.tsx @@ -21,7 +21,7 @@ import { ANY_AUTH_PROVIDER, ANY_AUTH_PROVIDER_OPTION_LABEL, ATTRIBUTE_VALUE_LABEL, - ATTRIBUTE_VALUE_ERROR, + REQUIRED_LABEL, AUTH_ANY_PROVIDER_LABEL, AUTH_INDIVIDUAL_PROVIDER_LABEL, AUTH_PROVIDER_LABEL, @@ -129,8 +129,7 @@ export const AttributeSelector: React.FC = ({ {attributeName === 'role' ? ( { + it('renders with new', () => { + const wrapper = shallow(); + + expect(wrapper).toMatchInlineSnapshot(` + + + + + + User deactivated + + + + + This user is not currently active, and access has been temporarily revoked. Users can be re-activated via the User Management area of the Kibana console. + + + + `); + }); + + it('renders with existing', () => { + const wrapper = shallow(); + + expect(wrapper).toMatchInlineSnapshot(` + + + + + + + User deactivated + + + + + This user is not currently active, and access has been temporarily revoked. Users can be re-activated via the User Management area of the Kibana console. + + + + `); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/deactivated_user_callout.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/deactivated_user_callout.tsx new file mode 100644 index 0000000000000..5b69420d169ce --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/deactivated_user_callout.tsx @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { EuiSpacer, EuiText, EuiIcon } from '@elastic/eui'; + +interface Props { + isNew: boolean; +} + +import { DEACTIVATED_USER_CALLOUT_LABEL, DEACTIVATED_USER_CALLOUT_DESCRIPTION } from './constants'; + +export const DeactivatedUserCallout: React.FC = ({ isNew }) => ( + <> + {!isNew && } + + {DEACTIVATED_USER_CALLOUT_LABEL} + + + {DEACTIVATED_USER_CALLOUT_DESCRIPTION} + + +); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/index.ts index 8096b86939ff3..c10fc5c9d8242 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/index.ts @@ -5,7 +5,9 @@ * 2.0. */ +export * from './types'; export { AttributeSelector } from './attribute_selector'; +export { DeactivatedUserCallout } from './deactivated_user_callout'; export { RolesEmptyPrompt } from './roles_empty_prompt'; export { RoleMappingsTable } from './role_mappings_table'; export { RoleOptionLabel } from './role_option_label'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mapping_flyout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mapping_flyout.test.tsx index ffcf5508233fc..651a46f5df85e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mapping_flyout.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mapping_flyout.test.tsx @@ -26,6 +26,7 @@ describe('RoleMappingFlyout', () => { const props = { isNew: true, disabled: false, + formLoading: false, closeUsersAndRolesFlyout, handleSaveMapping, }; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mapping_flyout.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mapping_flyout.tsx index 4416a2de28011..bcaf26ccf2cfc 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mapping_flyout.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mapping_flyout.tsx @@ -36,6 +36,7 @@ interface Props { children: React.ReactNode; isNew: boolean; disabled: boolean; + formLoading: boolean; closeUsersAndRolesFlyout(): void; handleSaveMapping(): void; } @@ -44,6 +45,7 @@ export const RoleMappingFlyout: React.FC = ({ children, isNew, disabled, + formLoading, closeUsersAndRolesFlyout, handleSaveMapping, }) => ( @@ -78,6 +80,7 @@ export const RoleMappingFlyout: React.FC = ({ { isNew: true, isComplete: false, disabled: false, + formLoading: false, closeUserFlyout, handleSaveUser, }; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_flyout.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_flyout.tsx index a3be5e295ddfe..8741a2b4517d3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_flyout.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_flyout.tsx @@ -28,6 +28,7 @@ interface Props { isNew: boolean; isComplete: boolean; disabled: boolean; + formLoading: boolean; closeUserFlyout(): void; handleSaveUser(): void; } @@ -49,6 +50,7 @@ export const UserFlyout: React.FC = ({ isNew, isComplete, disabled, + formLoading, closeUserFlyout, handleSaveUser, }) => { @@ -75,7 +77,7 @@ export const UserFlyout: React.FC = ({ {CANCEL_BUTTON_LABEL} - + {isNew ? ADD_USER_LABEL : UPDATE_USER_LABEL} diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_selector.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_selector.test.tsx index 60bac97d09835..0aea55a51040c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_selector.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_selector.test.tsx @@ -34,6 +34,7 @@ describe('UserSelector', () => { const props = { isNewUser: true, + smtpSettingsPresent: false, userFormUserIsExisting: true, elasticsearchUsers, elasticsearchUser: elasticsearchUsers[0], @@ -101,7 +102,7 @@ describe('UserSelector', () => { {...props} userFormUserIsExisting={false} elasticsearchUsers={[]} - elasticsearchUser={{ email: '', username: '' }} + elasticsearchUser={{ email: '', username: '', enabled: true }} /> ); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_selector.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_selector.tsx index d65f97265f6a3..25aff5077c680 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_selector.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_selector.tsx @@ -9,6 +9,7 @@ import React from 'react'; import { EuiFieldText, + EuiLink, EuiRadio, EuiFormRow, EuiSelect, @@ -21,6 +22,9 @@ import { ElasticsearchUser } from '../../shared/types'; import { Role as WSRole } from '../../workplace_search/types'; import { USERNAME_LABEL, EMAIL_LABEL } from '../constants'; +import { docLinks } from '../doc_links'; + +const SMTP_URL = `${docLinks.enterpriseSearchBase}/mailer-configuration.html`; import { NEW_USER_LABEL, @@ -28,12 +32,15 @@ import { USERNAME_NO_USERS_TEXT, REQUIRED_LABEL, ROLE_LABEL, + SMTP_CALLOUT_LABEL, + SMTP_LINK_LABEL, } from './constants'; type SharedRole = WSRole | ASRole; interface Props { isNewUser: boolean; + smtpSettingsPresent: boolean; userFormUserIsExisting: boolean; elasticsearchUsers: ElasticsearchUser[]; elasticsearchUser: ElasticsearchUser; @@ -48,6 +55,7 @@ interface Props { export const UserSelector: React.FC = ({ isNewUser, + smtpSettingsPresent, userFormUserIsExisting, elasticsearchUsers, elasticsearchUser, @@ -66,6 +74,14 @@ export const UserSelector: React.FC = ({ })); const hasElasticsearchUsers = elasticsearchUsers.length > 0; const showNewUserExistingUserControls = userFormUserIsExisting && hasElasticsearchUsers; + const smptHelpText = !smtpSettingsPresent && ( + <> + {SMTP_CALLOUT_LABEL}{' '} + + {SMTP_LINK_LABEL} + + + ); const roleSelect = ( @@ -80,7 +96,7 @@ export const UserSelector: React.FC = ({ ); const emailInput = ( - + { singleUserRoleMappings: [wsSingleUserRoleMapping], initializeSingleUserRoleMapping, handleDeleteMapping, + enabled: true, }; it('renders', () => { @@ -55,6 +56,7 @@ describe('UsersTable', () => { elasticsearchUser: { email: null, username: 'foo', + enabled: true, }, }; const wrapper = mount(); @@ -97,4 +99,20 @@ describe('UsersTable', () => { `${engines[0].name}, ${engines[1].name} + 1` ); }); + + it('renders deactivatedBadge', () => { + const disabledUser = { + ...wsSingleUserRoleMapping, + elasticsearchUser: { + email: 'email@user.com', + username: 'foo', + enabled: false, + }, + invitation: null, + }; + const wrapper = mount(); + const cell = wrapper.find('[data-test-subj="UsernameCell"]'); + + expect(cell.find(EuiBadge)).toHaveLength(1); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_table.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_table.tsx index 674796775b1d3..25a9eee38f93f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_table.tsx @@ -15,6 +15,7 @@ import { WSRoleMapping } from '../../workplace_search/types'; import { INVITATION_PENDING_LABEL, + DEACTIVATED_LABEL, ALL_LABEL, FILTER_USERS_LABEL, NO_USERS_LABEL, @@ -37,6 +38,7 @@ interface SharedUser extends SingleUserRoleMapping—; const invitationBadge = {INVITATION_PENDING_LABEL}; +const deactivatedBadge = {DEACTIVATED_LABEL}; export const UsersTable: React.FC = ({ accessItemKey, @@ -63,6 +66,7 @@ export const UsersTable: React.FC = ({ const users = ((singleUserRoleMappings as SharedUser[]).map((user) => ({ username: user.elasticsearchUser.username, email: user.elasticsearchUser.email, + enabled: user.elasticsearchUser.enabled, roleType: user.roleMapping.roleType, id: user.roleMapping.id, accessItems: (user.roleMapping as SharedRoleMapping)[accessItemKey], @@ -73,7 +77,11 @@ export const UsersTable: React.FC = ({ { field: 'username', name: USERNAME_LABEL, - render: (_, { username }: SharedUser) => username, + render: (_, { username, invitation, enabled }: SharedUser) => ( +
    + {username} {!invitation && !enabled && deactivatedBadge} +
    + ), }, { field: 'email', diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/types.ts b/x-pack/plugins/enterprise_search/public/applications/shared/types.ts index e6d2c67d1baf8..5a90dd2c4a6bb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/types.ts @@ -49,10 +49,11 @@ export interface Invitation { export interface ElasticsearchUser { email: string | null; username: string; + enabled: boolean; } export interface SingleUserRoleMapping { - invitation: Invitation; + invitation: Invitation | null; elasticsearchUser: ElasticsearchUser; roleMapping: T; } diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mapping.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mapping.tsx index 20211d40d7010..734716af9f627 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mapping.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mapping.tsx @@ -59,6 +59,7 @@ export const RoleMapping: React.FC = () => { selectedAuthProviders, roleMapping, roleMappingErrors, + formLoading, } = useValues(RoleMappingsLogic); const isNew = !roleMapping; @@ -69,6 +70,7 @@ export const RoleMapping: React.FC = () => { return ( { userCreated: false, userFormIsNewUser: true, userFormUserIsExisting: true, + smtpSettingsPresent: false, + formLoading: false, }; const roleGroup = { id: '123', @@ -76,6 +78,7 @@ describe('RoleMappingsLogic', () => { elasticsearchRoles: [], singleUserRoleMappings: [wsSingleUserRoleMapping], elasticsearchUsers, + smtpSettingsPresent: false, }; beforeEach(() => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.ts index 7f26c8738786c..6e7104964cdb7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.ts @@ -7,14 +7,17 @@ import { kea, MakeLogicType } from 'kea'; -import { EuiComboBoxOptionOption } from '@elastic/eui'; - import { clearFlashMessages, flashAPIErrors, setSuccessMessage, } from '../../../shared/flash_messages'; import { HttpLogic } from '../../../shared/http'; +import { + RoleMappingsBaseServerDetails, + RoleMappingsBaseActions, + RoleMappingsBaseValues, +} from '../../../shared/role_mapping'; import { ANY_AUTH_PROVIDER } from '../../../shared/role_mapping/constants'; import { AttributeName, SingleUserRoleMapping, ElasticsearchUser } from '../../../shared/types'; import { RoleGroup, WSRoleMapping, Role } from '../../types'; @@ -28,14 +31,9 @@ import { type UserMapping = SingleUserRoleMapping; -interface RoleMappingsServerDetails { +interface RoleMappingsServerDetails extends RoleMappingsBaseServerDetails { roleMappings: WSRoleMapping[]; - attributes: string[]; - authProviders: string[]; availableGroups: RoleGroup[]; - elasticsearchUsers: ElasticsearchUser[]; - elasticsearchRoles: string[]; - multipleAuthProvidersConfig: boolean; singleUserRoleMappings: UserMapping[]; } @@ -45,24 +43,8 @@ const getFirstAttributeValue = (roleMapping: WSRoleMapping): string => Object.entries(roleMapping.rules)[0][1] as string; const emptyUser = { username: '', email: '' } as ElasticsearchUser; -interface RoleMappingsActions { - handleAllGroupsSelectionChange(selected: boolean): { selected: boolean }; - handleAuthProviderChange(value: string[]): { value: string[] }; - handleAttributeSelectorChange( - value: AttributeName, - firstElasticsearchRole: string - ): { value: AttributeName; firstElasticsearchRole: string }; - handleAttributeValueChange(value: string): { value: string }; - handleDeleteMapping(roleMappingId: string): { roleMappingId: string }; - handleGroupSelectionChange(groupIds: string[]): { groupIds: string[] }; - handleRoleChange(roleType: Role): { roleType: Role }; - handleUsernameSelectChange(username: string): { username: string }; - handleSaveMapping(): void; - handleSaveUser(): void; - initializeRoleMapping(roleMappingId?: string): { roleMappingId?: string }; - initializeSingleUserRoleMapping(roleMappingId?: string): { roleMappingId?: string }; - initializeRoleMappings(): void; - resetState(): void; +interface RoleMappingsActions extends RoleMappingsBaseActions { + setDefaultGroup(availableGroups: RoleGroup[]): { availableGroups: RoleGroup[] }; setRoleMapping(roleMapping: WSRoleMapping): { roleMapping: WSRoleMapping }; setSingleUserRoleMapping(data?: UserMapping): { singleUserRoleMapping: UserMapping }; setRoleMappings({ @@ -71,34 +53,14 @@ interface RoleMappingsActions { roleMappings: WSRoleMapping[]; }): { roleMappings: WSRoleMapping[] }; setRoleMappingsData(data: RoleMappingsServerDetails): RoleMappingsServerDetails; - setElasticsearchUser( - elasticsearchUser?: ElasticsearchUser - ): { elasticsearchUser: ElasticsearchUser }; - setDefaultGroup(availableGroups: RoleGroup[]): { availableGroups: RoleGroup[] }; - openRoleMappingFlyout(): void; - openSingleUserRoleMappingFlyout(): void; - closeUsersAndRolesFlyout(): void; - setRoleMappingErrors(errors: string[]): { errors: string[] }; - enableRoleBasedAccess(): void; - setUserExistingRadioValue(userFormUserIsExisting: boolean): { userFormUserIsExisting: boolean }; - setElasticsearchUsernameValue(username: string): { username: string }; - setElasticsearchEmailValue(email: string): { email: string }; - setUserCreated(): void; - setUserFormIsNewUser(userFormIsNewUser: boolean): { userFormIsNewUser: boolean }; + handleAllGroupsSelectionChange(selected: boolean): { selected: boolean }; + handleGroupSelectionChange(groupIds: string[]): { groupIds: string[] }; + handleRoleChange(roleType: Role): { roleType: Role }; } -interface RoleMappingsValues { +interface RoleMappingsValues extends RoleMappingsBaseValues { includeInAllGroups: boolean; - attributeName: AttributeName; - attributeValue: string; - attributes: string[]; - availableAuthProviders: string[]; availableGroups: RoleGroup[]; - dataLoading: boolean; - elasticsearchRoles: string[]; - elasticsearchUsers: ElasticsearchUser[]; - elasticsearchUser: ElasticsearchUser; - multipleAuthProvidersConfig: boolean; roleMapping: WSRoleMapping | null; roleMappings: WSRoleMapping[]; singleUserRoleMapping: UserMapping | null; @@ -106,13 +68,6 @@ interface RoleMappingsValues { roleType: Role; selectedAuthProviders: string[]; selectedGroups: Set; - roleMappingFlyoutOpen: boolean; - singleUserRoleMappingFlyoutOpen: boolean; - selectedOptions: EuiComboBoxOptionOption[]; - roleMappingErrors: string[]; - userFormUserIsExisting: boolean; - userCreated: boolean; - userFormIsNewUser: boolean; } export const RoleMappingsLogic = kea>({ @@ -369,6 +324,21 @@ export const RoleMappingsLogic = kea userFormIsNewUser, }, ], + smtpSettingsPresent: [ + false, + { + setRoleMappingsData: (_, { smtpSettingsPresent }) => smtpSettingsPresent, + }, + ], + formLoading: [ + false, + { + handleSaveMapping: () => true, + handleSaveUser: () => true, + initializeRoleMappings: () => false, + setRoleMappingErrors: () => false, + }, + ], }, selectors: ({ selectors }) => ({ selectedOptions: [ diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/user.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/user.test.tsx index 32ee1a7f22875..d8e1fc160901f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/user.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/user.test.tsx @@ -14,7 +14,12 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { UserFlyout, UserAddedInfo, UserInvitationCallout } from '../../../shared/role_mapping'; +import { + UserFlyout, + UserAddedInfo, + UserInvitationCallout, + DeactivatedUserCallout, +} from '../../../shared/role_mapping'; import { elasticsearchUsers } from '../../../shared/role_mapping/__mocks__/elasticsearch_users'; import { wsSingleUserRoleMapping } from '../../../shared/role_mapping/__mocks__/roles'; @@ -90,6 +95,23 @@ describe('User', () => { expect(wrapper.find(UserAddedInfo)).toHaveLength(1); }); + it('renders DeactivatedUserCallout', () => { + setMockValues({ + ...mockValues, + singleUserRoleMapping: { + ...wsSingleUserRoleMapping, + invitation: null, + elasticsearchUser: { + ...wsSingleUserRoleMapping.elasticsearchUser, + enabled: false, + }, + }, + }); + const wrapper = shallow(); + + expect(wrapper.find(DeactivatedUserCallout)).toHaveLength(1); + }); + it('disables form when username value not present', () => { setMockValues({ ...mockValues, diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/user.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/user.tsx index bfb32ee31c121..6447f43e6ed4d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/user.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/user.tsx @@ -17,6 +17,7 @@ import { UserSelector, UserAddedInfo, UserInvitationCallout, + DeactivatedUserCallout, } from '../../../shared/role_mapping'; import { Role } from '../../types'; @@ -46,12 +47,19 @@ export const User: React.FC = () => { roleMappingErrors, userCreated, userFormIsNewUser, + smtpSettingsPresent, + formLoading, } = useValues(RoleMappingsLogic); const showGroupAssignmentSelector = availableGroups.length > 0; const hasAvailableUsers = elasticsearchUsers.length > 0; const flyoutDisabled = (!userFormUserIsExisting || !hasAvailableUsers) && !elasticsearchUser.username; + const userIsDeactivated = !!( + singleUserRoleMapping && + !singleUserRoleMapping.invitation && + !singleUserRoleMapping.elasticsearchUser.enabled + ); const userAddedInfo = singleUserRoleMapping && ( { 0} error={roleMappingErrors}> { return ( { > {userCreated ? userAddedInfo : createUserForm} {userInvitationCallout} + {userIsDeactivated && } ); }; From 41b015abc90f89d7b0b3466ba4bdbd3d9092a388 Mon Sep 17 00:00:00 2001 From: Kevin Logan <56395104+kevinlog@users.noreply.github.com> Date: Thu, 24 Jun 2021 20:23:13 -0400 Subject: [PATCH 64/69] [Security Solution] Correct linux OS lookup for Endpoint Exceptions (#103038) --- .../exceptionable_endpoint_fields.json | 20 ---- .../exceptionable_windows_mac_fields.json | 18 +++- .../components/exceptions/helpers.test.tsx | 11 --- .../common/components/exceptions/helpers.tsx | 99 +++++++++++-------- 4 files changed, 76 insertions(+), 72 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/exceptionable_endpoint_fields.json b/x-pack/plugins/security_solution/public/common/components/exceptions/exceptionable_endpoint_fields.json index d5134945441f5..b5480aac27f67 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/exceptionable_endpoint_fields.json +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/exceptionable_endpoint_fields.json @@ -1,19 +1,11 @@ [ "Endpoint.policy.applied.id", - "Target.process.Ext.code_signature.status", - "Target.process.Ext.code_signature.subject_name", - "Target.process.Ext.code_signature.trusted", - "Target.process.Ext.code_signature.valid", "Target.process.Ext.services", "Target.process.Ext.user", "Target.process.hash.md5", "Target.process.hash.sha1", "Target.process.hash.sha256", "Target.process.hash.sha512", - "Target.process.parent.Ext.code_signature.status", - "Target.process.parent.Ext.code_signature.subject_name", - "Target.process.parent.Ext.code_signature.trusted", - "Target.process.parent.Ext.code_signature.valid", "Target.process.parent.hash.md5", "Target.process.parent.hash.sha1", "Target.process.parent.hash.sha256", @@ -38,10 +30,6 @@ "event.outcome", "event.provider", "event.type", - "file.Ext.code_signature.status", - "file.Ext.code_signature.subject_name", - "file.Ext.code_signature.trusted", - "file.Ext.code_signature.valid", "file.attributes", "file.device", "file.directory", @@ -78,20 +66,12 @@ "host.os.platform", "host.os.version", "host.type", - "process.Ext.code_signature.status", - "process.Ext.code_signature.subject_name", - "process.Ext.code_signature.trusted", - "process.Ext.code_signature.valid", "process.Ext.services", "process.Ext.user", "process.hash.md5", "process.hash.sha1", "process.hash.sha256", "process.hash.sha512", - "process.parent.Ext.code_signature.status", - "process.parent.Ext.code_signature.subject_name", - "process.parent.Ext.code_signature.trusted", - "process.parent.Ext.code_signature.valid", "process.parent.hash.md5", "process.parent.hash.sha1", "process.parent.hash.sha256", diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/exceptionable_windows_mac_fields.json b/x-pack/plugins/security_solution/public/common/components/exceptions/exceptionable_windows_mac_fields.json index 31784bb9c764a..dc21434f96b5a 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/exceptionable_windows_mac_fields.json +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/exceptionable_windows_mac_fields.json @@ -18,5 +18,21 @@ "process.parent.executable.caseless", "process.parent.name.caseless", "process.parent.working_directory.caseless", - "process.working_directory.caseless" + "process.working_directory.caseless", + "Target.process.Ext.code_signature.status", + "Target.process.Ext.code_signature.subject_name", + "Target.process.Ext.code_signature.trusted", + "Target.process.Ext.code_signature.valid", + "Target.process.parent.Ext.code_signature.status", + "Target.process.parent.Ext.code_signature.subject_name", + "Target.process.parent.Ext.code_signature.trusted", + "Target.process.parent.Ext.code_signature.valid", + "file.Ext.code_signature.status", + "file.Ext.code_signature.subject_name", + "file.Ext.code_signature.trusted", + "file.Ext.code_signature.valid", + "process.parent.Ext.code_signature.status", + "process.parent.Ext.code_signature.subject_name", + "process.parent.Ext.code_signature.trusted", + "process.parent.Ext.code_signature.valid" ] \ No newline at end of file diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx index 383b177d40c64..0af83e2cff3b5 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx @@ -87,17 +87,6 @@ const mockLinuxEndpointFields = [ aggregatable: false, readFromDocValues: false, }, - { - name: 'file.Ext.code_signature.status', - type: 'string', - esTypes: ['text'], - count: 0, - scripted: false, - searchable: true, - aggregatable: false, - readFromDocValues: false, - subType: { nested: { path: 'file.Ext.code_signature' } }, - }, ]; export const getEndpointField = (name: string) => diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx index 20413a6493661..bfb5c7298f330 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx @@ -236,7 +236,10 @@ export const enrichExceptionItemsWithOS = ( export const retrieveAlertOsTypes = (alertData?: AlertData): OsTypeArray => { const osDefaults: OsTypeArray = ['windows', 'macos']; if (alertData != null) { - const os = alertData.host && alertData.host.os && alertData.host.os.family; + const os = + alertData?.agent?.type === 'endpoint' + ? alertData.host?.os?.name?.toLowerCase() + : alertData.host?.os?.family; if (os != null) { return osType.is(os) ? [os] : osDefaults; } @@ -361,48 +364,64 @@ export const getPrepopulatedEndpointException = ({ const { file, host } = alertEcsData; const filePath = file?.path ?? ''; const sha256Hash = file?.hash?.sha256 ?? ''; - const filePathDefault = host?.os?.family === 'linux' ? 'file.path' : 'file.path.caseless'; + const isLinux = host?.os?.name === 'Linux'; + + const commonFields: Array<{ + field: string; + operator: 'excluded' | 'included'; + type: 'match'; + value: string; + }> = [ + { + field: isLinux ? 'file.path' : 'file.path.caseless', + operator: 'included', + type: 'match', + value: filePath ?? '', + }, + { + field: 'file.hash.sha256', + operator: 'included', + type: 'match', + value: sha256Hash ?? '', + }, + { + field: 'event.code', + operator: 'included', + type: 'match', + value: eventCode ?? '', + }, + ]; + const entriesToAdd = () => { + if (isLinux) { + return addIdToEntries(commonFields); + } else { + return addIdToEntries([ + { + field: 'file.Ext.code_signature', + type: 'nested', + entries: [ + { + field: 'subject_name', + operator: 'included', + type: 'match', + value: codeSignature != null ? codeSignature.subjectName : '', + }, + { + field: 'trusted', + operator: 'included', + type: 'match', + value: codeSignature != null ? codeSignature.trusted : '', + }, + ], + }, + ...commonFields, + ]); + } + }; return { ...getNewExceptionItem({ listId, namespaceType: listNamespace, ruleName }), - entries: addIdToEntries([ - { - field: 'file.Ext.code_signature', - type: 'nested', - entries: [ - { - field: 'subject_name', - operator: 'included', - type: 'match', - value: codeSignature != null ? codeSignature.subjectName : '', - }, - { - field: 'trusted', - operator: 'included', - type: 'match', - value: codeSignature != null ? codeSignature.trusted : '', - }, - ], - }, - { - field: filePathDefault, - operator: 'included', - type: 'match', - value: filePath ?? '', - }, - { - field: 'file.hash.sha256', - operator: 'included', - type: 'match', - value: sha256Hash ?? '', - }, - { - field: 'event.code', - operator: 'included', - type: 'match', - value: eventCode ?? '', - }, - ]), + entries: entriesToAdd(), }; }; From 4b20ff3bad9f73a4c08973d03295c2dfba754776 Mon Sep 17 00:00:00 2001 From: Aaron Caldwell Date: Thu, 24 Jun 2021 20:25:26 -0600 Subject: [PATCH 65/69] [Maps] Add capability to delete features from layer & index (#103145) --- x-pack/plugins/maps/common/constants.ts | 1 + .../maps/public/actions/map_actions.ts | 19 +++++++ .../layers/vector_layer/vector_layer.tsx | 6 ++ .../es_search_source/es_search_source.tsx | 7 ++- .../es_search_source/util/feature_edit.ts | 10 ++++ .../mvt_single_layer_vector_source.tsx | 4 ++ .../sources/vector_source/vector_source.tsx | 5 ++ .../classes/util/mb_filter_expressions.ts | 2 +- .../mb_map/draw_control/draw_control.tsx | 14 +++++ .../draw_feature_control.tsx | 57 ++++++++++++++++++- .../draw_feature_control/index.ts | 9 ++- .../mb_map/draw_control/draw_tooltip.tsx | 4 ++ .../feature_edit_tools/feature_edit_tools.tsx | 19 +++++++ .../server/data_indexing/indexing_routes.ts | 53 +++++++++++++++++ .../translations/translations/ja-JP.json | 2 +- .../translations/translations/zh-CN.json | 2 +- .../apis/maps/delete_feature.js | 42 ++++++++++++++ .../test/api_integration/apis/maps/index.js | 1 + .../es_archives/maps/data/data.json | 36 ++++++++++++ .../es_archives/maps/data/mappings.json | 28 +++++++++ 20 files changed, 314 insertions(+), 7 deletions(-) create mode 100644 x-pack/test/api_integration/apis/maps/delete_feature.js diff --git a/x-pack/plugins/maps/common/constants.ts b/x-pack/plugins/maps/common/constants.ts index fa065e701184e..c16579cc142f0 100644 --- a/x-pack/plugins/maps/common/constants.ts +++ b/x-pack/plugins/maps/common/constants.ts @@ -169,6 +169,7 @@ export enum DRAW_SHAPE { POINT = 'POINT', LINE = 'LINE', SIMPLE_SELECT = 'SIMPLE_SELECT', + DELETE = 'DELETE', } export const AGG_DELIMITER = '_of_'; diff --git a/x-pack/plugins/maps/public/actions/map_actions.ts b/x-pack/plugins/maps/public/actions/map_actions.ts index 9d0d27496da92..464e4dbc6d5ae 100644 --- a/x-pack/plugins/maps/public/actions/map_actions.ts +++ b/x-pack/plugins/maps/public/actions/map_actions.ts @@ -376,3 +376,22 @@ export function addNewFeatureToIndex(geometry: Geometry | Position[]) { await dispatch(syncDataForLayer(layer, true)); }; } + +export function deleteFeatureFromIndex(featureId: string) { + return async ( + dispatch: ThunkDispatch, + getState: () => MapStoreState + ) => { + const editState = getEditState(getState()); + const layerId = editState ? editState.layerId : undefined; + if (!layerId) { + return; + } + const layer = getLayerById(layerId, getState()); + if (!layer || !(layer instanceof VectorLayer)) { + return; + } + await layer.deleteFeature(featureId); + await dispatch(syncDataForLayer(layer, true)); + }; +} diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx index 49a0878ef80b2..7a6d91a71db42 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx @@ -99,6 +99,7 @@ export interface IVectorLayer extends ILayer { supportsFeatureEditing(): boolean; getLeftJoinFields(): Promise; addFeature(geometry: Geometry | Position[]): Promise; + deleteFeature(featureId: string): Promise; } export class VectorLayer extends AbstractLayer implements IVectorLayer { @@ -1156,4 +1157,9 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { const layerSource = this.getSource(); await layerSource.addFeature(geometry); } + + async deleteFeature(featureId: string) { + const layerSource = this.getSource(); + await layerSource.deleteFeature(featureId); + } } diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx index 9f7bd1260ca22..019c3c1b4943b 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx @@ -66,7 +66,7 @@ import { isValidStringConfig } from '../../util/valid_string_config'; import { TopHitsUpdateSourceEditor } from './top_hits'; import { getDocValueAndSourceFields, ScriptField } from './util/get_docvalue_source_fields'; import { ITiledSingleLayerMvtParams } from '../tiled_single_layer_vector_source/tiled_single_layer_vector_source'; -import { addFeatureToIndex, getMatchingIndexes } from './util/feature_edit'; +import { addFeatureToIndex, deleteFeatureFromIndex, getMatchingIndexes } from './util/feature_edit'; export function timerangeToTimeextent(timerange: TimeRange): Timeslice | undefined { const timeRangeBounds = getTimeFilter().calculateBounds(timerange); @@ -716,6 +716,11 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye await addFeatureToIndex(indexPattern.title, geometry, this.getGeoFieldName()); } + async deleteFeature(featureId: string) { + const indexPattern = await this.getIndexPattern(); + await deleteFeatureFromIndex(indexPattern.title, featureId); + } + async getUrlTemplateWithMeta( searchFilters: VectorSourceRequestMeta ): Promise { diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/util/feature_edit.ts b/x-pack/plugins/maps/public/classes/sources/es_search_source/util/feature_edit.ts index ac8e2ba42f282..f306a225df69a 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/util/feature_edit.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/util/feature_edit.ts @@ -26,6 +26,16 @@ export const addFeatureToIndex = async ( }); }; +export const deleteFeatureFromIndex = async (indexName: string, featureId: string) => { + return await getHttp().fetch({ + path: `${INDEX_FEATURE_PATH}/${featureId}`, + method: 'DELETE', + body: JSON.stringify({ + index: indexName, + }), + }); +}; + export const getMatchingIndexes = async (indexPattern: string) => { return await getHttp().fetch({ path: `${GET_MATCHING_INDEXES_PATH}/${indexPattern}`, diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx index 5bf7a2e47cc66..f825a85f50bbd 100644 --- a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx @@ -102,6 +102,10 @@ export class MVTSingleLayerVectorSource throw new Error('Does not implement addFeature'); } + deleteFeature(featureId: string): Promise { + throw new Error('Does not implement deleteFeature'); + } + getMVTFields(): MVTField[] { return this._descriptor.fields.map((field: MVTFieldDescriptor) => { return new MVTField({ diff --git a/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx b/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx index 8f93de705e365..f006fa7fde3a4 100644 --- a/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx @@ -69,6 +69,7 @@ export interface IVectorSource extends ISource { getTimesliceMaskFieldName(): Promise; supportsFeatureEditing(): Promise; addFeature(geometry: Geometry | Position[]): Promise; + deleteFeature(featureId: string): Promise; } export class AbstractVectorSource extends AbstractSource implements IVectorSource { @@ -165,6 +166,10 @@ export class AbstractVectorSource extends AbstractSource implements IVectorSourc throw new Error('Should implement VectorSource#addFeature'); } + async deleteFeature(featureId: string): Promise { + throw new Error('Should implement VectorSource#deleteFeature'); + } + async supportsFeatureEditing(): Promise { return false; } diff --git a/x-pack/plugins/maps/public/classes/util/mb_filter_expressions.ts b/x-pack/plugins/maps/public/classes/util/mb_filter_expressions.ts index 6a193216c7c1e..9568ef5c35bb1 100644 --- a/x-pack/plugins/maps/public/classes/util/mb_filter_expressions.ts +++ b/x-pack/plugins/maps/public/classes/util/mb_filter_expressions.ts @@ -20,7 +20,7 @@ export interface TimesliceMaskConfig { } export const EXCLUDE_TOO_MANY_FEATURES_BOX = ['!=', ['get', KBN_TOO_MANY_FEATURES_PROPERTY], true]; -const EXCLUDE_CENTROID_FEATURES = ['!=', ['get', KBN_IS_CENTROID_FEATURE], true]; +export const EXCLUDE_CENTROID_FEATURES = ['!=', ['get', KBN_IS_CENTROID_FEATURE], true]; function getFilterExpression( filters: unknown[], diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_control.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_control.tsx index 5d9cb59bbe522..66f0e0c4a9515 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_control.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_control.tsx @@ -13,6 +13,7 @@ import MapboxDraw from '@mapbox/mapbox-gl-draw'; import DrawRectangle from 'mapbox-gl-draw-rectangle-mode'; import type { Map as MbMap } from '@kbn/mapbox-gl'; import { Feature } from 'geojson'; +import { MapMouseEvent } from '@kbn/mapbox-gl'; import { DRAW_SHAPE } from '../../../../common/constants'; import { DrawCircle, DRAW_CIRCLE_RADIUS_MB_FILTER } from './draw_circle'; import { DrawTooltip } from './draw_tooltip'; @@ -37,6 +38,7 @@ mbDrawModes[DRAW_CIRCLE] = DrawCircle; export interface Props { drawShape?: DRAW_SHAPE; onDraw: (event: { features: Feature[] }, drawControl?: MapboxDraw) => void; + onClick?: (event: MapMouseEvent, drawControl?: MapboxDraw) => void; mbMap: MbMap; enable: boolean; updateEditShape: (shapeToDraw: DRAW_SHAPE) => void; @@ -68,6 +70,12 @@ export class DrawControl extends Component { this.props.onDraw(event, this._mbDrawControl); }; + _onClick = (event: MapMouseEvent) => { + if (this.props.onClick) { + this.props.onClick(event, this._mbDrawControl); + } + }; + // debounce with zero timeout needed to allow mapbox-draw finish logic to complete // before _removeDrawControl is called _syncDrawControl = _.debounce(() => { @@ -96,6 +104,9 @@ export class DrawControl extends Component { this.props.mbMap.getCanvas().style.cursor = ''; this.props.mbMap.off('draw.modechange', this._onModeChange); this.props.mbMap.off('draw.create', this._onDraw); + if (this.props.onClick) { + this.props.mbMap.off('click', this._onClick); + } this.props.mbMap.removeLayer(GL_DRAW_RADIUS_LABEL_LAYER_ID); this.props.mbMap.removeControl(this._mbDrawControl); this._mbDrawControlAdded = false; @@ -131,6 +142,9 @@ export class DrawControl extends Component { this.props.mbMap.getCanvas().style.cursor = 'crosshair'; this.props.mbMap.on('draw.modechange', this._onModeChange); this.props.mbMap.on('draw.create', this._onDraw); + if (this.props.onClick) { + this.props.mbMap.on('click', this._onClick); + } } const { DRAW_LINE_STRING, DRAW_POLYGON, DRAW_POINT, SIMPLE_SELECT } = this._mbDrawControl.modes; diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_feature_control/draw_feature_control.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_feature_control/draw_feature_control.tsx index fb595e7804dfe..eb5ea9b5ddba5 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_feature_control/draw_feature_control.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_feature_control/draw_feature_control.tsx @@ -6,26 +6,34 @@ */ import React, { Component } from 'react'; -import { Map as MbMap } from 'mapbox-gl'; +import { Map as MbMap, Point as MbPoint } from 'mapbox-gl'; // @ts-expect-error import MapboxDraw from '@mapbox/mapbox-gl-draw'; import { Feature, Geometry, Position } from 'geojson'; import { i18n } from '@kbn/i18n'; // @ts-expect-error import * as jsts from 'jsts'; +import { MapMouseEvent } from '@kbn/mapbox-gl'; import { getToasts } from '../../../../kibana_services'; import { DrawControl } from '../'; import { DRAW_MODE, DRAW_SHAPE } from '../../../../../common'; +import { ILayer } from '../../../../classes/layers/layer'; +import { + EXCLUDE_CENTROID_FEATURES, + EXCLUDE_TOO_MANY_FEATURES_BOX, +} from '../../../../classes/util/mb_filter_expressions'; const geoJSONReader = new jsts.io.GeoJSONReader(); export interface ReduxStateProps { drawShape?: DRAW_SHAPE; drawMode: DRAW_MODE; + editLayer: ILayer | undefined; } export interface ReduxDispatchProps { addNewFeatureToIndex: (geometry: Geometry | Position[]) => void; + deleteFeatureFromIndex: (featureId: string) => void; disableDrawState: () => void; } @@ -75,11 +83,58 @@ export class DrawFeatureControl extends Component { } }; + _onClick = async (event: MapMouseEvent, drawControl?: MapboxDraw) => { + const mbLngLatPoint: MbPoint = event.point; + if (!this.props.editLayer) { + return; + } + const mbEditLayerIds = this.props.editLayer + .getMbLayerIds() + .filter((mbLayerId) => !!this.props.mbMap.getLayer(mbLayerId)); + const PADDING = 2; // in pixels + const mbBbox = [ + { + x: mbLngLatPoint.x - PADDING, + y: mbLngLatPoint.y - PADDING, + }, + { + x: mbLngLatPoint.x + PADDING, + y: mbLngLatPoint.y + PADDING, + }, + ] as [MbPoint, MbPoint]; + const selectedFeatures = this.props.mbMap.queryRenderedFeatures(mbBbox, { + layers: mbEditLayerIds, + filter: ['all', EXCLUDE_TOO_MANY_FEATURES_BOX, EXCLUDE_CENTROID_FEATURES], + }); + if (!selectedFeatures.length) { + return; + } + const topMostFeature = selectedFeatures[0]; + + try { + if (!(topMostFeature.properties && topMostFeature.properties._id)) { + throw Error(`Associated Elasticsearch document id not found`); + } + const docId = topMostFeature.properties._id; + this.props.deleteFeatureFromIndex(docId); + } catch (error) { + getToasts().addWarning( + i18n.translate('xpack.maps.drawFeatureControl.unableToDeleteFeature', { + defaultMessage: `Unable to delete feature, error: '{errorMsg}'.`, + values: { + errorMsg: error.message, + }, + }) + ); + } + }; + render() { return ( diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_feature_control/index.ts b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_feature_control/index.ts index 9034e40913e77..e1d703173fc2d 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_feature_control/index.ts +++ b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_feature_control/index.ts @@ -15,16 +15,18 @@ import { ReduxStateProps, OwnProps, } from './draw_feature_control'; -import { addNewFeatureToIndex, updateEditShape } from '../../../../actions'; +import { addNewFeatureToIndex, deleteFeatureFromIndex, updateEditShape } from '../../../../actions'; import { MapStoreState } from '../../../../reducers/store'; -import { getEditState } from '../../../../selectors/map_selectors'; +import { getEditState, getLayerById } from '../../../../selectors/map_selectors'; import { getDrawMode } from '../../../../selectors/ui_selectors'; function mapStateToProps(state: MapStoreState): ReduxStateProps { const editState = getEditState(state); + const editLayer = editState ? getLayerById(editState.layerId, state) : undefined; return { drawShape: editState ? editState.drawShape : undefined, drawMode: getDrawMode(state), + editLayer, }; } @@ -35,6 +37,9 @@ function mapDispatchToProps( addNewFeatureToIndex(geometry: Geometry | Position[]) { dispatch(addNewFeatureToIndex(geometry)); }, + deleteFeatureFromIndex(featureId: string) { + dispatch(deleteFeatureFromIndex(featureId)); + }, disableDrawState() { dispatch(updateEditShape(null)); }, diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_tooltip.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_tooltip.tsx index 5321c30f75245..4122f7ea796d4 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_tooltip.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_tooltip.tsx @@ -98,6 +98,10 @@ export class DrawTooltip extends Component { instructions = i18n.translate('xpack.maps.drawTooltip.pointInstructions', { defaultMessage: 'Click to create point.', }); + } else if (this.props.drawShape === DRAW_SHAPE.DELETE) { + instructions = i18n.translate('xpack.maps.drawTooltip.deleteInstructions', { + defaultMessage: 'Click feature to delete.', + }); } else { // unknown draw type, tooltip not needed return null; diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/feature_draw_controls/feature_edit_tools/feature_edit_tools.tsx b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/feature_draw_controls/feature_edit_tools/feature_edit_tools.tsx index 66948c0fc9bca..a2b3b20ae1877 100644 --- a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/feature_draw_controls/feature_edit_tools/feature_edit_tools.tsx +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/feature_draw_controls/feature_edit_tools/feature_edit_tools.tsx @@ -34,6 +34,7 @@ export function FeatureEditTools(props: Props) { const drawCircleSelected = props.drawShape === DRAW_SHAPE.DISTANCE; const drawBBoxSelected = props.drawShape === DRAW_SHAPE.BOUNDS; const drawPointSelected = props.drawShape === DRAW_SHAPE.POINT; + const deleteSelected = props.drawShape === DRAW_SHAPE.DELETE; return ( @@ -117,6 +118,24 @@ export function FeatureEditTools(props: Props) { isSelected={drawPointSelected} display={drawPointSelected ? 'fill' : 'empty'} /> + props.setDrawShape(DRAW_SHAPE.DELETE)} + iconType="trash" + aria-label={i18n.translate( + 'xpack.maps.toolbarOverlay.featureDraw.deletePointOrShapeLabel', + { + defaultMessage: 'Delete point or shape', + } + )} + title={i18n.translate('xpack.maps.toolbarOverlay.featureDraw.deletePointOrShapeTitle', { + defaultMessage: 'Delete point or shape', + })} + aria-pressed={deleteSelected} + isSelected={deleteSelected} + display={deleteSelected ? 'fill' : 'empty'} + /> { + try { + const { body: resp } = await context.core.elasticsearch.client.asCurrentUser.delete({ + index: request.body.index, + id: request.params.featureId, + refresh: true, + }); + if (resp.result === 'Error') { + throw resp; + } else { + return response.ok({ body: { success: true } }); + } + } catch (error) { + logger.error(error); + const errorStatusCode = error.meta?.statusCode; + if (errorStatusCode === 401) { + return response.unauthorized({ + body: { + message: 'User not authorized to delete indexed feature', + }, + }); + } else if (errorStatusCode === 403) { + return response.forbidden({ + body: { + message: 'Access to delete indexed feature forbidden', + }, + }); + } else if (errorStatusCode === 404) { + return response.notFound({ + body: { message: 'Feature not found' }, + }); + } else { + return response.custom({ + body: 'Unknown error deleting feature', + statusCode: 500, + }); + } + } + } + ); + router.get( { path: `${GET_MATCHING_INDEXES_PATH}/{indexPattern}`, diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index aab80fe308861..3cf0891bc73b2 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -24284,4 +24284,4 @@ "xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "フィールドを選択してください。", "xpack.watcher.watcherDescription": "アラートの作成、管理、監視によりデータへの変更を検知します。" } -} \ No newline at end of file +} diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index d4f263b58b8ab..3a1836cc25014 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -24660,4 +24660,4 @@ "xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "此字段必填。", "xpack.watcher.watcherDescription": "通过创建、管理和监测警报来检测数据中的更改。" } -} \ No newline at end of file +} diff --git a/x-pack/test/api_integration/apis/maps/delete_feature.js b/x-pack/test/api_integration/apis/maps/delete_feature.js new file mode 100644 index 0000000000000..0755b1a1f6b59 --- /dev/null +++ b/x-pack/test/api_integration/apis/maps/delete_feature.js @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export default function ({ getService }) { + const supertest = getService('supertest'); + + describe('doc feature deletion', () => { + it('should delete a valid feature document', async () => { + await supertest + .delete(`/api/maps/feature/999`) + .set('kbn-xsrf', 'kibana') + .send({ + index: 'drawing_data', + }) + .expect(200); + }); + + it('previously deleted document no longer exists in index', async () => { + await supertest + .delete(`/api/maps/feature/999`) + .set('kbn-xsrf', 'kibana') + .send({ + index: 'drawing_data', + }) + .expect(404); + }); + + it('should fail if not a valid document', async () => { + await supertest + .delete(`/api/maps/feature/998`) + .set('kbn-xsrf', 'kibana') + .send({ + index: 'drawing_data', + }) + .expect(404); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/maps/index.js b/x-pack/test/api_integration/apis/maps/index.js index 14a42e06dbb9a..b3f1cf1a89e3b 100644 --- a/x-pack/test/api_integration/apis/maps/index.js +++ b/x-pack/test/api_integration/apis/maps/index.js @@ -17,6 +17,7 @@ export default function ({ loadTestFile, getService }) { describe('', () => { loadTestFile(require.resolve('./get_indexes_matching_pattern')); loadTestFile(require.resolve('./create_doc_source')); + loadTestFile(require.resolve('./delete_feature')); loadTestFile(require.resolve('./index_data')); loadTestFile(require.resolve('./fonts_api')); loadTestFile(require.resolve('./index_settings')); diff --git a/x-pack/test/functional/es_archives/maps/data/data.json b/x-pack/test/functional/es_archives/maps/data/data.json index e44b19504af38..86b257db8205c 100644 --- a/x-pack/test/functional/es_archives/maps/data/data.json +++ b/x-pack/test/functional/es_archives/maps/data/data.json @@ -137,6 +137,42 @@ } +{ + "type": "doc", + "value": { + "id": "999", + "index": "drawing_data", + "source": { + "geometry": { + "coordinates": [ + [ + [ + 100, + 5 + ], + [ + 95, + -5 + ], + [ + 105, + -5 + ], + [ + 100, + 5 + ] + ] + ], + "type": "polygon" + }, + "name": "doc-to-delete", + "prop1": 9 + } + } +} + + { "type": "doc", "value": { diff --git a/x-pack/test/functional/es_archives/maps/data/mappings.json b/x-pack/test/functional/es_archives/maps/data/mappings.json index 4ad5d6c33295b..3b674d162fced 100644 --- a/x-pack/test/functional/es_archives/maps/data/mappings.json +++ b/x-pack/test/functional/es_archives/maps/data/mappings.json @@ -26,6 +26,34 @@ } } +{ + "type": "index", + "value": { + "index": "drawing_data", + "mappings": { + "properties": { + "geometry": { + "type": "geo_shape" + }, + "name": { + "type": "keyword" + }, + "prop1": { + "type": "byte" + } + } + }, + "settings": { + "index": { + "number_of_replicas": "0", + "number_of_shards": "1", + "max_result_window": "10001", + "max_inner_result_window": "101" + } + } + } +} + { "type": "index", "value": { From bc8ba8314a615e1cbf54a48974f8d36c7b38b83b Mon Sep 17 00:00:00 2001 From: Andrew Kroh Date: Fri, 25 Jun 2021 00:22:29 -0400 Subject: [PATCH 66/69] [Fleet] Add support for constant_keyword "value" in package field definitions (#103000) This enables Fleet to accept package field definitions that declare a constant "value" for `constant_keyword` fields. Fleet will generate an index template for the constant_keyword field that contains the `value` attribute. Relates: https://github.com/elastic/package-spec/pull/194 Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../__snapshots__/template.test.ts.snap | 4 ++++ .../elasticsearch/template/template.test.ts | 20 +++++++++++++++++++ .../epm/elasticsearch/template/template.ts | 6 ++++++ .../fields/__snapshots__/field.test.ts.snap | 5 +++++ .../fleet/server/services/epm/fields/field.ts | 1 + .../server/services/epm/fields/tests/base.yml | 3 +++ 6 files changed, 39 insertions(+) diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap index 6a4476316bfa5..eff35a30ba2d6 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap @@ -86,6 +86,10 @@ exports[`EPM template tests loading base.yml: base.yml 1`] = ` }, "validarray": { "type": "integer" + }, + "cycle_type": { + "type": "constant_keyword", + "value": "bicycle" } }, "_meta": { diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.test.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.test.ts index d1f806f67ca5c..10db955c52ee1 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.test.ts @@ -613,6 +613,26 @@ describe('EPM template', () => { expect(JSON.stringify(mappings)).toEqual(JSON.stringify(constantKeywordMapping)); }); + it('tests constant_keyword field type with value', () => { + const constantKeywordLiteralYaml = ` +- name: constantKeyword + type: constant_keyword + value: always_the_same + `; + const constantKeywordMapping = { + properties: { + constantKeyword: { + type: 'constant_keyword', + value: 'always_the_same', + }, + }, + }; + const fields: Field[] = safeLoad(constantKeywordLiteralYaml); + const processedFields = processFields(fields); + const mappings = generateMappings(processedFields); + expect(JSON.stringify(mappings)).toEqual(JSON.stringify(constantKeywordMapping)); + }); + it('processes meta fields', () => { const metaFieldLiteralYaml = ` - name: fieldWithMetas diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts index 6aa7680395bed..d4181201677c5 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts @@ -150,6 +150,12 @@ export function generateMappings(fields: Field[]): IndexTemplateMappings { fieldProps.fields = generateMultiFields(field.multi_fields); } break; + case 'constant_keyword': + fieldProps.type = field.type; + if (field.value) { + fieldProps.value = field.value; + } + break; case 'object': fieldProps = { ...fieldProps, ...generateDynamicAndEnabled(field), type: 'object' }; break; diff --git a/x-pack/plugins/fleet/server/services/epm/fields/__snapshots__/field.test.ts.snap b/x-pack/plugins/fleet/server/services/epm/fields/__snapshots__/field.test.ts.snap index 5c402b896093a..78f1fbc528696 100644 --- a/x-pack/plugins/fleet/server/services/epm/fields/__snapshots__/field.test.ts.snap +++ b/x-pack/plugins/fleet/server/services/epm/fields/__snapshots__/field.test.ts.snap @@ -55,6 +55,11 @@ exports[`tests loading fields.yml: base.yml 1`] = ` "name": "validarray", "type": "array", "object_type": "integer" + }, + { + "name": "cycle_type", + "type": "constant_keyword", + "value": "bicycle" } ] `; diff --git a/x-pack/plugins/fleet/server/services/epm/fields/field.ts b/x-pack/plugins/fleet/server/services/epm/fields/field.ts index b8839b88bb78c..9fa738aa60d09 100644 --- a/x-pack/plugins/fleet/server/services/epm/fields/field.ts +++ b/x-pack/plugins/fleet/server/services/epm/fields/field.ts @@ -15,6 +15,7 @@ export interface Field { name: string; type?: string; description?: string; + value?: string; format?: string; fields?: Fields; enabled?: boolean; diff --git a/x-pack/plugins/fleet/server/services/epm/fields/tests/base.yml b/x-pack/plugins/fleet/server/services/epm/fields/tests/base.yml index 5a71c7dee54dc..ee7cd63539c15 100644 --- a/x-pack/plugins/fleet/server/services/epm/fields/tests/base.yml +++ b/x-pack/plugins/fleet/server/services/epm/fields/tests/base.yml @@ -20,4 +20,7 @@ object_type: integer - name: invalidarray type: array +- name: cycle_type + type: constant_keyword + value: bicycle From bc6ee27a29c24582d9553813d4d84864749a7cc8 Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Fri, 25 Jun 2021 07:55:06 +0200 Subject: [PATCH 67/69] Maps locators (#102810) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 🎸 implement maps locator * feat: 🎸 add tile map locator * feat: 🎸 add region map locator * feat: 🎸 register maps locators * refactor: 💡 remove usage of mpas url gen, replace by locator * chore: 🤖 remove url generators * refactor: 💡 use locators in maps deprecation messages * chore: 🤖 remove maps url generators * refactor: 💡 use new property name * feat: 🎸 use constant Co-authored-by: Vadim Kibana Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/get_deprecation_message.tsx | 32 +-- .../public/get_deprecation_message.tsx | 30 +- ...url_generator.test.ts => locators.test.ts} | 85 +++--- x-pack/plugins/maps/public/locators.ts | 260 ++++++++++++++++++ x-pack/plugins/maps/public/plugin.ts | 36 +-- .../visualize_geo_field_action.ts | 35 ++- x-pack/plugins/maps/public/url_generator.ts | 241 ---------------- 7 files changed, 367 insertions(+), 352 deletions(-) rename x-pack/plugins/maps/public/{url_generator.test.ts => locators.test.ts} (52%) create mode 100644 x-pack/plugins/maps/public/locators.ts delete mode 100644 x-pack/plugins/maps/public/url_generator.ts diff --git a/src/plugins/region_map/public/get_deprecation_message.tsx b/src/plugins/region_map/public/get_deprecation_message.tsx index 5ae39a1291c4c..2606c8ed108e2 100644 --- a/src/plugins/region_map/public/get_deprecation_message.tsx +++ b/src/plugins/region_map/public/get_deprecation_message.tsx @@ -8,8 +8,7 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; -import { UrlGeneratorContract } from 'src/plugins/share/public'; -import { getCoreService, getQueryService, getShareService } from './kibana_services'; +import { getQueryService, getShareService } from './kibana_services'; import { Vis } from '../../visualizations/public'; import { LegacyMapDeprecationMessage } from '../../maps_legacy/public'; @@ -25,24 +24,16 @@ function getEmsLayerId(id: string | number, layerId: string) { } export function getDeprecationMessage(vis: Vis) { - let mapsRegionMapUrlGenerator: - | UrlGeneratorContract<'MAPS_APP_REGION_MAP_URL_GENERATOR'> - | undefined; - try { - mapsRegionMapUrlGenerator = getShareService().urlGenerators.getUrlGenerator( - 'MAPS_APP_REGION_MAP_URL_GENERATOR' - ); - } catch (error) { - // ignore error thrown when url generator is not available - } - const title = i18n.translate('regionMap.mapVis.regionMapTitle', { defaultMessage: 'Region Map' }); async function onClick(e: React.MouseEvent) { e.preventDefault(); + const locator = getShareService().url.locators.get('MAPS_APP_REGION_MAP_LOCATOR'); + if (!locator) return; + const query = getQueryService(); - const createUrlParams: { [key: string]: any } = { + const params: { [key: string]: any } = { label: vis.title ? vis.title : title, emsLayerId: vis.params.selectedLayer.isEMS ? getEmsLayerId(vis.params.selectedLayer.id, vis.params.selectedLayer.layerId) @@ -59,23 +50,22 @@ export function getDeprecationMessage(vis: Vis) { const bucketAggs = vis.data?.aggs?.byType('buckets'); if (bucketAggs?.length && bucketAggs[0].type.dslName === 'terms') { - createUrlParams.termsFieldName = bucketAggs[0].getField()?.name; - createUrlParams.termsSize = bucketAggs[0].getParam('size'); + params.termsFieldName = bucketAggs[0].getField()?.name; + params.termsSize = bucketAggs[0].getParam('size'); } const metricAggs = vis.data?.aggs?.byType('metrics'); if (metricAggs?.length) { - createUrlParams.metricAgg = metricAggs[0].type.dslName; - createUrlParams.metricFieldName = metricAggs[0].getField()?.name; + params.metricAgg = metricAggs[0].type.dslName; + params.metricFieldName = metricAggs[0].getField()?.name; } - const url = await mapsRegionMapUrlGenerator!.createUrl(createUrlParams); - getCoreService().application.navigateToUrl(url); + locator.navigate(params); } return ( diff --git a/src/plugins/tile_map/public/get_deprecation_message.tsx b/src/plugins/tile_map/public/get_deprecation_message.tsx index 592b2b5f36eb2..6f71aa15b8a6b 100644 --- a/src/plugins/tile_map/public/get_deprecation_message.tsx +++ b/src/plugins/tile_map/public/get_deprecation_message.tsx @@ -8,22 +8,12 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; -import { UrlGeneratorContract } from 'src/plugins/share/public'; -import { getCoreService, getQueryService, getShareService } from './services'; +import { getQueryService, getShareService } from './services'; import { indexPatterns } from '../../data/public'; import { Vis } from '../../visualizations/public'; import { LegacyMapDeprecationMessage } from '../../maps_legacy/public'; export function getDeprecationMessage(vis: Vis) { - let mapsTileMapUrlGenerator: UrlGeneratorContract<'MAPS_APP_TILE_MAP_URL_GENERATOR'> | undefined; - try { - mapsTileMapUrlGenerator = getShareService().urlGenerators.getUrlGenerator( - 'MAPS_APP_TILE_MAP_URL_GENERATOR' - ); - } catch (error) { - // ignore error thrown when url generator is not available - } - const title = i18n.translate('tileMap.vis.mapTitle', { defaultMessage: 'Coordinate Map', }); @@ -31,8 +21,11 @@ export function getDeprecationMessage(vis: Vis) { async function onClick(e: React.MouseEvent) { e.preventDefault(); + const locator = getShareService().url.locators.get('MAPS_APP_TILE_MAP_LOCATOR'); + if (!locator) return; + const query = getQueryService(); - const createUrlParams: { [key: string]: any } = { + const params: { [key: string]: any } = { label: vis.title ? vis.title : title, mapType: vis.params.mapType, colorSchema: vis.params.colorSchema, @@ -45,7 +38,7 @@ export function getDeprecationMessage(vis: Vis) { const bucketAggs = vis.data?.aggs?.byType('buckets'); if (bucketAggs?.length && bucketAggs[0].type.dslName === 'geohash_grid') { - createUrlParams.geoFieldName = bucketAggs[0].getField()?.name; + params.geoFieldName = bucketAggs[0].getField()?.name; } else if (vis.data.indexPattern) { // attempt to default to first geo point field when geohash is not configured yet const geoField = vis.data.indexPattern.fields.find((field) => { @@ -54,23 +47,22 @@ export function getDeprecationMessage(vis: Vis) { ); }); if (geoField) { - createUrlParams.geoFieldName = geoField.name; + params.geoFieldName = geoField.name; } } const metricAggs = vis.data?.aggs?.byType('metrics'); if (metricAggs?.length) { - createUrlParams.metricAgg = metricAggs[0].type.dslName; - createUrlParams.metricFieldName = metricAggs[0].getField()?.name; + params.metricAgg = metricAggs[0].type.dslName; + params.metricFieldName = metricAggs[0].getField()?.name; } - const url = await mapsTileMapUrlGenerator!.createUrl(createUrlParams); - getCoreService().application.navigateToUrl(url); + locator.navigate(params); } return ( diff --git a/x-pack/plugins/maps/public/url_generator.test.ts b/x-pack/plugins/maps/public/locators.test.ts similarity index 52% rename from x-pack/plugins/maps/public/url_generator.test.ts rename to x-pack/plugins/maps/public/locators.test.ts index 827deb4cc9ad4..d6e82d1cdb601 100644 --- a/x-pack/plugins/maps/public/url_generator.test.ts +++ b/x-pack/plugins/maps/public/locators.test.ts @@ -5,49 +5,49 @@ * 2.0. */ -import { createMapsUrlGenerator } from './url_generator'; import { LAYER_TYPE, SOURCE_TYPES, SCALING_TYPES } from '../common/constants'; import { esFilters } from '../../../../src/plugins/data/public'; +import { MapsAppLocatorDefinition } from './locators'; +import { SerializableState } from '../../../../src/plugins/kibana_utils/common'; +import { LayerDescriptor } from '../common/descriptor_types'; -const APP_BASE_PATH: string = 'test/app/maps'; const MAP_ID: string = '2c9c1f60-1909-11e9-919b-ffe5949a18d2'; const LAYER_ID: string = '13823000-99b9-11ea-9eb6-d9e8adceb647'; const INDEX_PATTERN_ID: string = '90943e30-9a47-11e8-b64d-95841ca0b247'; describe('visualize url generator', () => { test('creates a link to a new visualization', async () => { - const generator = createMapsUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: false, - }) - ); - const url = await generator.createUrl!({}); - expect(url).toMatchInlineSnapshot(`"test/app/maps/map#/?_g=()&_a=()"`); + const locator = new MapsAppLocatorDefinition({ + useHash: false, + }); + const location = await locator.getLocation({}); + + expect(location).toMatchObject({ + app: 'maps', + path: '/map#/?_g=()&_a=()', + state: {}, + }); }); test('creates a link with global time range set up', async () => { - const generator = createMapsUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: false, - }) - ); - const url = await generator.createUrl!({ + const locator = new MapsAppLocatorDefinition({ + useHash: false, + }); + const location = await locator.getLocation({ timeRange: { to: 'now', from: 'now-15m', mode: 'relative' }, }); - expect(url).toMatchInlineSnapshot( - `"test/app/maps/map#/?_g=(time:(from:now-15m,mode:relative,to:now))&_a=()"` - ); + + expect(location).toMatchObject({ + app: 'maps', + path: '/map#/?_g=(time:(from:now-15m,mode:relative,to:now))&_a=()', + state: {}, + }); }); test('creates a link with initialLayers set up', async () => { - const generator = createMapsUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: false, - }) - ); + const locator = new MapsAppLocatorDefinition({ + useHash: false, + }); const initialLayers = [ { id: LAYER_ID, @@ -64,22 +64,22 @@ describe('visualize url generator', () => { }, }, ]; - const url = await generator.createUrl!({ - initialLayers, + const location = await locator.getLocation({ + initialLayers: (initialLayers as unknown) as LayerDescriptor[] & SerializableState, + }); + + expect(location).toMatchObject({ + app: 'maps', + path: `/map#/?_g=()&_a=()&initialLayers=(id%3A'13823000-99b9-11ea-9eb6-d9e8adceb647'%2CsourceDescriptor%3A(geoField%3Atest%2Cid%3A'13823000-99b9-11ea-9eb6-d9e8adceb647'%2CindexPatternId%3A'90943e30-9a47-11e8-b64d-95841ca0b247'%2Clabel%3A'Sample%20Data'%2CscalingType%3ALIMIT%2CtooltipProperties%3A!()%2Ctype%3AES_SEARCH)%2Ctype%3AVECTOR%2Cvisible%3A!t)`, + state: {}, }); - expect(url).toMatchInlineSnapshot( - `"test/app/maps/map#/?_g=()&_a=()&initialLayers=(id%3A'13823000-99b9-11ea-9eb6-d9e8adceb647'%2CsourceDescriptor%3A(geoField%3Atest%2Cid%3A'13823000-99b9-11ea-9eb6-d9e8adceb647'%2CindexPatternId%3A'90943e30-9a47-11e8-b64d-95841ca0b247'%2Clabel%3A'Sample%20Data'%2CscalingType%3ALIMIT%2CtooltipProperties%3A!()%2Ctype%3AES_SEARCH)%2Ctype%3AVECTOR%2Cvisible%3A!t)"` - ); }); test('creates a link with filters, time range, refresh interval and query to a saved visualization', async () => { - const generator = createMapsUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: false, - }) - ); - const url = await generator.createUrl!({ + const locator = new MapsAppLocatorDefinition({ + useHash: false, + }); + const location = await locator.getLocation({ timeRange: { to: 'now', from: 'now-15m', mode: 'relative' }, refreshInterval: { pause: false, value: 300 }, mapId: MAP_ID, @@ -106,8 +106,11 @@ describe('visualize url generator', () => { ], query: { query: 'q2', language: 'kuery' }, }); - expect(url).toMatchInlineSnapshot( - `"test/app/maps/map#/${MAP_ID}?_g=(filters:!(('$state':(store:globalState),meta:(alias:!n,disabled:!f,negate:!f),query:(query:q1))),refreshInterval:(pause:!f,value:300),time:(from:now-15m,mode:relative,to:now))&_a=(filters:!((meta:(alias:!n,disabled:!f,negate:!f),query:(query:q1))),query:(language:kuery,query:q2))"` - ); + + expect(location).toMatchObject({ + app: 'maps', + path: `/map#/${MAP_ID}?_g=(filters:!(('$state':(store:globalState),meta:(alias:!n,disabled:!f,negate:!f),query:(query:q1))),refreshInterval:(pause:!f,value:300),time:(from:now-15m,mode:relative,to:now))&_a=(filters:!((meta:(alias:!n,disabled:!f,negate:!f),query:(query:q1))),query:(language:kuery,query:q2))`, + state: {}, + }); }); }); diff --git a/x-pack/plugins/maps/public/locators.ts b/x-pack/plugins/maps/public/locators.ts new file mode 100644 index 0000000000000..7e2be7c6c7ec9 --- /dev/null +++ b/x-pack/plugins/maps/public/locators.ts @@ -0,0 +1,260 @@ +/* + * 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. + */ + +/* eslint-disable max-classes-per-file */ + +import rison from 'rison-node'; +import type { + TimeRange, + Filter, + Query, + QueryState, + RefreshInterval, +} from '../../../../src/plugins/data/public'; +import { esFilters } from '../../../../src/plugins/data/public'; +import { setStateToKbnUrl } from '../../../../src/plugins/kibana_utils/public'; +import { SerializableState } from '../../../../src/plugins/kibana_utils/common'; +import type { LocatorDefinition, LocatorPublic } from '../../../../src/plugins/share/public'; +import type { LayerDescriptor } from '../common/descriptor_types'; +import { INITIAL_LAYERS_KEY, APP_ID } from '../common/constants'; +import { lazyLoadMapModules } from './lazy_load_bundle'; + +export interface MapsAppLocatorParams extends SerializableState { + /** + * If given, it will load the given map else will load the create a new map page. + */ + mapId?: string; + + /** + * Optionally set the time range in the time picker. + */ + timeRange?: TimeRange; + + /** + * Optionally set the initial Layers. + */ + initialLayers?: LayerDescriptor[] & SerializableState; + + /** + * Optionally set the refresh interval. + */ + refreshInterval?: RefreshInterval & SerializableState; + + /** + * Optionally apply filers. NOTE: if given and used in conjunction with `mapId`, and the + * saved map has filters saved with it, this will _replace_ those filters. + */ + filters?: Filter[]; + + /** + * Optionally set a query. NOTE: if given and used in conjunction with `mapId`, and the + * saved map has a query saved with it, this will _replace_ that query. + */ + query?: Query; + + /** + * If not given, will use the uiSettings configuration for `storeInSessionStorage`. useHash determines + * whether to hash the data in the url to avoid url length issues. + */ + hash?: boolean; +} + +export const MAPS_APP_LOCATOR = 'MAPS_APP_LOCATOR' as const; + +export type MapsAppLocator = LocatorPublic; + +export interface MapsAppLocatorDependencies { + useHash: boolean; +} + +export class MapsAppLocatorDefinition implements LocatorDefinition { + public readonly id = MAPS_APP_LOCATOR; + + constructor(protected readonly deps: MapsAppLocatorDependencies) {} + + public readonly getLocation = async (params: MapsAppLocatorParams) => { + const { mapId, filters, query, refreshInterval, timeRange, initialLayers, hash } = params; + const useHash = hash ?? this.deps.useHash; + const appState: { + query?: Query; + filters?: Filter[]; + vis?: unknown; + } = {}; + const queryState: QueryState = {}; + + if (query) appState.query = query; + if (filters && filters.length) + appState.filters = filters?.filter((f) => !esFilters.isFilterPinned(f)); + if (timeRange) queryState.time = timeRange; + if (filters && filters.length) + queryState.filters = filters?.filter((f) => esFilters.isFilterPinned(f)); + if (refreshInterval) queryState.refreshInterval = refreshInterval; + + let path = `/map#/${mapId || ''}`; + path = setStateToKbnUrl('_g', queryState, { useHash }, path); + path = setStateToKbnUrl('_a', appState, { useHash }, path); + + if (initialLayers && initialLayers.length) { + const risonEncodedInitialLayers = ((rison as unknown) as { + encode_array: ( + initialLayers: (LayerDescriptor[] & SerializableState) | undefined + ) => string; + }).encode_array(initialLayers); + path = `${path}&${INITIAL_LAYERS_KEY}=${encodeURIComponent(risonEncodedInitialLayers)}`; + } + + return { + app: APP_ID, + path, + state: {}, + }; + }; +} + +export interface MapsAppTileMapLocatorParams extends SerializableState { + label: string; + mapType: string; + colorSchema: string; + indexPatternId?: string; + geoFieldName?: string; + metricAgg: string; + metricFieldName?: string; + timeRange?: TimeRange; + filters?: Filter[]; + query?: Query; + hash?: boolean; +} + +export type MapsAppTileMapLocator = LocatorPublic; + +export const MAPS_APP_TILE_MAP_LOCATOR = 'MAPS_APP_TILE_MAP_LOCATOR' as const; + +export interface MapsAppTileMapLocatorDependencies { + locator: MapsAppLocator; +} + +export class MapsAppTileMapLocatorDefinition + implements LocatorDefinition { + public readonly id = MAPS_APP_TILE_MAP_LOCATOR; + + constructor(protected readonly deps: MapsAppTileMapLocatorDependencies) {} + + public readonly getLocation = async (params: MapsAppTileMapLocatorParams) => { + const { + label, + mapType, + colorSchema, + indexPatternId, + geoFieldName, + metricAgg, + metricFieldName, + filters, + query, + timeRange, + hash = true, + } = params; + const mapModules = await lazyLoadMapModules(); + const initialLayers = ([] as unknown) as LayerDescriptor[] & SerializableState; + const tileMapLayerDescriptor = mapModules.createTileMapLayerDescriptor({ + label, + mapType, + colorSchema, + indexPatternId, + geoFieldName, + metricAgg, + metricFieldName, + }); + + if (tileMapLayerDescriptor) { + initialLayers.push(tileMapLayerDescriptor); + } + + return await this.deps.locator.getLocation({ + initialLayers, + filters, + query, + timeRange, + hash, + }); + }; +} + +export interface MapsAppRegionMapLocatorParams extends SerializableState { + label: string; + emsLayerId?: string; + leftFieldName?: string; + termsFieldName?: string; + termsSize?: number; + colorSchema: string; + indexPatternId?: string; + indexPatternTitle?: string; + metricAgg: string; + metricFieldName?: string; + timeRange?: TimeRange; + filters?: Filter[]; + query?: Query; + hash?: boolean; +} + +export type MapsAppRegionMapLocator = LocatorPublic; + +export const MAPS_APP_REGION_MAP_LOCATOR = 'MAPS_APP_REGION_MAP_LOCATOR' as const; + +export interface MapsAppRegionMapLocatorDependencies { + locator: MapsAppLocator; +} + +export class MapsAppRegionMapLocatorDefinition + implements LocatorDefinition { + public readonly id = MAPS_APP_REGION_MAP_LOCATOR; + + constructor(protected readonly deps: MapsAppRegionMapLocatorDependencies) {} + + public readonly getLocation = async (params: MapsAppRegionMapLocatorParams) => { + const { + label, + emsLayerId, + leftFieldName, + termsFieldName, + termsSize, + colorSchema, + indexPatternId, + indexPatternTitle, + metricAgg, + metricFieldName, + filters, + query, + timeRange, + hash = true, + } = params; + const mapModules = await lazyLoadMapModules(); + const initialLayers = ([] as unknown) as LayerDescriptor[] & SerializableState; + const regionMapLayerDescriptor = mapModules.createRegionMapLayerDescriptor({ + label, + emsLayerId, + leftFieldName, + termsFieldName, + termsSize, + colorSchema, + indexPatternId, + indexPatternTitle, + metricAgg, + metricFieldName, + }); + if (regionMapLayerDescriptor) { + initialLayers.push(regionMapLayerDescriptor); + } + + return await this.deps.locator.getLocation({ + initialLayers, + filters, + query, + timeRange, + hash, + }); + }; +} diff --git a/x-pack/plugins/maps/public/plugin.ts b/x-pack/plugins/maps/public/plugin.ts index 740112124a251..b526e7b24d90f 100644 --- a/x-pack/plugins/maps/public/plugin.ts +++ b/x-pack/plugins/maps/public/plugin.ts @@ -36,11 +36,6 @@ import type { } from '../../../../src/plugins/visualizations/public'; import { APP_ICON_SOLUTION, APP_ID, MAP_SAVED_OBJECT_TYPE } from '../common/constants'; import { VISUALIZE_GEO_FIELD_TRIGGER } from '../../../../src/plugins/ui_actions/public'; -import { - createMapsUrlGenerator, - createRegionMapUrlGenerator, - createTileMapUrlGenerator, -} from './url_generator'; import { visualizeGeoFieldAction } from './trigger_actions/visualize_geo_field_action'; import { filterByMapExtentAction } from './trigger_actions/filter_by_map_extent_action'; import { MapEmbeddableFactory } from './embeddable/map_embeddable_factory'; @@ -71,6 +66,11 @@ import { import { EMSSettings } from '../common/ems_settings'; import type { SavedObjectTaggingPluginStart } from '../../saved_objects_tagging/public'; import type { ChartsPluginStart } from '../../../../src/plugins/charts/public'; +import { + MapsAppLocatorDefinition, + MapsAppRegionMapLocatorDefinition, + MapsAppTileMapLocatorDefinition, +} from './locators'; export interface MapsPluginSetupDependencies { inspector: InspectorSetupContract; @@ -133,17 +133,21 @@ export class MapsPlugin const emsSettings = new EMSSettings(plugins.mapsEms.config, getIsEnterprisePlus); setEMSSettings(emsSettings); - // register url generators - const getStartServices = async () => { - const [coreStart] = await core.getStartServices(); - return { - appBasePath: coreStart.application.getUrlForApp('maps'), - useHashedUrl: coreStart.uiSettings.get('state:storeInSessionStorage'), - }; - }; - plugins.share.urlGenerators.registerUrlGenerator(createMapsUrlGenerator(getStartServices)); - plugins.share.urlGenerators.registerUrlGenerator(createTileMapUrlGenerator(getStartServices)); - plugins.share.urlGenerators.registerUrlGenerator(createRegionMapUrlGenerator(getStartServices)); + const locator = plugins.share.url.locators.create( + new MapsAppLocatorDefinition({ + useHash: core.uiSettings.get('state:storeInSessionStorage'), + }) + ); + plugins.share.url.locators.create( + new MapsAppTileMapLocatorDefinition({ + locator, + }) + ); + plugins.share.url.locators.create( + new MapsAppRegionMapLocatorDefinition({ + locator, + }) + ); plugins.inspector.registerView(MapView); if (plugins.home) { diff --git a/x-pack/plugins/maps/public/trigger_actions/visualize_geo_field_action.ts b/x-pack/plugins/maps/public/trigger_actions/visualize_geo_field_action.ts index acdc5164cb17d..c6daee554c9fc 100644 --- a/x-pack/plugins/maps/public/trigger_actions/visualize_geo_field_action.ts +++ b/x-pack/plugins/maps/public/trigger_actions/visualize_geo_field_action.ts @@ -7,6 +7,7 @@ import uuid from 'uuid/v4'; import { i18n } from '@kbn/i18n'; +import { SerializableState } from 'src/plugins/kibana_utils/common'; import { createAction, ACTION_VISUALIZE_GEO_FIELD, @@ -17,10 +18,11 @@ import { getIndexPatternService, getData, getShareService, - getNavigateToApp, + getCore, } from '../kibana_services'; -import { MAPS_APP_URL_GENERATOR, MapsUrlGeneratorState } from '../url_generator'; -import { LAYER_TYPE, SOURCE_TYPES, SCALING_TYPES, APP_ID, MAP_PATH } from '../../common/constants'; +import { MapsAppLocator, MAPS_APP_LOCATOR } from '../locators'; +import { LAYER_TYPE, SOURCE_TYPES, SCALING_TYPES } from '../../common/constants'; +import { LayerDescriptor } from '../../common/descriptor_types'; export const visualizeGeoFieldAction = createAction({ id: ACTION_VISUALIZE_GEO_FIELD, @@ -31,15 +33,19 @@ export const visualizeGeoFieldAction = createAction({ }), isCompatible: async () => !!getVisualizeCapabilities().show, getHref: async (context) => { - const url = await getMapsLink(context); - return url; + const { app, path } = await getMapsLink(context); + + return getCore().application.getUrlForApp(app, { + path, + absolute: false, + }); }, execute: async (context) => { - const url = await getMapsLink(context); - const hash = url.split('#')[1]; + const { app, path, state } = await getMapsLink(context); - getNavigateToApp()(APP_ID, { - path: `${MAP_PATH}/#${hash}`, + getCore().application.navigateToApp(app, { + path, + state, }); }, }); @@ -68,12 +74,13 @@ const getMapsLink = async (context: VisualizeFieldContext) => { }, ]; - const generator = getShareService().urlGenerators.getUrlGenerator(MAPS_APP_URL_GENERATOR); - const urlState: MapsUrlGeneratorState = { + const locator = getShareService().url.locators.get(MAPS_APP_LOCATOR) as MapsAppLocator; + const location = await locator.getLocation({ filters: getData().query.filterManager.getFilters(), query: getData().query.queryString.getQuery(), - initialLayers, + initialLayers: (initialLayers as unknown) as LayerDescriptor[] & SerializableState, timeRange: getData().query.timefilter.timefilter.getTime(), - }; - return generator.createUrl(urlState); + }); + + return location; }; diff --git a/x-pack/plugins/maps/public/url_generator.ts b/x-pack/plugins/maps/public/url_generator.ts deleted file mode 100644 index 9f28b388c4756..0000000000000 --- a/x-pack/plugins/maps/public/url_generator.ts +++ /dev/null @@ -1,241 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import rison from 'rison-node'; -import type { - TimeRange, - Filter, - Query, - QueryState, - RefreshInterval, -} from '../../../../src/plugins/data/public'; -import { esFilters } from '../../../../src/plugins/data/public'; -import { setStateToKbnUrl } from '../../../../src/plugins/kibana_utils/public'; -import type { UrlGeneratorsDefinition } from '../../../../src/plugins/share/public'; -import type { LayerDescriptor } from '../common/descriptor_types'; -import { INITIAL_LAYERS_KEY } from '../common/constants'; -import { lazyLoadMapModules } from './lazy_load_bundle'; - -const STATE_STORAGE_KEY = '_a'; -const GLOBAL_STATE_STORAGE_KEY = '_g'; - -export const MAPS_APP_URL_GENERATOR = 'MAPS_APP_URL_GENERATOR'; -export const MAPS_APP_TILE_MAP_URL_GENERATOR = 'MAPS_APP_TILE_MAP_URL_GENERATOR'; -export const MAPS_APP_REGION_MAP_URL_GENERATOR = 'MAPS_APP_REGION_MAP_URL_GENERATOR'; - -export interface MapsUrlGeneratorState { - /** - * If given, it will load the given map else will load the create a new map page. - */ - mapId?: string; - /** - * Optionally set the time range in the time picker. - */ - timeRange?: TimeRange; - - /** - * Optionally set the initial Layers. - */ - initialLayers?: LayerDescriptor[]; - - /** - * Optionally set the refresh interval. - */ - refreshInterval?: RefreshInterval; - - /** - * Optionally apply filers. NOTE: if given and used in conjunction with `mapId`, and the - * saved map has filters saved with it, this will _replace_ those filters. - */ - filters?: Filter[]; - /** - * Optionally set a query. NOTE: if given and used in conjunction with `mapId`, and the - * saved map has a query saved with it, this will _replace_ that query. - */ - query?: Query; - /** - * If not given, will use the uiSettings configuration for `storeInSessionStorage`. useHash determines - * whether to hash the data in the url to avoid url length issues. - */ - hash?: boolean; -} - -type GetStartServices = () => Promise<{ - appBasePath: string; - useHashedUrl: boolean; -}>; - -async function createMapUrl({ - getStartServices, - mapId, - filters, - query, - refreshInterval, - timeRange, - initialLayers, - hash, -}: MapsUrlGeneratorState & { getStartServices: GetStartServices }): Promise { - const startServices = await getStartServices(); - const useHash = hash ?? startServices.useHashedUrl; - const appBasePath = startServices.appBasePath; - - const appState: { - query?: Query; - filters?: Filter[]; - vis?: unknown; - } = {}; - const queryState: QueryState = {}; - - if (query) appState.query = query; - if (filters && filters.length) - appState.filters = filters?.filter((f) => !esFilters.isFilterPinned(f)); - - if (timeRange) queryState.time = timeRange; - if (filters && filters.length) - queryState.filters = filters?.filter((f) => esFilters.isFilterPinned(f)); - if (refreshInterval) queryState.refreshInterval = refreshInterval; - - let url = `${appBasePath}/map#/${mapId || ''}`; - url = setStateToKbnUrl(GLOBAL_STATE_STORAGE_KEY, queryState, { useHash }, url); - url = setStateToKbnUrl(STATE_STORAGE_KEY, appState, { useHash }, url); - - if (initialLayers && initialLayers.length) { - // @ts-ignore - const risonEncodedInitialLayers = rison.encode_array(initialLayers); - url = `${url}&${INITIAL_LAYERS_KEY}=${encodeURIComponent(risonEncodedInitialLayers)}`; - } - - return url; -} - -export const createMapsUrlGenerator = ( - getStartServices: GetStartServices -): UrlGeneratorsDefinition => ({ - id: MAPS_APP_URL_GENERATOR, - createUrl: async (mapsUrlGeneratorState: MapsUrlGeneratorState): Promise => { - return createMapUrl({ ...mapsUrlGeneratorState, getStartServices }); - }, -}); - -export const createTileMapUrlGenerator = ( - getStartServices: GetStartServices -): UrlGeneratorsDefinition => ({ - id: MAPS_APP_TILE_MAP_URL_GENERATOR, - createUrl: async ({ - label, - mapType, - colorSchema, - indexPatternId, - geoFieldName, - metricAgg, - metricFieldName, - filters, - query, - timeRange, - hash, - }: { - label: string; - mapType: string; - colorSchema: string; - indexPatternId?: string; - geoFieldName?: string; - metricAgg: string; - metricFieldName?: string; - timeRange?: TimeRange; - filters?: Filter[]; - query?: Query; - hash?: boolean; - }): Promise => { - const mapModules = await lazyLoadMapModules(); - const initialLayers = []; - const tileMapLayerDescriptor = mapModules.createTileMapLayerDescriptor({ - label, - mapType, - colorSchema, - indexPatternId, - geoFieldName, - metricAgg, - metricFieldName, - }); - if (tileMapLayerDescriptor) { - initialLayers.push(tileMapLayerDescriptor); - } - - return createMapUrl({ - initialLayers, - filters, - query, - timeRange, - hash: true, - getStartServices, - }); - }, -}); - -export const createRegionMapUrlGenerator = ( - getStartServices: GetStartServices -): UrlGeneratorsDefinition => ({ - id: MAPS_APP_REGION_MAP_URL_GENERATOR, - createUrl: async ({ - label, - emsLayerId, - leftFieldName, - termsFieldName, - termsSize, - colorSchema, - indexPatternId, - indexPatternTitle, - metricAgg, - metricFieldName, - filters, - query, - timeRange, - hash, - }: { - label: string; - emsLayerId?: string; - leftFieldName?: string; - termsFieldName?: string; - termsSize?: number; - colorSchema: string; - indexPatternId?: string; - indexPatternTitle?: string; - metricAgg: string; - metricFieldName?: string; - timeRange?: TimeRange; - filters?: Filter[]; - query?: Query; - hash?: boolean; - }): Promise => { - const mapModules = await lazyLoadMapModules(); - const initialLayers = []; - const regionMapLayerDescriptor = mapModules.createRegionMapLayerDescriptor({ - label, - emsLayerId, - leftFieldName, - termsFieldName, - termsSize, - colorSchema, - indexPatternId, - indexPatternTitle, - metricAgg, - metricFieldName, - }); - if (regionMapLayerDescriptor) { - initialLayers.push(regionMapLayerDescriptor); - } - - return createMapUrl({ - initialLayers, - filters, - query, - timeRange, - hash: true, - getStartServices, - }); - }, -}); From baf2de5415c6c7ad5a29a95ff54f8376896d1b81 Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Fri, 25 Jun 2021 07:58:03 +0200 Subject: [PATCH 68/69] Dashboard locator (#102854) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add dashboard locator * feat: 🎸 expose dashboard locator from dashboard plugin * Use dashboard locator in dashboard drilldown * Add tests for dashboard locator * Fix dashboard drilldown tests after refactor * Deprecate dashboard URL generator * Fix TypeScript errors in exmaple plugin * Use correct type for dashboard locator * refactor: 💡 change "route" attribute to "path" * chore: 🤖 remove unused bundle Co-authored-by: Vadim Kibana Co-authored-by: Vadim Kibana <82822460+vadimkibana@users.noreply.github.com> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- src/plugins/dashboard/public/index.ts | 3 + src/plugins/dashboard/public/locator.test.ts | 323 ++++++++++++++++++ src/plugins/dashboard/public/locator.ts | 160 +++++++++ src/plugins/dashboard/public/plugin.tsx | 36 +- src/plugins/dashboard/public/url_generator.ts | 6 + .../ui_actions_enhanced_examples/kibana.json | 3 +- .../app1_to_dashboard_drilldown.ts | 9 +- .../app2_to_dashboard_drilldown.ts | 9 +- .../abstract_dashboard_drilldown.tsx | 28 +- ...embeddable_to_dashboard_drilldown.test.tsx | 37 +- .../embeddable_to_dashboard_drilldown.tsx | 23 +- 11 files changed, 578 insertions(+), 59 deletions(-) create mode 100644 src/plugins/dashboard/public/locator.test.ts create mode 100644 src/plugins/dashboard/public/locator.ts diff --git a/src/plugins/dashboard/public/index.ts b/src/plugins/dashboard/public/index.ts index c584b44286e07..ff7708689c221 100644 --- a/src/plugins/dashboard/public/index.ts +++ b/src/plugins/dashboard/public/index.ts @@ -22,11 +22,14 @@ export { DashboardUrlGenerator, DashboardFeatureFlagConfig, } from './plugin'; + export { DASHBOARD_APP_URL_GENERATOR, createDashboardUrlGenerator, DashboardUrlGeneratorState, } from './url_generator'; +export { DashboardAppLocator, DashboardAppLocatorParams } from './locator'; + export { DashboardSavedObject } from './saved_dashboards'; export { SavedDashboardPanel, DashboardContainerInput } from './types'; diff --git a/src/plugins/dashboard/public/locator.test.ts b/src/plugins/dashboard/public/locator.test.ts new file mode 100644 index 0000000000000..0b647ac00ce31 --- /dev/null +++ b/src/plugins/dashboard/public/locator.test.ts @@ -0,0 +1,323 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { DashboardAppLocatorDefinition } from './locator'; +import { hashedItemStore } from '../../kibana_utils/public'; +import { mockStorage } from '../../kibana_utils/public/storage/hashed_item_store/mock'; +import { esFilters } from '../../data/public'; + +describe('dashboard locator', () => { + beforeEach(() => { + // @ts-ignore + hashedItemStore.storage = mockStorage; + }); + + test('creates a link to a saved dashboard', async () => { + const definition = new DashboardAppLocatorDefinition({ + useHashedUrl: false, + getDashboardFilterFields: async (dashboardId: string) => [], + }); + const location = await definition.getLocation({}); + + expect(location).toMatchObject({ + app: 'dashboards', + path: '#/create?_a=()&_g=()', + state: {}, + }); + }); + + test('creates a link with global time range set up', async () => { + const definition = new DashboardAppLocatorDefinition({ + useHashedUrl: false, + getDashboardFilterFields: async (dashboardId: string) => [], + }); + const location = await definition.getLocation({ + timeRange: { to: 'now', from: 'now-15m', mode: 'relative' }, + }); + + expect(location).toMatchObject({ + app: 'dashboards', + path: '#/create?_a=()&_g=(time:(from:now-15m,mode:relative,to:now))', + state: {}, + }); + }); + + test('creates a link with filters, time range, refresh interval and query to a saved object', async () => { + const definition = new DashboardAppLocatorDefinition({ + useHashedUrl: false, + getDashboardFilterFields: async (dashboardId: string) => [], + }); + const location = await definition.getLocation({ + timeRange: { to: 'now', from: 'now-15m', mode: 'relative' }, + refreshInterval: { pause: false, value: 300 }, + dashboardId: '123', + filters: [ + { + meta: { + alias: null, + disabled: false, + negate: false, + }, + query: { query: 'hi' }, + }, + { + meta: { + alias: null, + disabled: false, + negate: false, + }, + query: { query: 'hi' }, + $state: { + store: esFilters.FilterStateStore.GLOBAL_STATE, + }, + }, + ], + query: { query: 'bye', language: 'kuery' }, + }); + + expect(location).toMatchObject({ + app: 'dashboards', + path: `#/view/123?_a=(filters:!((meta:(alias:!n,disabled:!f,negate:!f),query:(query:hi))),query:(language:kuery,query:bye))&_g=(filters:!(('$state':(store:globalState),meta:(alias:!n,disabled:!f,negate:!f),query:(query:hi))),refreshInterval:(pause:!f,value:300),time:(from:now-15m,mode:relative,to:now))`, + state: {}, + }); + }); + + test('searchSessionId', async () => { + const definition = new DashboardAppLocatorDefinition({ + useHashedUrl: false, + getDashboardFilterFields: async (dashboardId: string) => [], + }); + const location = await definition.getLocation({ + timeRange: { to: 'now', from: 'now-15m', mode: 'relative' }, + refreshInterval: { pause: false, value: 300 }, + dashboardId: '123', + filters: [], + query: { query: 'bye', language: 'kuery' }, + searchSessionId: '__sessionSearchId__', + }); + + expect(location).toMatchObject({ + app: 'dashboards', + path: `#/view/123?_a=(filters:!(),query:(language:kuery,query:bye))&_g=(filters:!(),refreshInterval:(pause:!f,value:300),time:(from:now-15m,mode:relative,to:now))&searchSessionId=__sessionSearchId__`, + state: {}, + }); + }); + + test('savedQuery', async () => { + const definition = new DashboardAppLocatorDefinition({ + useHashedUrl: false, + getDashboardFilterFields: async (dashboardId: string) => [], + }); + const location = await definition.getLocation({ + savedQuery: '__savedQueryId__', + }); + + expect(location).toMatchObject({ + app: 'dashboards', + path: `#/create?_a=(savedQuery:__savedQueryId__)&_g=()`, + state: {}, + }); + expect(location.path).toContain('__savedQueryId__'); + }); + + test('panels', async () => { + const definition = new DashboardAppLocatorDefinition({ + useHashedUrl: false, + getDashboardFilterFields: async (dashboardId: string) => [], + }); + const location = await definition.getLocation({ + panels: [{ fakePanelContent: 'fakePanelContent' }] as any, + }); + + expect(location).toMatchObject({ + app: 'dashboards', + path: `#/create?_a=(panels:!((fakePanelContent:fakePanelContent)))&_g=()`, + state: {}, + }); + }); + + test('if no useHash setting is given, uses the one was start services', async () => { + const definition = new DashboardAppLocatorDefinition({ + useHashedUrl: true, + getDashboardFilterFields: async (dashboardId: string) => [], + }); + const location = await definition.getLocation({ + timeRange: { to: 'now', from: 'now-15m', mode: 'relative' }, + }); + + expect(location.path.indexOf('relative')).toBe(-1); + }); + + test('can override a false useHash ui setting', async () => { + const definition = new DashboardAppLocatorDefinition({ + useHashedUrl: false, + getDashboardFilterFields: async (dashboardId: string) => [], + }); + const location = await definition.getLocation({ + timeRange: { to: 'now', from: 'now-15m', mode: 'relative' }, + useHash: true, + }); + + expect(location.path.indexOf('relative')).toBe(-1); + }); + + test('can override a true useHash ui setting', async () => { + const definition = new DashboardAppLocatorDefinition({ + useHashedUrl: true, + getDashboardFilterFields: async (dashboardId: string) => [], + }); + const location = await definition.getLocation({ + timeRange: { to: 'now', from: 'now-15m', mode: 'relative' }, + useHash: false, + }); + + expect(location.path.indexOf('relative')).toBeGreaterThan(1); + }); + + describe('preserving saved filters', () => { + const savedFilter1 = { + meta: { + alias: null, + disabled: false, + negate: false, + }, + query: { query: 'savedfilter1' }, + }; + + const savedFilter2 = { + meta: { + alias: null, + disabled: false, + negate: false, + }, + query: { query: 'savedfilter2' }, + }; + + const appliedFilter = { + meta: { + alias: null, + disabled: false, + negate: false, + }, + query: { query: 'appliedfilter' }, + }; + + test('attaches filters from destination dashboard', async () => { + const definition = new DashboardAppLocatorDefinition({ + useHashedUrl: false, + getDashboardFilterFields: async (dashboardId: string) => { + return dashboardId === 'dashboard1' + ? [savedFilter1] + : dashboardId === 'dashboard2' + ? [savedFilter2] + : []; + }, + }); + + const location1 = await definition.getLocation({ + dashboardId: 'dashboard1', + filters: [appliedFilter], + }); + + expect(location1.path).toEqual(expect.stringContaining('query:savedfilter1')); + expect(location1.path).toEqual(expect.stringContaining('query:appliedfilter')); + + const location2 = await definition.getLocation({ + dashboardId: 'dashboard2', + filters: [appliedFilter], + }); + + expect(location2.path).toEqual(expect.stringContaining('query:savedfilter2')); + expect(location2.path).toEqual(expect.stringContaining('query:appliedfilter')); + }); + + test("doesn't fail if can't retrieve filters from destination dashboard", async () => { + const definition = new DashboardAppLocatorDefinition({ + useHashedUrl: false, + getDashboardFilterFields: async (dashboardId: string) => { + if (dashboardId === 'dashboard1') { + throw new Error('Not found'); + } + return []; + }, + }); + + const location = await definition.getLocation({ + dashboardId: 'dashboard1', + filters: [appliedFilter], + }); + + expect(location.path).not.toEqual(expect.stringContaining('query:savedfilter1')); + expect(location.path).toEqual(expect.stringContaining('query:appliedfilter')); + }); + + test('can enforce empty filters', async () => { + const definition = new DashboardAppLocatorDefinition({ + useHashedUrl: false, + getDashboardFilterFields: async (dashboardId: string) => { + if (dashboardId === 'dashboard1') { + return [savedFilter1]; + } + return []; + }, + }); + + const location = await definition.getLocation({ + dashboardId: 'dashboard1', + filters: [], + preserveSavedFilters: false, + }); + + expect(location.path).not.toEqual(expect.stringContaining('query:savedfilter1')); + expect(location.path).not.toEqual(expect.stringContaining('query:appliedfilter')); + expect(location.path).toMatchInlineSnapshot( + `"#/view/dashboard1?_a=(filters:!())&_g=(filters:!())"` + ); + }); + + test('no filters in result url if no filters applied', async () => { + const definition = new DashboardAppLocatorDefinition({ + useHashedUrl: false, + getDashboardFilterFields: async (dashboardId: string) => { + if (dashboardId === 'dashboard1') { + return [savedFilter1]; + } + return []; + }, + }); + + const location = await definition.getLocation({ + dashboardId: 'dashboard1', + }); + + expect(location.path).not.toEqual(expect.stringContaining('filters')); + expect(location.path).toMatchInlineSnapshot(`"#/view/dashboard1?_a=()&_g=()"`); + }); + + test('can turn off preserving filters', async () => { + const definition = new DashboardAppLocatorDefinition({ + useHashedUrl: false, + getDashboardFilterFields: async (dashboardId: string) => { + if (dashboardId === 'dashboard1') { + return [savedFilter1]; + } + return []; + }, + }); + + const location = await definition.getLocation({ + dashboardId: 'dashboard1', + filters: [appliedFilter], + preserveSavedFilters: false, + }); + + expect(location.path).not.toEqual(expect.stringContaining('query:savedfilter1')); + expect(location.path).toEqual(expect.stringContaining('query:appliedfilter')); + }); + }); +}); diff --git a/src/plugins/dashboard/public/locator.ts b/src/plugins/dashboard/public/locator.ts new file mode 100644 index 0000000000000..e154351819ee9 --- /dev/null +++ b/src/plugins/dashboard/public/locator.ts @@ -0,0 +1,160 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { SerializableState } from 'src/plugins/kibana_utils/common'; +import type { TimeRange, Filter, Query, QueryState, RefreshInterval } from '../../data/public'; +import type { LocatorDefinition, LocatorPublic } from '../../share/public'; +import type { SavedDashboardPanel } from '../common/types'; +import { esFilters } from '../../data/public'; +import { setStateToKbnUrl } from '../../kibana_utils/public'; +import { ViewMode } from '../../embeddable/public'; +import { DashboardConstants } from './dashboard_constants'; + +const cleanEmptyKeys = (stateObj: Record) => { + Object.keys(stateObj).forEach((key) => { + if (stateObj[key] === undefined) { + delete stateObj[key]; + } + }); + return stateObj; +}; + +export const DASHBOARD_APP_LOCATOR = 'DASHBOARD_APP_LOCATOR'; + +export interface DashboardAppLocatorParams extends SerializableState { + /** + * If given, the dashboard saved object with this id will be loaded. If not given, + * a new, unsaved dashboard will be loaded up. + */ + dashboardId?: string; + /** + * Optionally set the time range in the time picker. + */ + timeRange?: TimeRange; + + /** + * Optionally set the refresh interval. + */ + refreshInterval?: RefreshInterval & SerializableState; + + /** + * Optionally apply filers. NOTE: if given and used in conjunction with `dashboardId`, and the + * saved dashboard has filters saved with it, this will _replace_ those filters. + */ + filters?: Filter[]; + /** + * Optionally set a query. NOTE: if given and used in conjunction with `dashboardId`, and the + * saved dashboard has a query saved with it, this will _replace_ that query. + */ + query?: Query; + /** + * If not given, will use the uiSettings configuration for `storeInSessionStorage`. useHash determines + * whether to hash the data in the url to avoid url length issues. + */ + useHash?: boolean; + + /** + * When `true` filters from saved filters from destination dashboard as merged with applied filters + * When `false` applied filters take precedence and override saved filters + * + * true is default + */ + preserveSavedFilters?: boolean; + + /** + * View mode of the dashboard. + */ + viewMode?: ViewMode; + + /** + * Search search session ID to restore. + * (Background search) + */ + searchSessionId?: string; + + /** + * List of dashboard panels + */ + panels?: SavedDashboardPanel[] & SerializableState; + + /** + * Saved query ID + */ + savedQuery?: string; +} + +export type DashboardAppLocator = LocatorPublic; + +export interface DashboardAppLocatorDependencies { + useHashedUrl: boolean; + getDashboardFilterFields: (dashboardId: string) => Promise; +} + +export class DashboardAppLocatorDefinition implements LocatorDefinition { + public readonly id = DASHBOARD_APP_LOCATOR; + + constructor(protected readonly deps: DashboardAppLocatorDependencies) {} + + public readonly getLocation = async (params: DashboardAppLocatorParams) => { + const useHash = params.useHash ?? this.deps.useHashedUrl; + const hash = params.dashboardId ? `view/${params.dashboardId}` : `create`; + + const getSavedFiltersFromDestinationDashboardIfNeeded = async (): Promise => { + if (params.preserveSavedFilters === false) return []; + if (!params.dashboardId) return []; + try { + return await this.deps.getDashboardFilterFields(params.dashboardId); + } catch (e) { + // In case dashboard is missing, build the url without those filters. + // The Dashboard app will handle redirect to landing page with a toast message. + return []; + } + }; + + // leave filters `undefined` if no filters was applied + // in this case dashboard will restore saved filters on its own + const filters = params.filters && [ + ...(await getSavedFiltersFromDestinationDashboardIfNeeded()), + ...params.filters, + ]; + + let path = setStateToKbnUrl( + '_a', + cleanEmptyKeys({ + query: params.query, + filters: filters?.filter((f) => !esFilters.isFilterPinned(f)), + viewMode: params.viewMode, + panels: params.panels, + savedQuery: params.savedQuery, + }), + { useHash }, + `#/${hash}` + ); + + path = setStateToKbnUrl( + '_g', + cleanEmptyKeys({ + time: params.timeRange, + filters: filters?.filter((f) => esFilters.isFilterPinned(f)), + refreshInterval: params.refreshInterval, + }), + { useHash }, + path + ); + + if (params.searchSessionId) { + path = `${path}&${DashboardConstants.SEARCH_SESSION_ID}=${params.searchSessionId}`; + } + + return { + app: DashboardConstants.DASHBOARDS_ID, + path, + state: {}, + }; + }; +} diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index b5d6eda71ca4a..53a8e90a8c35c 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -72,6 +72,7 @@ import { DASHBOARD_APP_URL_GENERATOR, DashboardUrlGeneratorState, } from './url_generator'; +import { DashboardAppLocatorDefinition, DashboardAppLocator } from './locator'; import { createSavedDashboardLoader } from './saved_dashboards'; import { DashboardConstants } from './dashboard_constants'; import { PlaceholderEmbeddableFactory } from './application/embeddable/placeholder'; @@ -121,14 +122,25 @@ export interface DashboardStartDependencies { visualizations: VisualizationsStart; } -export type DashboardSetup = void; +export interface DashboardSetup { + locator?: DashboardAppLocator; +} export interface DashboardStart { getSavedDashboardLoader: () => SavedObjectLoader; getDashboardContainerByValueRenderer: () => ReturnType< typeof createDashboardContainerByValueRenderer >; + /** + * @deprecated Use dashboard locator instead. Dashboard locator is available + * under `.locator` key. This dashboard URL generator will be removed soon. + * + * ```ts + * plugins.dashboard.locator.getLocation({ ... }); + * ``` + */ dashboardUrlGenerator?: DashboardUrlGenerator; + locator?: DashboardAppLocator; dashboardFeatureFlagConfig: DashboardFeatureFlagConfig; } @@ -142,7 +154,11 @@ export class DashboardPlugin private currentHistory: ScopedHistory | undefined = undefined; private dashboardFeatureFlagConfig?: DashboardFeatureFlagConfig; + /** + * @deprecated Use locator instead. + */ private dashboardUrlGenerator?: DashboardUrlGenerator; + private locator?: DashboardAppLocator; public setup( core: CoreSetup, @@ -205,6 +221,19 @@ export class DashboardPlugin }; }; + if (share) { + this.locator = share.url.locators.create( + new DashboardAppLocatorDefinition({ + useHashedUrl: core.uiSettings.get('state:storeInSessionStorage'), + getDashboardFilterFields: async (dashboardId: string) => { + const [, , selfStart] = await core.getStartServices(); + const dashboard = await selfStart.getSavedDashboardLoader().get(dashboardId); + return dashboard?.searchSource?.getField('filter') ?? []; + }, + }) + ); + } + const { appMounted, appUnMounted, @@ -333,6 +362,10 @@ export class DashboardPlugin order: 100, }); } + + return { + locator: this.locator, + }; } public start(core: CoreStart, plugins: DashboardStartDependencies): DashboardStart { @@ -417,6 +450,7 @@ export class DashboardPlugin }); }, dashboardUrlGenerator: this.dashboardUrlGenerator, + locator: this.locator, dashboardFeatureFlagConfig: this.dashboardFeatureFlagConfig!, }; } diff --git a/src/plugins/dashboard/public/url_generator.ts b/src/plugins/dashboard/public/url_generator.ts index 58036ef70fa4a..5c0cd32ee5a16 100644 --- a/src/plugins/dashboard/public/url_generator.ts +++ b/src/plugins/dashboard/public/url_generator.ts @@ -26,6 +26,9 @@ export const GLOBAL_STATE_STORAGE_KEY = '_g'; export const DASHBOARD_APP_URL_GENERATOR = 'DASHBOARD_APP_URL_GENERATOR'; +/** + * @deprecated Use dashboard locator instead. + */ export interface DashboardUrlGeneratorState { /** * If given, the dashboard saved object with this id will be loaded. If not given, @@ -88,6 +91,9 @@ export interface DashboardUrlGeneratorState { savedQuery?: string; } +/** + * @deprecated Use dashboard locator instead. + */ export const createDashboardUrlGenerator = ( getStartServices: () => Promise<{ appBasePath: string; diff --git a/x-pack/examples/ui_actions_enhanced_examples/kibana.json b/x-pack/examples/ui_actions_enhanced_examples/kibana.json index 4f5ac8519fe5b..59a0926118962 100644 --- a/x-pack/examples/ui_actions_enhanced_examples/kibana.json +++ b/x-pack/examples/ui_actions_enhanced_examples/kibana.json @@ -19,7 +19,6 @@ "dashboardEnhanced", "embeddable", "kibanaUtils", - "kibanaReact", - "share" + "kibanaReact" ] } diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/drilldowns/app1_to_dashboard_drilldown/app1_to_dashboard_drilldown.ts b/x-pack/examples/ui_actions_enhanced_examples/public/drilldowns/app1_to_dashboard_drilldown/app1_to_dashboard_drilldown.ts index 3efe2fed9a78a..1bb911530dc50 100644 --- a/x-pack/examples/ui_actions_enhanced_examples/public/drilldowns/app1_to_dashboard_drilldown/app1_to_dashboard_drilldown.ts +++ b/x-pack/examples/ui_actions_enhanced_examples/public/drilldowns/app1_to_dashboard_drilldown/app1_to_dashboard_drilldown.ts @@ -10,7 +10,7 @@ import { DashboardEnhancedAbstractDashboardDrilldownConfig as Config, } from '../../../../../plugins/dashboard_enhanced/public'; import { SAMPLE_APP1_CLICK_TRIGGER, SampleApp1ClickContext } from '../../triggers'; -import { KibanaURL } from '../../../../../../src/plugins/share/public'; +import { KibanaLocation } from '../../../../../../src/plugins/share/public'; export const APP1_TO_DASHBOARD_DRILLDOWN = 'APP1_TO_DASHBOARD_DRILLDOWN'; @@ -21,12 +21,11 @@ export class App1ToDashboardDrilldown extends AbstractDashboardDrilldown [SAMPLE_APP1_CLICK_TRIGGER]; - protected async getURL(config: Config, context: Context): Promise { - const path = await this.urlGenerator.createUrl({ + protected async getLocation(config: Config, context: Context): Promise { + const location = await this.locator.getLocation({ dashboardId: config.dashboardId, }); - const url = new KibanaURL(path); - return url; + return location; } } diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/drilldowns/app2_to_dashboard_drilldown/app2_to_dashboard_drilldown.ts b/x-pack/examples/ui_actions_enhanced_examples/public/drilldowns/app2_to_dashboard_drilldown/app2_to_dashboard_drilldown.ts index 7245e37414686..daac998872c14 100644 --- a/x-pack/examples/ui_actions_enhanced_examples/public/drilldowns/app2_to_dashboard_drilldown/app2_to_dashboard_drilldown.ts +++ b/x-pack/examples/ui_actions_enhanced_examples/public/drilldowns/app2_to_dashboard_drilldown/app2_to_dashboard_drilldown.ts @@ -10,7 +10,7 @@ import { DashboardEnhancedAbstractDashboardDrilldownConfig as Config, } from '../../../../../plugins/dashboard_enhanced/public'; import { SAMPLE_APP2_CLICK_TRIGGER, SampleApp2ClickContext } from '../../triggers'; -import { KibanaURL } from '../../../../../../src/plugins/share/public'; +import { KibanaLocation } from '../../../../../../src/plugins/share/public'; export const APP2_TO_DASHBOARD_DRILLDOWN = 'APP2_TO_DASHBOARD_DRILLDOWN'; @@ -21,12 +21,11 @@ export class App2ToDashboardDrilldown extends AbstractDashboardDrilldown [SAMPLE_APP2_CLICK_TRIGGER]; - protected async getURL(config: Config, context: Context): Promise { - const path = await this.urlGenerator.createUrl({ + protected async getLocation(config: Config, context: Context): Promise { + const location = await this.locator.getLocation({ dashboardId: config.dashboardId, }); - const url = new KibanaURL(path); - return url; + return location; } } diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/abstract_dashboard_drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/abstract_dashboard_drilldown.tsx index 1b1fecb2bcaf7..6f02946596a87 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/abstract_dashboard_drilldown.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/abstract_dashboard_drilldown.tsx @@ -5,6 +5,7 @@ * 2.0. */ +import type { KibanaLocation } from 'src/plugins/share/public'; import React from 'react'; import { DataPublicPluginStart } from 'src/plugins/data/public'; import { DashboardStart } from 'src/plugins/dashboard/public'; @@ -20,7 +21,6 @@ import { CollectConfigProps, StartServicesGetter, } from '../../../../../../../src/plugins/kibana_utils/public'; -import { KibanaURL } from '../../../../../../../src/plugins/share/public'; import { Config } from './types'; export interface Params { @@ -39,7 +39,7 @@ export abstract class AbstractDashboardDrilldown string[]; - protected abstract getURL(config: Config, context: Context): Promise; + protected abstract getLocation(config: Config, context: Context): Promise; public readonly order = 100; @@ -65,19 +65,25 @@ export abstract class AbstractDashboardDrilldown => { - const url = await this.getURL(config, context); - return url.path; + const { app, path } = await this.getLocation(config, context); + const url = await this.params.start().core.application.getUrlForApp(app, { + path, + absolute: true, + }); + return url; }; public readonly execute = async (config: Config, context: Context) => { - const url = await this.getURL(config, context); - await this.params.start().core.application.navigateToApp(url.appName, { path: url.appPath }); + const { app, path, state } = await this.getLocation(config, context); + await this.params.start().core.application.navigateToApp(app, { + path, + state, + }); }; - protected get urlGenerator() { - const urlGenerator = this.params.start().plugins.dashboard.dashboardUrlGenerator; - if (!urlGenerator) - throw new Error('Dashboard URL generator is required for dashboard drilldown.'); - return urlGenerator; + protected get locator() { + const locator = this.params.start().plugins.dashboard.locator; + if (!locator) throw new Error('Dashboard locator is required for dashboard drilldown.'); + return locator; } } diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.test.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.test.tsx index cd82e23544ad0..a817d9f65c916 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.test.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.test.tsx @@ -7,7 +7,7 @@ import { EmbeddableToDashboardDrilldown } from './embeddable_to_dashboard_drilldown'; import { AbstractDashboardDrilldownConfig as Config } from '../abstract_dashboard_drilldown'; -import { coreMock, savedObjectsServiceMock } from '../../../../../../../src/core/public/mocks'; +import { savedObjectsServiceMock } from '../../../../../../../src/core/public/mocks'; import { Filter, FilterStateStore, @@ -19,10 +19,11 @@ import { ApplyGlobalFilterActionContext, esFilters, } from '../../../../../../../src/plugins/data/public'; -import { createDashboardUrlGenerator } from '../../../../../../../src/plugins/dashboard/public/url_generator'; -import { UrlGeneratorsService } from '../../../../../../../src/plugins/share/public/url_generators'; +import { + DashboardAppLocatorDefinition, + DashboardAppLocatorParams, +} from '../../../../../../../src/plugins/dashboard/public/locator'; import { StartDependencies } from '../../../plugin'; -import { SavedObjectLoader } from '../../../../../../../src/plugins/saved_objects/public'; import { StartServicesGetter } from '../../../../../../../src/plugins/kibana_utils/public/core'; import { EnhancedEmbeddableContext } from '../../../../../embeddable_enhanced/public'; @@ -74,13 +75,6 @@ test('inject/extract are defined', () => { }); describe('.execute() & getHref', () => { - /** - * A convenience test setup helper - * Beware: `dataPluginMock.createStartContract().actions` and extracting filters from event is mocked! - * The url generation is not mocked and uses real implementation - * So this tests are mostly focused on making sure the filters returned from `dataPluginMock.createStartContract().actions` helpers - * end up in resulting navigation path - */ async function setupTestBed( config: Partial, embeddableInput: { filters?: Filter[]; timeRange?: TimeRange; query?: Query }, @@ -90,7 +84,10 @@ describe('.execute() & getHref', () => { const navigateToApp = jest.fn(); const getUrlForApp = jest.fn((app, opt) => `${app}/${opt.path}`); const savedObjectsClient = savedObjectsServiceMock.createStartContract().client; - + const definition = new DashboardAppLocatorDefinition({ + useHashedUrl: false, + getDashboardFilterFields: async () => [], + }); const drilldown = new EmbeddableToDashboardDrilldown({ start: ((() => ({ core: { @@ -105,17 +102,11 @@ describe('.execute() & getHref', () => { plugins: { uiActionsEnhanced: {}, dashboard: { - dashboardUrlGenerator: new UrlGeneratorsService() - .setup(coreMock.createSetup()) - .registerUrlGenerator( - createDashboardUrlGenerator(() => - Promise.resolve({ - appBasePath: 'xyz/app/dashboards', - useHashedUrl: false, - savedDashboardLoader: ({} as unknown) as SavedObjectLoader, - }) - ) - ), + locator: { + getLocation: async (params: DashboardAppLocatorParams) => { + return await definition.getLocation(params); + }, + }, }, }, self: {}, diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.tsx index 7b55244985981..97355d9eb435e 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.tsx @@ -5,7 +5,8 @@ * 2.0. */ -import { DashboardUrlGeneratorState } from '../../../../../../../src/plugins/dashboard/public'; +import type { KibanaLocation } from 'src/plugins/share/public'; +import { DashboardAppLocatorParams } from '../../../../../../../src/plugins/dashboard/public'; import { ApplyGlobalFilterActionContext, APPLY_FILTER_TRIGGER, @@ -23,7 +24,6 @@ import { AbstractDashboardDrilldownParams, AbstractDashboardDrilldownConfig as Config, } from '../abstract_dashboard_drilldown'; -import { KibanaURL } from '../../../../../../../src/plugins/share/public'; import { EMBEDDABLE_TO_DASHBOARD_DRILLDOWN } from './constants'; import { createExtract, createInject } from '../../../../common'; import { EnhancedEmbeddableContext } from '../../../../../embeddable_enhanced/public'; @@ -49,26 +49,26 @@ export class EmbeddableToDashboardDrilldown extends AbstractDashboardDrilldown [APPLY_FILTER_TRIGGER]; - protected async getURL(config: Config, context: Context): Promise { - const state: DashboardUrlGeneratorState = { + protected async getLocation(config: Config, context: Context): Promise { + const params: DashboardAppLocatorParams = { dashboardId: config.dashboardId, }; if (context.embeddable) { const embeddable = context.embeddable as IEmbeddable; const input = embeddable.getInput(); - if (isQuery(input.query) && config.useCurrentFilters) state.query = input.query; + if (isQuery(input.query) && config.useCurrentFilters) params.query = input.query; // if useCurrentDashboardDataRange is enabled, then preserve current time range // if undefined is passed, then destination dashboard will figure out time range itself // for brush event this time range would be overwritten if (isTimeRange(input.timeRange) && config.useCurrentDateRange) - state.timeRange = input.timeRange; + params.timeRange = input.timeRange; // if useCurrentDashboardFilters enabled, then preserve all the filters (pinned and unpinned) // otherwise preserve only pinned if (isFilters(input.filters)) - state.filters = config.useCurrentFilters + params.filters = config.useCurrentFilters ? input.filters : input.filters?.filter((f) => esFilters.isFilterPinned(f)); } @@ -79,17 +79,16 @@ export class EmbeddableToDashboardDrilldown extends AbstractDashboardDrilldown Date: Fri, 25 Jun 2021 11:03:46 +0300 Subject: [PATCH 69/69] [Osquery] Return proper indices permissions for osquery_manager package (#103363) --- ...kage_policies_to_agent_permissions.test.ts | 104 ++++++++++++++++++ .../package_policies_to_agent_permissions.ts | 7 ++ 2 files changed, 111 insertions(+) diff --git a/x-pack/plugins/fleet/server/services/package_policies_to_agent_permissions.test.ts b/x-pack/plugins/fleet/server/services/package_policies_to_agent_permissions.test.ts index 39759a6fc9e9c..a84118cdf1bfa 100644 --- a/x-pack/plugins/fleet/server/services/package_policies_to_agent_permissions.test.ts +++ b/x-pack/plugins/fleet/server/services/package_policies_to_agent_permissions.test.ts @@ -274,6 +274,110 @@ describe('storedPackagePoliciesToAgentPermissions()', () => { }, }); }); + + it('Returns the dataset for osquery_manager package', async () => { + getPackageInfoMock.mockResolvedValueOnce({ + format_version: '1.0.0', + name: 'osquery_manager', + title: 'Osquery Manager', + version: '0.3.0', + license: 'basic', + description: + 'Centrally manage osquery deployments, run live queries, and schedule recurring queries', + type: 'integration', + release: 'beta', + categories: ['security', 'os_system', 'config_management'], + icons: [ + { + src: '/img/logo_osquery.svg', + title: 'logo osquery', + size: '32x32', + type: 'image/svg+xml', + }, + ], + owner: { github: 'elastic/integrations' }, + readme: '/package/osquery_manager/0.3.0/docs/README.md', + data_streams: [ + { + dataset: 'osquery_manager.result', + package: 'osquery_manager', + ingest_pipeline: 'default', + path: 'result', + streams: [], + title: 'Osquery Manager queries', + type: 'logs', + release: 'experimental', + }, + ], + latestVersion: '0.3.0', + removable: true, + notice: undefined, + status: 'not_installed', + assets: { + kibana: { + dashboard: [], + visualization: [], + search: [], + index_pattern: [], + map: [], + lens: [], + security_rule: [], + ml_module: [], + }, + elasticsearch: { + component_template: [], + ingest_pipeline: [], + ilm_policy: [], + transform: [], + index_template: [], + data_stream_ilm_policy: [], + }, + }, + }); + + const packagePolicies: PackagePolicy[] = [ + { + id: '12345', + name: 'test-policy', + namespace: 'test', + enabled: true, + package: { name: 'osquery_manager', version: '0.0.0', title: 'Test Package' }, + inputs: [ + { + type: 'osquery_manager', + enabled: true, + streams: [ + { + id: 'test-logs', + enabled: true, + data_stream: { type: 'logs', dataset: 'some-logs' }, + compiled_stream: { data_stream: { dataset: 'compiled' } }, + }, + ], + }, + ], + created_at: '', + updated_at: '', + created_by: '', + updated_by: '', + revision: 1, + policy_id: '', + output_id: '', + }, + ]; + + const permissions = await storedPackagePoliciesToAgentPermissions(soClient, packagePolicies); + expect(permissions).toMatchObject({ + 'test-policy': { + indices: [ + { + names: ['logs-osquery_manager.result-test'], + privileges: ['auto_configure', 'create_doc'], + }, + ], + }, + }); + }); }); describe('getDataStreamPermissions()', () => { diff --git a/x-pack/plugins/fleet/server/services/package_policies_to_agent_permissions.ts b/x-pack/plugins/fleet/server/services/package_policies_to_agent_permissions.ts index bd73b88e7c893..07ad892adc653 100644 --- a/x-pack/plugins/fleet/server/services/package_policies_to_agent_permissions.ts +++ b/x-pack/plugins/fleet/server/services/package_policies_to_agent_permissions.ts @@ -73,6 +73,13 @@ export async function storedPackagePoliciesToAgentPermissions( dataStreamsForPermissions = pkg.data_streams; break; + case 'osquery_manager': + // - Osquery manager doesn't store the `data_stream` metadata in + // `packagePolicy.inputs`, so we will use _all_ data_streams from + // the package. + dataStreamsForPermissions = pkg.data_streams; + break; + default: // - Normal packages store some of the `data_stream` metadata in // `packagePolicy.inputs[].streams[].data_stream`