From 35ff37a434e61d2475428ff474fce50ee9bb68b1 Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Fri, 17 Jul 2020 14:02:52 -0400 Subject: [PATCH 01/45] [Lens] Fix switching with layers (#71982) * [Lens] Fix chart switching with multiple layers * Unskip Lens smokescreen test * Fix types * Revert

change --- .../config_panel/config_panel.tsx | 2 +- .../config_panel/layer_panel.test.tsx | 6 +- .../editor_frame/config_panel/layer_panel.tsx | 2 +- .../workspace_panel/chart_switch.test.tsx | 53 +++++++++++ .../workspace_panel/chart_switch.tsx | 7 +- .../xy_visualization/xy_suggestions.test.ts | 92 +++++++++++++++++-- .../public/xy_visualization/xy_suggestions.ts | 16 +++- .../test/functional/apps/lens/smokescreen.ts | 26 ++++++ .../test/functional/page_objects/lens_page.ts | 28 ++++++ 9 files changed, 214 insertions(+), 18 deletions(-) diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx index 7f4a48fa2fda2..73126b814f256 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx @@ -129,7 +129,7 @@ function LayerPanels( className="lnsConfigPanel__addLayerBtn" fullWidth size="s" - data-test-subj="lnsXY_layer_add" + data-test-subj="lnsLayerAddButton" aria-label={i18n.translate('xpack.lens.xyChart.addLayerButton', { defaultMessage: 'Add layer', })} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx index 1f987f86d3950..9545bd3c840da 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx @@ -93,14 +93,14 @@ describe('LayerPanel', () => { describe('layer reset and remove', () => { it('should show the reset button when single layer', () => { const component = mountWithIntl(); - expect(component.find('[data-test-subj="lns_layer_remove"]').first().text()).toContain( + expect(component.find('[data-test-subj="lnsLayerRemove"]').first().text()).toContain( 'Reset layer' ); }); it('should show the delete button when multiple layers', () => { const component = mountWithIntl(); - expect(component.find('[data-test-subj="lns_layer_remove"]').first().text()).toContain( + expect(component.find('[data-test-subj="lnsLayerRemove"]').first().text()).toContain( 'Delete layer' ); }); @@ -109,7 +109,7 @@ describe('LayerPanel', () => { const cb = jest.fn(); const component = mountWithIntl(); act(() => { - component.find('[data-test-subj="lns_layer_remove"]').first().simulate('click'); + component.find('[data-test-subj="lnsLayerRemove"]').first().simulate('click'); }); expect(cb).toHaveBeenCalled(); }); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx index e51a155a19935..f72b1429967d2 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx @@ -429,7 +429,7 @@ export function LayerPanel( size="xs" iconType="trash" color="danger" - data-test-subj="lns_layer_remove" + data-test-subj="lnsLayerRemove" onClick={() => { // If we don't blur the remove / clear button, it remains focused // which is a strange UX in this case. e.target.blur doesn't work diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.test.tsx index 648bb5c03cb39..ceced2a7a353c 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.test.tsx @@ -46,6 +46,16 @@ describe('chart_switch', () => { }; } + /** + * There are three visualizations. Each one has the same suggestion behavior: + * + * visA: suggests an empty state + * visB: suggests an empty state + * visC: + * - Never switches to subvisC2 + * - Allows a switch to subvisC3 + * - Allows a switch to subvisC1 + */ function mockVisualizations() { return { visA: generateVisualization('visA'), @@ -292,6 +302,49 @@ describe('chart_switch', () => { expect(getMenuItem('visB', component).prop('betaBadgeIconType')).toEqual('alert'); }); + it('should support multi-layer suggestions without data loss', () => { + const dispatch = jest.fn(); + const visualizations = mockVisualizations(); + const frame = mockFrame(['a', 'b']); + + const datasourceMap = mockDatasourceMap(); + datasourceMap.testDatasource.getDatasourceSuggestionsFromCurrentState.mockReturnValue([ + { + state: {}, + table: { + columns: [ + { + columnId: 'a', + operation: { + label: '', + dataType: 'string', + isBucketed: true, + }, + }, + ], + isMultiRow: true, + layerId: 'a', + changeType: 'unchanged', + }, + keptLayerIds: ['a', 'b'], + }, + ]); + + const component = mount( + + ); + + expect(getMenuItem('visB', component).prop('betaBadgeIconType')).toBeUndefined(); + }); + it('should indicate data loss if no data will be used', () => { const dispatch = jest.fn(); const visualizations = mockVisualizations(); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx index fa87d80e5cf40..51b4a347af6f1 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx @@ -139,7 +139,7 @@ export function ChartSwitch(props: Props) { dataLoss = 'nothing'; } else if (!topSuggestion) { dataLoss = 'everything'; - } else if (layers.length > 1) { + } else if (layers.length > 1 && layers.length !== topSuggestion.keptLayerIds.length) { dataLoss = 'layers'; } else if (topSuggestion.columns !== layers[0][1].getTableSpec().length) { dataLoss = 'columns'; @@ -258,14 +258,15 @@ function getTopSuggestion( newVisualization: Visualization, subVisualizationId?: string ): Suggestion | undefined { - const suggestions = getSuggestions({ + const unfilteredSuggestions = getSuggestions({ datasourceMap: props.datasourceMap, datasourceStates: props.datasourceStates, visualizationMap: { [visualizationId]: newVisualization }, activeVisualizationId: props.visualizationId, visualizationState: props.visualizationState, subVisualizationId, - }).filter((suggestion) => { + }); + const suggestions = unfilteredSuggestions.filter((suggestion) => { // don't use extended versions of current data table on switching between visualizations // to avoid confusing the user. return ( diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts index f301206355060..f5828dbaeccc3 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts @@ -13,6 +13,7 @@ import { } from '../types'; import { State, XYState, visualizationTypes } from './types'; import { generateId } from '../id_generator'; +import { xyVisualization } from './xy_visualization'; jest.mock('../id_generator'); @@ -119,7 +120,33 @@ describe('xy_suggestions', () => { }); expect(suggestions).toHaveLength(visualizationTypes.length); - expect(suggestions.map(({ state }) => state.preferredSeriesType)).toEqual([ + expect(suggestions.map(({ state }) => xyVisualization.getVisualizationTypeId(state))).toEqual([ + 'bar_stacked', + 'area_stacked', + 'area', + 'line', + 'bar_horizontal_stacked', + 'bar_horizontal', + 'bar', + ]); + }); + + // This limitation is acceptable for now, but is now tested + test('is unable to generate layers when switching from a non-XY chart with multiple layers', () => { + (generateId as jest.Mock).mockReturnValueOnce('aaa'); + const suggestions = getSuggestions({ + table: { + isMultiRow: true, + columns: [numCol('bytes'), dateCol('date')], + layerId: 'first', + changeType: 'unchanged', + }, + keptLayerIds: ['first', 'second'], + }); + + expect(suggestions).toHaveLength(visualizationTypes.length); + expect(suggestions.map(({ state }) => state.layers.length)).toEqual([1, 1, 1, 1, 1, 1, 1]); + expect(suggestions.map(({ state }) => xyVisualization.getVisualizationTypeId(state))).toEqual([ 'bar_stacked', 'area_stacked', 'area', @@ -156,7 +183,51 @@ describe('xy_suggestions', () => { }); expect(suggestions).toHaveLength(visualizationTypes.length); - expect(suggestions.map(({ state }) => state.preferredSeriesType)).toEqual([ + expect(suggestions.map(({ state }) => xyVisualization.getVisualizationTypeId(state))).toEqual([ + 'line', + 'bar', + 'bar_horizontal', + 'bar_stacked', + 'bar_horizontal_stacked', + 'area', + 'area_stacked', + ]); + }); + + test('suggests all basic x y charts when switching from another x y chart with multiple layers', () => { + (generateId as jest.Mock).mockReturnValueOnce('aaa'); + const suggestions = getSuggestions({ + table: { + isMultiRow: true, + columns: [numCol('bytes'), dateCol('date')], + layerId: 'first', + changeType: 'unchanged', + }, + keptLayerIds: ['first', 'second'], + state: { + legend: { isVisible: true, position: 'bottom' }, + preferredSeriesType: 'bar', + layers: [ + { + layerId: 'first', + seriesType: 'bar', + xAccessor: 'date', + accessors: ['bytes'], + splitAccessor: undefined, + }, + { + layerId: 'second', + seriesType: 'bar', + xAccessor: undefined, + accessors: [], + splitAccessor: undefined, + }, + ], + }, + }); + + expect(suggestions).toHaveLength(visualizationTypes.length); + expect(suggestions.map(({ state }) => xyVisualization.getVisualizationTypeId(state))).toEqual([ 'line', 'bar', 'bar_horizontal', @@ -165,6 +236,15 @@ describe('xy_suggestions', () => { 'area', 'area_stacked', ]); + expect(suggestions.map(({ state }) => state.layers.map((l) => l.layerId))).toEqual([ + ['first', 'second'], + ['first', 'second'], + ['first', 'second'], + ['first', 'second'], + ['first', 'second'], + ['first', 'second'], + ['first', 'second'], + ]); }); test('suggests all basic x y chart with date on x', () => { @@ -388,7 +468,7 @@ describe('xy_suggestions', () => { changeType: 'unchanged', }, state: currentState, - keptLayerIds: [], + keptLayerIds: ['first'], }); expect(rest).toHaveLength(visualizationTypes.length - 2); @@ -497,7 +577,7 @@ describe('xy_suggestions', () => { changeType: 'extended', }, state: currentState, - keptLayerIds: [], + keptLayerIds: ['first'], }); expect(rest).toHaveLength(0); @@ -536,7 +616,7 @@ describe('xy_suggestions', () => { changeType: 'reorder', }, state: currentState, - keptLayerIds: [], + keptLayerIds: ['first'], }); expect(rest).toHaveLength(0); @@ -576,7 +656,7 @@ describe('xy_suggestions', () => { changeType: 'extended', }, state: currentState, - keptLayerIds: [], + keptLayerIds: ['first'], }); expect(rest).toHaveLength(0); diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts index e0bfbd266f8f1..d7348f00bf8b8 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts +++ b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts @@ -394,17 +394,25 @@ function buildSuggestion({ : undefined, }; + // Maintain consistent order for any layers that were saved const keptLayers = currentState - ? currentState.layers.filter( - (layer) => layer.layerId !== layerId && keptLayerIds.includes(layer.layerId) - ) + ? currentState.layers + // Remove layers that aren't being suggested + .filter((layer) => keptLayerIds.includes(layer.layerId)) + // Update in place + .map((layer) => (layer.layerId === layerId ? newLayer : layer)) + // Replace the seriesType on all previous layers + .map((layer) => ({ + ...layer, + seriesType, + })) : []; const state: State = { legend: currentState ? currentState.legend : { isVisible: true, position: Position.Right }, fittingFunction: currentState?.fittingFunction || 'None', preferredSeriesType: seriesType, - layers: [...keptLayers, newLayer], + layers: Object.keys(existingLayer).length ? keptLayers : [...keptLayers, newLayer], }; return { diff --git a/x-pack/test/functional/apps/lens/smokescreen.ts b/x-pack/test/functional/apps/lens/smokescreen.ts index 8bb5faf2469d7..23d4cc972675b 100644 --- a/x-pack/test/functional/apps/lens/smokescreen.ts +++ b/x-pack/test/functional/apps/lens/smokescreen.ts @@ -165,5 +165,31 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // legend item(s), so we're using a class selector here. expect(await find.allByCssSelector('.echLegendItem')).to.have.length(3); }); + + it('should switch from a multi-layer stacked bar to a multi-layer line chart', async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + await PageObjects.lens.goToTimeRange(); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension', + operation: 'date_histogram', + field: '@timestamp', + }); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension', + operation: 'avg', + field: 'bytes', + }); + + await PageObjects.lens.createLayer(); + + expect(await PageObjects.lens.hasChartSwitchWarning('line')).to.eql(false); + + await PageObjects.lens.switchToVisualization('line'); + + expect(await PageObjects.lens.getLayerCount()).to.eql(2); + }); }); } diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index 4252c400ff1cd..d101c9754d562 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -167,5 +167,33 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont await testSubjects.existOrFail('visTypeTitle'); }); }, + + /** + * Checks a specific subvisualization in the chart switcher for a "data loss" indicator + * + * @param subVisualizationId - the ID of the sub-visualization to switch to, such as + * lnsDatatable or bar_stacked + */ + async hasChartSwitchWarning(subVisualizationId: string) { + await this.openChartSwitchPopover(); + + const element = await testSubjects.find(`lnsChartSwitchPopover_${subVisualizationId}`); + return await testSubjects.descendantExists('euiKeyPadMenuItem__betaBadgeWrapper', element); + }, + + /** + * Returns the number of layers visible in the chart configuration + */ + async getLayerCount() { + const elements = await testSubjects.findAll('lnsLayerRemove'); + return elements.length; + }, + + /** + * Adds a new layer to the chart, fails if the chart does not support new layers + */ + async createLayer() { + await testSubjects.click('lnsLayerAddButton'); + }, }); } From 3cef292bbd52edb94d87ec73401553e8450a8736 Mon Sep 17 00:00:00 2001 From: Quynh Nguyen <43350163+qn895@users.noreply.github.com> Date: Fri, 17 Jul 2020 13:35:25 -0500 Subject: [PATCH 02/45] [ML] Fix annotations pagination & change labels from letters to numbers (#72204) --- .../__snapshots__/annotations_table.test.js.snap | 1 + .../annotations_table/annotations_table.js | 13 ++++++++----- .../public/application/explorer/explorer_utils.js | 2 +- .../timeseriesexplorer_utils/get_focus_data.ts | 4 ++-- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/ml/public/application/components/annotations/annotations_table/__snapshots__/annotations_table.test.js.snap b/x-pack/plugins/ml/public/application/components/annotations/annotations_table/__snapshots__/annotations_table.test.js.snap index 63ec1744b62d0..9eb44c71aa799 100644 --- a/x-pack/plugins/ml/public/application/components/annotations/annotations_table/__snapshots__/annotations_table.test.js.snap +++ b/x-pack/plugins/ml/public/application/components/annotations/annotations_table/__snapshots__/annotations_table.test.js.snap @@ -77,6 +77,7 @@ exports[`AnnotationsTable Initialization with annotations prop. 1`] = ` "dataType": "boolean", "field": "current_series", "name": "current_series", + "render": [Function], "width": "0px", }, ] diff --git a/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js b/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js index cf4d25f159a1a..86398a57c3a45 100644 --- a/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js +++ b/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js @@ -13,7 +13,7 @@ import _ from 'lodash'; import PropTypes from 'prop-types'; import rison from 'rison-node'; import React, { Component, Fragment } from 'react'; - +import memoizeOne from 'memoize-one'; import { EuiBadge, EuiButtonIcon, @@ -130,7 +130,7 @@ export class AnnotationsTable extends Component { } } - getAnnotationsWithExtraInfo(annotations) { + getAnnotationsWithExtraInfo = memoizeOne((annotations) => { // if there is a specific view/chart entities that the annotations can be scoped to // add a new column called 'current_series' if (Array.isArray(this.props.chartDetails?.entityData?.entities)) { @@ -147,7 +147,7 @@ export class AnnotationsTable extends Component { // if not make it return the original annotations return annotations; } - } + }); getJob(jobId) { // check if the job was supplied via props and matches the supplied jobId @@ -438,7 +438,7 @@ export class AnnotationsTable extends Component { name: i18n.translate('xpack.ml.annotationsTable.labelColumnName', { defaultMessage: 'Label', }), - sortable: true, + sortable: (key) => +key, width: '60px', render: (key) => { return {key}; @@ -644,15 +644,18 @@ export class AnnotationsTable extends Component { name: CURRENT_SERIES, dataType: 'boolean', width: '0px', + render: () => '', } ); + + const items = this.getAnnotationsWithExtraInfo(annotations); return ( { - d.key = String.fromCharCode(65 + i); + d.key = (i + 1).toString(); return d; }), aggregations: resp.aggregations, diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/get_focus_data.ts b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/get_focus_data.ts index 8bac9a51af174..d213d371f1d90 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/get_focus_data.ts +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/get_focus_data.ts @@ -156,8 +156,8 @@ export function getFocusData( .sort((a, b) => { return a.timestamp - b.timestamp; }) - .map((d, i) => { - d.key = String.fromCharCode(65 + i); + .map((d, i: number) => { + d.key = (i + 1).toString(); return d; }); From 4c58018d337330348b858d32cc01834e3e722c43 Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Fri, 17 Jul 2020 16:27:25 -0400 Subject: [PATCH 03/45] skip flaky suite (#72339) --- .../cypress/integration/timeline_local_storage.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/cypress/integration/timeline_local_storage.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timeline_local_storage.spec.ts index 383ebe2220585..7c047459c56cc 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timeline_local_storage.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timeline_local_storage.spec.ts @@ -13,7 +13,8 @@ import { TABLE_COLUMN_EVENTS_MESSAGE } from '../screens/hosts/external_events'; import { waitsForEventsToBeLoaded, openEventsViewerFieldsBrowser } from '../tasks/hosts/events'; import { removeColumn, resetFields } from '../tasks/timeline'; -describe('persistent timeline', () => { +// Failing: See https://github.com/elastic/kibana/issues/72339 +describe.skip('persistent timeline', () => { before(() => { loginAndWaitForPage(HOSTS_URL); openEvents(); From 5356941f22ccfb358f4cffab6a20bfb5a1bc9cff Mon Sep 17 00:00:00 2001 From: Madison Caldwell Date: Fri, 17 Jul 2020 16:51:28 -0400 Subject: [PATCH 04/45] [Security Solution][Endpoint][Exceptions] Only write manifest to policy when there are changes (#72000) * Refactor security_solution policy creation callback - part 1 * Fix manifest dispatch * Change how dispatches are performed * simplify manifest types * Remove unused mock * Fix tests * one place to construct artifact ids * fixing linter exceptions * Add tests for stable hashes * Additional testing and type cleanup * Remove unnecessary log * Minor fixup * jsdoc * type fixup * Additional type adjustments --- x-pack/plugins/ingest_manager/common/mocks.ts | 87 ++++++ .../common/endpoint/schema/common.ts | 5 + .../common/endpoint/schema/manifest.ts | 46 ++- .../endpoint/ingest_integration.test.ts | 105 ++++--- .../server/endpoint/ingest_integration.ts | 156 +++++----- .../server/endpoint/lib/artifacts/common.ts | 16 +- .../endpoint/lib/artifacts/lists.test.ts | 94 +++++- .../server/endpoint/lib/artifacts/lists.ts | 33 +- .../endpoint/lib/artifacts/manifest.test.ts | 158 +++++----- .../server/endpoint/lib/artifacts/manifest.ts | 97 +++++- .../lib/artifacts/manifest_entry.test.ts | 28 +- .../endpoint/lib/artifacts/manifest_entry.ts | 3 +- .../server/endpoint/lib/artifacts/mocks.ts | 68 +++++ .../server/endpoint/lib/artifacts/task.ts | 84 +++-- .../server/endpoint/mocks.ts | 25 +- .../artifacts/download_exception_list.ts | 6 +- .../endpoint/schemas/artifacts/lists.mock.ts | 4 +- .../schemas/artifacts/saved_objects.mock.ts | 54 ++-- .../schemas/artifacts/saved_objects.ts | 41 ++- .../artifacts/artifact_client.test.ts | 9 +- .../services/artifacts/artifact_client.ts | 28 +- .../artifacts/manifest_client.test.ts | 5 +- .../services/artifacts/manifest_client.ts | 9 +- .../manifest_manager/manifest_manager.mock.ts | 81 ++--- .../manifest_manager/manifest_manager.test.ts | 209 +++++++++---- .../manifest_manager/manifest_manager.ts | 286 ++++++++---------- 26 files changed, 1154 insertions(+), 583 deletions(-) create mode 100644 x-pack/plugins/security_solution/server/endpoint/lib/artifacts/mocks.ts diff --git a/x-pack/plugins/ingest_manager/common/mocks.ts b/x-pack/plugins/ingest_manager/common/mocks.ts index e85364f2bb672..236324b11c580 100644 --- a/x-pack/plugins/ingest_manager/common/mocks.ts +++ b/x-pack/plugins/ingest_manager/common/mocks.ts @@ -44,3 +44,90 @@ export const createPackageConfigMock = (): PackageConfig => { ], }; }; + +export const createPackageConfigWithInitialManifestMock = (): PackageConfig => { + const packageConfig = createPackageConfigMock(); + packageConfig.inputs[0].config!.artifact_manifest = { + value: { + artifacts: { + 'endpoint-exceptionlist-linux-v1': { + compression_algorithm: 'zlib', + encryption_algorithm: 'none', + decoded_sha256: 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + encoded_sha256: 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', + decoded_size: 14, + encoded_size: 22, + relative_url: + '/api/endpoint/artifacts/download/endpoint-exceptionlist-linux-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + }, + 'endpoint-exceptionlist-macos-v1': { + compression_algorithm: 'zlib', + encryption_algorithm: 'none', + decoded_sha256: 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + encoded_sha256: 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', + decoded_size: 14, + encoded_size: 22, + relative_url: + '/api/endpoint/artifacts/download/endpoint-exceptionlist-macos-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + }, + 'endpoint-exceptionlist-windows-v1': { + compression_algorithm: 'zlib', + encryption_algorithm: 'none', + decoded_sha256: 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + encoded_sha256: 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', + decoded_size: 14, + encoded_size: 22, + relative_url: + '/api/endpoint/artifacts/download/endpoint-exceptionlist-windows-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + }, + }, + manifest_version: 'a9b7ef358a363f327f479e31efc4f228b2277a7fb4d1914ca9b4e7ca9ffcf537', + schema_version: 'v1', + }, + }; + return packageConfig; +}; + +export const createPackageConfigWithManifestMock = (): PackageConfig => { + const packageConfig = createPackageConfigMock(); + packageConfig.inputs[0].config!.artifact_manifest = { + value: { + artifacts: { + 'endpoint-exceptionlist-linux-v1': { + compression_algorithm: 'zlib', + encryption_algorithm: 'none', + decoded_sha256: '0a5a2013a79f9e60682472284a1be45ab1ff68b9b43426d00d665016612c15c8', + encoded_sha256: '57941169bb2c5416f9bd7224776c8462cb9a2be0fe8b87e6213e77a1d29be824', + decoded_size: 292, + encoded_size: 131, + relative_url: + '/api/endpoint/artifacts/download/endpoint-exceptionlist-linux-v1/0a5a2013a79f9e60682472284a1be45ab1ff68b9b43426d00d665016612c15c8', + }, + 'endpoint-exceptionlist-macos-v1': { + compression_algorithm: 'zlib', + encryption_algorithm: 'none', + decoded_sha256: '96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + encoded_sha256: '975382ab55d019cbab0bbac207a54e2a7d489fad6e8f6de34fc6402e5ef37b1e', + decoded_size: 432, + encoded_size: 147, + relative_url: + '/api/endpoint/artifacts/download/endpoint-exceptionlist-macos-v1/96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + }, + 'endpoint-exceptionlist-windows-v1': { + compression_algorithm: 'zlib', + encryption_algorithm: 'none', + decoded_sha256: '96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + encoded_sha256: '975382ab55d019cbab0bbac207a54e2a7d489fad6e8f6de34fc6402e5ef37b1e', + decoded_size: 432, + encoded_size: 147, + relative_url: + '/api/endpoint/artifacts/download/endpoint-exceptionlist-windows-v1/96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + }, + }, + manifest_version: '520f6cf88b3f36a065c6ca81058d5f8690aadadf6fe857f8dec4cc37589e7283', + schema_version: 'v1', + }, + }; + + return packageConfig; +}; diff --git a/x-pack/plugins/security_solution/common/endpoint/schema/common.ts b/x-pack/plugins/security_solution/common/endpoint/schema/common.ts index 014673ebe6398..8f2ea1f8a6452 100644 --- a/x-pack/plugins/security_solution/common/endpoint/schema/common.ts +++ b/x-pack/plugins/security_solution/common/endpoint/schema/common.ts @@ -12,6 +12,11 @@ export const compressionAlgorithm = t.keyof({ }); export type CompressionAlgorithm = t.TypeOf; +export const compressionAlgorithmDispatch = t.keyof({ + zlib: null, +}); +export type CompressionAlgorithmDispatch = t.TypeOf; + export const encryptionAlgorithm = t.keyof({ none: null, }); diff --git a/x-pack/plugins/security_solution/common/endpoint/schema/manifest.ts b/x-pack/plugins/security_solution/common/endpoint/schema/manifest.ts index 1c8916dfdd5bb..f8bb8b70f2d5b 100644 --- a/x-pack/plugins/security_solution/common/endpoint/schema/manifest.ts +++ b/x-pack/plugins/security_solution/common/endpoint/schema/manifest.ts @@ -7,6 +7,7 @@ import * as t from 'io-ts'; import { compressionAlgorithm, + compressionAlgorithmDispatch, encryptionAlgorithm, identifier, manifestSchemaVersion, @@ -16,25 +17,60 @@ import { size, } from './common'; -export const manifestEntrySchema = t.exact( +export const manifestEntryBaseSchema = t.exact( t.type({ relative_url: relativeUrl, decoded_sha256: sha256, decoded_size: size, encoded_sha256: sha256, encoded_size: size, - compression_algorithm: compressionAlgorithm, encryption_algorithm: encryptionAlgorithm, }) ); -export const manifestSchema = t.exact( +export const manifestEntrySchema = t.intersection([ + manifestEntryBaseSchema, + t.exact( + t.type({ + compression_algorithm: compressionAlgorithm, + }) + ), +]); +export type ManifestEntrySchema = t.TypeOf; + +export const manifestEntryDispatchSchema = t.intersection([ + manifestEntryBaseSchema, + t.exact( + t.type({ + compression_algorithm: compressionAlgorithmDispatch, + }) + ), +]); +export type ManifestEntryDispatchSchema = t.TypeOf; + +export const manifestBaseSchema = t.exact( t.type({ manifest_version: manifestVersion, schema_version: manifestSchemaVersion, - artifacts: t.record(identifier, manifestEntrySchema), }) ); -export type ManifestEntrySchema = t.TypeOf; +export const manifestSchema = t.intersection([ + manifestBaseSchema, + t.exact( + t.type({ + artifacts: t.record(identifier, manifestEntrySchema), + }) + ), +]); export type ManifestSchema = t.TypeOf; + +export const manifestDispatchSchema = t.intersection([ + manifestBaseSchema, + t.exact( + t.type({ + artifacts: t.record(identifier, manifestEntryDispatchSchema), + }) + ), +]); +export type ManifestDispatchSchema = t.TypeOf; diff --git a/x-pack/plugins/security_solution/server/endpoint/ingest_integration.test.ts b/x-pack/plugins/security_solution/server/endpoint/ingest_integration.test.ts index bb035a19f33d6..be749b2ebd25a 100644 --- a/x-pack/plugins/security_solution/server/endpoint/ingest_integration.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/ingest_integration.test.ts @@ -4,87 +4,122 @@ * you may not use this file except in compliance with the Elastic License. */ -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { loggerMock } from 'src/core/server/logging/logger.mock'; +import { loggingSystemMock } from 'src/core/server/mocks'; import { createNewPackageConfigMock } from '../../../ingest_manager/common/mocks'; import { factory as policyConfigFactory } from '../../common/endpoint/models/policy_config'; -import { getManifestManagerMock } from './services/artifacts/manifest_manager/manifest_manager.mock'; +import { + getManifestManagerMock, + ManifestManagerMockType, +} from './services/artifacts/manifest_manager/manifest_manager.mock'; import { getPackageConfigCreateCallback } from './ingest_integration'; +import { ManifestConstants } from './lib/artifacts'; describe('ingest_integration tests ', () => { describe('ingest_integration sanity checks', () => { - test('policy is updated with manifest', async () => { - const logger = loggerMock.create(); - const manifestManager = getManifestManagerMock(); + test('policy is updated with initial manifest', async () => { + const logger = loggingSystemMock.create().get('ingest_integration.test'); + const manifestManager = getManifestManagerMock({ + mockType: ManifestManagerMockType.InitialSystemState, + }); + const callback = getPackageConfigCreateCallback(logger, manifestManager); - const policyConfig = createNewPackageConfigMock(); - const newPolicyConfig = await callback(policyConfig); + const policyConfig = createNewPackageConfigMock(); // policy config without manifest + const newPolicyConfig = await callback(policyConfig); // policy config WITH manifest + expect(newPolicyConfig.inputs[0]!.type).toEqual('endpoint'); expect(newPolicyConfig.inputs[0]!.config!.policy.value).toEqual(policyConfigFactory()); expect(newPolicyConfig.inputs[0]!.config!.artifact_manifest.value).toEqual({ artifacts: { 'endpoint-exceptionlist-linux-v1': { compression_algorithm: 'zlib', - decoded_sha256: '1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', - decoded_size: 287, - encoded_sha256: 'c3dec543df1177561ab2aa74a37997ea3c1d748d532a597884f5a5c16670d56c', - encoded_size: 133, + decoded_sha256: 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + decoded_size: 14, + encoded_sha256: 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', + encoded_size: 22, + encryption_algorithm: 'none', + relative_url: + '/api/endpoint/artifacts/download/endpoint-exceptionlist-linux-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + }, + 'endpoint-exceptionlist-macos-v1': { + compression_algorithm: 'zlib', + decoded_sha256: 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + decoded_size: 14, + encoded_sha256: 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', + encoded_size: 22, + encryption_algorithm: 'none', + relative_url: + '/api/endpoint/artifacts/download/endpoint-exceptionlist-macos-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + }, + 'endpoint-exceptionlist-windows-v1': { + compression_algorithm: 'zlib', + decoded_sha256: 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + decoded_size: 14, + encoded_sha256: 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', + encoded_size: 22, encryption_algorithm: 'none', relative_url: - '/api/endpoint/artifacts/download/endpoint-exceptionlist-linux-v1/1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', + '/api/endpoint/artifacts/download/endpoint-exceptionlist-windows-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', }, }, - manifest_version: 'WzAsMF0=', + manifest_version: 'a9b7ef358a363f327f479e31efc4f228b2277a7fb4d1914ca9b4e7ca9ffcf537', schema_version: 'v1', }); }); - test('policy is returned even if error is encountered during artifact sync', async () => { - const logger = loggerMock.create(); + test('policy is returned even if error is encountered during artifact creation', async () => { + const logger = loggingSystemMock.create().get('ingest_integration.test'); const manifestManager = getManifestManagerMock(); - manifestManager.syncArtifacts = jest.fn().mockRejectedValue([new Error('error updating')]); - const lastDispatched = await manifestManager.getLastDispatchedManifest(); + manifestManager.pushArtifacts = jest.fn().mockResolvedValue([new Error('error updating')]); + const lastComputed = await manifestManager.getLastComputedManifest( + ManifestConstants.SCHEMA_VERSION + ); + const callback = getPackageConfigCreateCallback(logger, manifestManager); const policyConfig = createNewPackageConfigMock(); const newPolicyConfig = await callback(policyConfig); + expect(newPolicyConfig.inputs[0]!.type).toEqual('endpoint'); expect(newPolicyConfig.inputs[0]!.config!.policy.value).toEqual(policyConfigFactory()); expect(newPolicyConfig.inputs[0]!.config!.artifact_manifest.value).toEqual( - lastDispatched.toEndpointFormat() + lastComputed!.toEndpointFormat() ); }); - test('initial policy creation succeeds if snapshot retrieval fails', async () => { - const logger = loggerMock.create(); - const manifestManager = getManifestManagerMock(); - const lastDispatched = await manifestManager.getLastDispatchedManifest(); - manifestManager.getSnapshot = jest.fn().mockResolvedValue(null); + test('initial policy creation succeeds if manifest retrieval fails', async () => { + const logger = loggingSystemMock.create().get('ingest_integration.test'); + const manifestManager = getManifestManagerMock({ + mockType: ManifestManagerMockType.InitialSystemState, + }); + const lastComputed = await manifestManager.getLastComputedManifest( + ManifestConstants.SCHEMA_VERSION + ); + expect(lastComputed).toEqual(null); + + manifestManager.buildNewManifest = jest.fn().mockRejectedValue(new Error('abcd')); const callback = getPackageConfigCreateCallback(logger, manifestManager); const policyConfig = createNewPackageConfigMock(); const newPolicyConfig = await callback(policyConfig); + expect(newPolicyConfig.inputs[0]!.type).toEqual('endpoint'); expect(newPolicyConfig.inputs[0]!.config!.policy.value).toEqual(policyConfigFactory()); - expect(newPolicyConfig.inputs[0]!.config!.artifact_manifest.value).toEqual( - lastDispatched.toEndpointFormat() - ); }); test('subsequent policy creations succeed', async () => { - const logger = loggerMock.create(); + const logger = loggingSystemMock.create().get('ingest_integration.test'); const manifestManager = getManifestManagerMock(); - const snapshot = await manifestManager.getSnapshot(); - manifestManager.getLastDispatchedManifest = jest.fn().mockResolvedValue(snapshot!.manifest); - manifestManager.getSnapshot = jest.fn().mockResolvedValue({ - manifest: snapshot!.manifest, - diffs: [], - }); + const lastComputed = await manifestManager.getLastComputedManifest( + ManifestConstants.SCHEMA_VERSION + ); + + manifestManager.buildNewManifest = jest.fn().mockResolvedValue(lastComputed); // no diffs const callback = getPackageConfigCreateCallback(logger, manifestManager); const policyConfig = createNewPackageConfigMock(); const newPolicyConfig = await callback(policyConfig); + expect(newPolicyConfig.inputs[0]!.type).toEqual('endpoint'); expect(newPolicyConfig.inputs[0]!.config!.policy.value).toEqual(policyConfigFactory()); expect(newPolicyConfig.inputs[0]!.config!.artifact_manifest.value).toEqual( - snapshot!.manifest.toEndpointFormat() + lastComputed!.toEndpointFormat() ); }); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/ingest_integration.ts b/x-pack/plugins/security_solution/server/endpoint/ingest_integration.ts index e2522ac4af778..11d4b12d0b76a 100644 --- a/x-pack/plugins/security_solution/server/endpoint/ingest_integration.ts +++ b/x-pack/plugins/security_solution/server/endpoint/ingest_integration.ts @@ -8,9 +8,63 @@ import { Logger } from '../../../../../src/core/server'; import { NewPackageConfig } from '../../../ingest_manager/common/types/models'; import { factory as policyConfigFactory } from '../../common/endpoint/models/policy_config'; import { NewPolicyData } from '../../common/endpoint/types'; -import { ManifestManager, ManifestSnapshot } from './services/artifacts'; +import { ManifestManager } from './services/artifacts'; +import { Manifest } from './lib/artifacts'; import { reportErrors, ManifestConstants } from './lib/artifacts/common'; -import { ManifestSchemaVersion } from '../../common/endpoint/schema/common'; +import { InternalArtifactCompleteSchema } from './schemas/artifacts'; +import { manifestDispatchSchema } from '../../common/endpoint/schema/manifest'; + +const getManifest = async (logger: Logger, manifestManager: ManifestManager): Promise => { + let manifest: Manifest | null = null; + + try { + manifest = await manifestManager.getLastComputedManifest(ManifestConstants.SCHEMA_VERSION); + + // If we have not yet computed a manifest, then we have to do so now. This should only happen + // once. + if (manifest == null) { + // New computed manifest based on current state of exception list + const newManifest = await manifestManager.buildNewManifest(ManifestConstants.SCHEMA_VERSION); + const diffs = newManifest.diff(Manifest.getDefault(ManifestConstants.SCHEMA_VERSION)); + + // Compress new artifacts + const adds = diffs.filter((diff) => diff.type === 'add').map((diff) => diff.id); + for (const artifactId of adds) { + const compressError = await newManifest.compressArtifact(artifactId); + if (compressError) { + throw compressError; + } + } + + // Persist new artifacts + const artifacts = adds + .map((artifactId) => newManifest.getArtifact(artifactId)) + .filter((artifact): artifact is InternalArtifactCompleteSchema => artifact !== undefined); + if (artifacts.length !== adds.length) { + throw new Error('Invalid artifact encountered.'); + } + const persistErrors = await manifestManager.pushArtifacts(artifacts); + if (persistErrors.length) { + reportErrors(logger, persistErrors); + throw new Error('Unable to persist new artifacts.'); + } + + // Commit the manifest state + if (diffs.length) { + const error = await manifestManager.commit(newManifest); + if (error) { + throw error; + } + } + + manifest = newManifest; + } + } catch (err) { + logger.error(err); + } + + return manifest ?? Manifest.getDefault(ManifestConstants.SCHEMA_VERSION); +}; /** * Callback to handle creation of PackageConfigs in Ingest Manager @@ -31,85 +85,37 @@ export const getPackageConfigCreateCallback = ( // follow the types/schema expected let updatedPackageConfig = newPackageConfig as NewPolicyData; - // get current manifest from SO (last dispatched) - const manifest = ( - await manifestManager.getLastDispatchedManifest(ManifestConstants.SCHEMA_VERSION) - )?.toEndpointFormat() ?? { - manifest_version: 'default', - schema_version: ManifestConstants.SCHEMA_VERSION as ManifestSchemaVersion, - artifacts: {}, - }; + // Get most recent manifest + const manifest = await getManifest(logger, manifestManager); + const serializedManifest = manifest.toEndpointFormat(); + if (!manifestDispatchSchema.is(serializedManifest)) { + // This should not happen. + // But if it does, we log it and return it anyway. + logger.error('Invalid manifest'); + } // Until we get the Default Policy Configuration in the Endpoint package, // we will add it here manually at creation time. - if (newPackageConfig.inputs.length === 0) { - updatedPackageConfig = { - ...newPackageConfig, - inputs: [ - { - type: 'endpoint', - enabled: true, - streams: [], - config: { - artifact_manifest: { - value: manifest, - }, - policy: { - value: policyConfigFactory(), - }, + updatedPackageConfig = { + ...newPackageConfig, + inputs: [ + { + type: 'endpoint', + enabled: true, + streams: [], + config: { + artifact_manifest: { + value: serializedManifest, + }, + policy: { + value: policyConfigFactory(), }, }, - ], - }; - } - - let snapshot: ManifestSnapshot | null = null; - let success = true; - try { - // Try to get most up-to-date manifest data. - - // get snapshot based on exception-list-agnostic SOs - // with diffs from last dispatched manifest, if it exists - snapshot = await manifestManager.getSnapshot({ initialize: true }); - - if (snapshot && snapshot.diffs.length) { - // create new artifacts - const errors = await manifestManager.syncArtifacts(snapshot, 'add'); - if (errors.length) { - reportErrors(logger, errors); - throw new Error('Error writing new artifacts.'); - } - } - - if (snapshot) { - updatedPackageConfig.inputs[0].config.artifact_manifest = { - value: snapshot.manifest.toEndpointFormat(), - }; - } - - return updatedPackageConfig; - } catch (err) { - success = false; - logger.error(err); - return updatedPackageConfig; - } finally { - if (success && snapshot !== null) { - try { - if (snapshot.diffs.length > 0) { - // TODO: let's revisit the way this callback happens... use promises? - // only commit when we know the package config was created - await manifestManager.commit(snapshot.manifest); + }, + ], + }; - // clean up old artifacts - await manifestManager.syncArtifacts(snapshot, 'delete'); - } - } catch (err) { - logger.error(err); - } - } else if (snapshot === null) { - logger.error('No manifest snapshot available.'); - } - } + return updatedPackageConfig; }; return handlePackageConfigCreate; diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/common.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/common.ts index 77a5e85b14199..7298a9bfa72a6 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/common.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/common.ts @@ -4,6 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ import { Logger } from 'src/core/server'; +import { + InternalArtifactSchema, + InternalArtifactCompleteSchema, + internalArtifactCompleteSchema, +} from '../../schemas/artifacts'; export const ArtifactConstants = { GLOBAL_ALLOWLIST_NAME: 'endpoint-exceptionlist', @@ -15,7 +20,16 @@ export const ArtifactConstants = { export const ManifestConstants = { SAVED_OBJECT_TYPE: 'endpoint:user-artifact-manifest', SCHEMA_VERSION: 'v1', - INITIAL_VERSION: 'WzAsMF0=', +}; + +export const getArtifactId = (artifact: InternalArtifactSchema) => { + return `${artifact.identifier}-${artifact.decodedSha256}`; +}; + +export const isCompleteArtifact = ( + artifact: InternalArtifactSchema +): artifact is InternalArtifactCompleteSchema => { + return internalArtifactCompleteSchema.is(artifact); }; export const reportErrors = (logger: Logger, errors: Error[]) => { diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts index 1a19306b2fd60..d3d073efa73c1 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts @@ -9,7 +9,8 @@ import { listMock } from '../../../../../lists/server/mocks'; import { getFoundExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/found_exception_list_item_schema.mock'; import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; import { EntriesArray, EntryList } from '../../../../../lists/common/schemas/types/entries'; -import { getFullEndpointExceptionList } from './lists'; +import { buildArtifact, getFullEndpointExceptionList } from './lists'; +import { TranslatedEntry, TranslatedExceptionListItem } from '../../schemas/artifacts'; describe('buildEventTypeSignal', () => { let mockExceptionClient: ExceptionListClient; @@ -340,4 +341,95 @@ describe('buildEventTypeSignal', () => { const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', 'v1'); expect(resp.entries.length).toEqual(0); }); + + test('it should return a stable hash regardless of order of entries', async () => { + const translatedEntries: TranslatedEntry[] = [ + { + entries: [ + { + field: 'some.nested.field', + operator: 'included', + type: 'exact_cased', + value: 'some value', + }, + ], + field: 'some.parentField', + type: 'nested', + }, + { + field: 'nested.field', + operator: 'included', + type: 'exact_cased', + value: 'some value', + }, + ]; + const translatedEntriesReversed = translatedEntries.reverse(); + + const translatedExceptionList = { + entries: [ + { + type: 'simple', + entries: translatedEntries, + }, + ], + }; + + const translatedExceptionListReversed = { + entries: [ + { + type: 'simple', + entries: translatedEntriesReversed, + }, + ], + }; + + const artifact1 = await buildArtifact(translatedExceptionList, 'linux', 'v1'); + const artifact2 = await buildArtifact(translatedExceptionListReversed, 'linux', 'v1'); + expect(artifact1.decodedSha256).toEqual(artifact2.decodedSha256); + }); + + test('it should return a stable hash regardless of order of items', async () => { + const translatedItems: TranslatedExceptionListItem[] = [ + { + type: 'simple', + entries: [ + { + entries: [ + { + field: 'some.nested.field', + operator: 'included', + type: 'exact_cased', + value: 'some value', + }, + ], + field: 'some.parentField', + type: 'nested', + }, + ], + }, + { + type: 'simple', + entries: [ + { + field: 'nested.field', + operator: 'included', + type: 'exact_cased', + value: 'some value', + }, + ], + }, + ]; + + const translatedExceptionList = { + entries: translatedItems, + }; + + const translatedExceptionListReversed = { + entries: translatedItems.reverse(), + }; + + const artifact1 = await buildArtifact(translatedExceptionList, 'linux', 'v1'); + const artifact2 = await buildArtifact(translatedExceptionListReversed, 'linux', 'v1'); + expect(artifact1.decodedSha256).toEqual(artifact2.decodedSha256); + }); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts index e6fd4bad97c5f..68fa2a0511a48 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts @@ -25,6 +25,8 @@ import { translatedEntryMatchMatcher, translatedEntryMatchAnyMatcher, TranslatedExceptionListItem, + internalArtifactCompleteSchema, + InternalArtifactCompleteSchema, } from '../../schemas'; import { ArtifactConstants } from './common'; @@ -32,7 +34,7 @@ export async function buildArtifact( exceptions: WrappedTranslatedExceptionList, os: string, schemaVersion: string -): Promise { +): Promise { const exceptionsBuffer = Buffer.from(JSON.stringify(exceptions)); const sha256 = createHash('sha256').update(exceptionsBuffer.toString()).digest('hex'); @@ -45,11 +47,32 @@ export async function buildArtifact( encodedSha256: sha256, decodedSize: exceptionsBuffer.byteLength, encodedSize: exceptionsBuffer.byteLength, - created: Date.now(), body: exceptionsBuffer.toString('base64'), }; } +export async function maybeCompressArtifact( + uncompressedArtifact: InternalArtifactSchema +): Promise { + const compressedArtifact = { ...uncompressedArtifact }; + if (internalArtifactCompleteSchema.is(uncompressedArtifact)) { + const compressedExceptionList = await compressExceptionList( + Buffer.from(uncompressedArtifact.body, 'base64') + ); + compressedArtifact.body = compressedExceptionList.toString('base64'); + compressedArtifact.encodedSize = compressedExceptionList.byteLength; + compressedArtifact.compressionAlgorithm = 'zlib'; + compressedArtifact.encodedSha256 = createHash('sha256') + .update(compressedExceptionList) + .digest('hex'); + } + return compressedArtifact; +} + +export function isCompressed(artifact: InternalArtifactSchema) { + return artifact.compressionAlgorithm === 'zlib'; +} + export async function getFullEndpointExceptionList( eClient: ExceptionListClient, os: string, @@ -136,7 +159,7 @@ function translateItem( const itemSet = new Set(); return { type: item.type, - entries: item.entries.reduce((translatedEntries: TranslatedEntry[], entry) => { + entries: item.entries.reduce((translatedEntries, entry) => { const translatedEntry = translateEntry(schemaVersion, entry); if (translatedEntry !== undefined && translatedEntryType.is(translatedEntry)) { const itemHash = createHash('sha256').update(JSON.stringify(translatedEntry)).digest('hex'); @@ -156,8 +179,8 @@ function translateEntry( ): TranslatedEntry | undefined { switch (entry.type) { case 'nested': { - const nestedEntries = entry.entries.reduce( - (entries: TranslatedEntryNestedEntry[], nestedEntry) => { + const nestedEntries = entry.entries.reduce( + (entries, nestedEntry) => { const translatedEntry = translateEntry(schemaVersion, nestedEntry); if (nestedEntry !== undefined && translatedEntryNestedEntry.is(translatedEntry)) { entries.push(translatedEntry); diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.test.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.test.ts index e1f6bac2620ea..95587c6fc105d 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.test.ts @@ -5,103 +5,125 @@ */ import { ManifestSchemaVersion } from '../../../../common/endpoint/schema/common'; -import { InternalArtifactSchema } from '../../schemas'; -import { - getInternalArtifactMock, - getInternalArtifactMockWithDiffs, -} from '../../schemas/artifacts/saved_objects.mock'; -import { ManifestConstants } from './common'; +import { InternalArtifactCompleteSchema } from '../../schemas'; +import { ManifestConstants, getArtifactId } from './common'; import { Manifest } from './manifest'; +import { + getMockArtifacts, + getMockManifest, + getMockManifestWithDiffs, + getEmptyMockManifest, +} from './mocks'; describe('manifest', () => { describe('Manifest object sanity checks', () => { - const artifacts: InternalArtifactSchema[] = []; - const now = new Date(); + let artifacts: InternalArtifactCompleteSchema[] = []; let manifest1: Manifest; let manifest2: Manifest; + let emptyManifest: Manifest; beforeAll(async () => { - const artifactLinux = await getInternalArtifactMock('linux', 'v1'); - const artifactMacos = await getInternalArtifactMock('macos', 'v1'); - const artifactWindows = await getInternalArtifactMock('windows', 'v1'); - artifacts.push(artifactLinux); - artifacts.push(artifactMacos); - artifacts.push(artifactWindows); - - manifest1 = new Manifest(now, 'v1', ManifestConstants.INITIAL_VERSION); - manifest1.addEntry(artifactLinux); - manifest1.addEntry(artifactMacos); - manifest1.addEntry(artifactWindows); - manifest1.setVersion('abcd'); - - const newArtifactLinux = await getInternalArtifactMockWithDiffs('linux', 'v1'); - manifest2 = new Manifest(new Date(), 'v1', ManifestConstants.INITIAL_VERSION); - manifest2.addEntry(newArtifactLinux); - manifest2.addEntry(artifactMacos); - manifest2.addEntry(artifactWindows); + artifacts = await getMockArtifacts({ compress: true }); + manifest1 = await getMockManifest({ compress: true }); + manifest2 = await getMockManifestWithDiffs({ compress: true }); + emptyManifest = await getEmptyMockManifest({ compress: true }); }); test('Can create manifest with valid schema version', () => { - const manifest = new Manifest(new Date(), 'v1', ManifestConstants.INITIAL_VERSION); + const manifest = new Manifest('v1'); expect(manifest).toBeInstanceOf(Manifest); }); test('Cannot create manifest with invalid schema version', () => { expect(() => { - new Manifest( - new Date(), - 'abcd' as ManifestSchemaVersion, - ManifestConstants.INITIAL_VERSION - ); + new Manifest('abcd' as ManifestSchemaVersion); }).toThrow(); }); + test('Empty manifest transforms correctly to expected endpoint format', async () => { + expect(emptyManifest.toEndpointFormat()).toStrictEqual({ + artifacts: { + 'endpoint-exceptionlist-linux-v1': { + compression_algorithm: 'zlib', + encryption_algorithm: 'none', + decoded_sha256: 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + encoded_sha256: 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', + decoded_size: 14, + encoded_size: 22, + relative_url: + '/api/endpoint/artifacts/download/endpoint-exceptionlist-linux-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + }, + 'endpoint-exceptionlist-macos-v1': { + compression_algorithm: 'zlib', + encryption_algorithm: 'none', + decoded_sha256: 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + encoded_sha256: 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', + decoded_size: 14, + encoded_size: 22, + relative_url: + '/api/endpoint/artifacts/download/endpoint-exceptionlist-macos-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + }, + 'endpoint-exceptionlist-windows-v1': { + compression_algorithm: 'zlib', + encryption_algorithm: 'none', + decoded_sha256: 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + encoded_sha256: 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', + decoded_size: 14, + encoded_size: 22, + relative_url: + '/api/endpoint/artifacts/download/endpoint-exceptionlist-windows-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + }, + }, + manifest_version: 'a9b7ef358a363f327f479e31efc4f228b2277a7fb4d1914ca9b4e7ca9ffcf537', + schema_version: 'v1', + }); + }); + test('Manifest transforms correctly to expected endpoint format', async () => { expect(manifest1.toEndpointFormat()).toStrictEqual({ artifacts: { 'endpoint-exceptionlist-linux-v1': { - compression_algorithm: 'none', + compression_algorithm: 'zlib', encryption_algorithm: 'none', - decoded_sha256: '5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735', - encoded_sha256: '5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735', - decoded_size: 430, - encoded_size: 430, + decoded_sha256: '96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + encoded_sha256: '975382ab55d019cbab0bbac207a54e2a7d489fad6e8f6de34fc6402e5ef37b1e', + decoded_size: 432, + encoded_size: 147, relative_url: - '/api/endpoint/artifacts/download/endpoint-exceptionlist-linux-v1/5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735', + '/api/endpoint/artifacts/download/endpoint-exceptionlist-linux-v1/96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', }, 'endpoint-exceptionlist-macos-v1': { - compression_algorithm: 'none', + compression_algorithm: 'zlib', encryption_algorithm: 'none', - decoded_sha256: '5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735', - encoded_sha256: '5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735', - decoded_size: 430, - encoded_size: 430, + decoded_sha256: '96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + encoded_sha256: '975382ab55d019cbab0bbac207a54e2a7d489fad6e8f6de34fc6402e5ef37b1e', + decoded_size: 432, + encoded_size: 147, relative_url: - '/api/endpoint/artifacts/download/endpoint-exceptionlist-macos-v1/5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735', + '/api/endpoint/artifacts/download/endpoint-exceptionlist-macos-v1/96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', }, 'endpoint-exceptionlist-windows-v1': { - compression_algorithm: 'none', + compression_algorithm: 'zlib', encryption_algorithm: 'none', - decoded_sha256: '5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735', - encoded_sha256: '5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735', - decoded_size: 430, - encoded_size: 430, + decoded_sha256: '96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + encoded_sha256: '975382ab55d019cbab0bbac207a54e2a7d489fad6e8f6de34fc6402e5ef37b1e', + decoded_size: 432, + encoded_size: 147, relative_url: - '/api/endpoint/artifacts/download/endpoint-exceptionlist-windows-v1/5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735', + '/api/endpoint/artifacts/download/endpoint-exceptionlist-windows-v1/96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', }, }, - manifest_version: 'abcd', + manifest_version: 'a7f4760bfa2662e85e30fe4fb8c01b4c4a20938c76ab21d3c5a3e781e547cce7', schema_version: 'v1', }); }); test('Manifest transforms correctly to expected saved object format', async () => { expect(manifest1.toSavedObject()).toStrictEqual({ - created: now.getTime(), ids: [ - 'endpoint-exceptionlist-linux-v1-5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735', - 'endpoint-exceptionlist-macos-v1-5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735', - 'endpoint-exceptionlist-windows-v1-5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735', + 'endpoint-exceptionlist-linux-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + 'endpoint-exceptionlist-macos-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + 'endpoint-exceptionlist-windows-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', ], }); }); @@ -111,12 +133,12 @@ describe('manifest', () => { expect(diffs).toEqual([ { id: - 'endpoint-exceptionlist-linux-v1-5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735', + 'endpoint-exceptionlist-linux-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', type: 'delete', }, { id: - 'endpoint-exceptionlist-linux-v1-3d3546e94f70493021ee845be32c66e36ea7a720c64b4d608d8029fe949f7e51', + 'endpoint-exceptionlist-linux-v1-0a5a2013a79f9e60682472284a1be45ab1ff68b9b43426d00d665016612c15c8', type: 'add', }, ]); @@ -124,7 +146,7 @@ describe('manifest', () => { test('Manifest returns data for given artifact', async () => { const artifact = artifacts[0]; - const returned = manifest1.getArtifact(`${artifact.identifier}-${artifact.decodedSha256}`); + const returned = manifest1.getArtifact(getArtifactId(artifact)); expect(returned).toEqual(artifact); }); @@ -132,39 +154,35 @@ describe('manifest', () => { const entries = manifest1.getEntries(); const keys = Object.keys(entries); expect(keys).toEqual([ - 'endpoint-exceptionlist-linux-v1-5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735', - 'endpoint-exceptionlist-macos-v1-5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735', - 'endpoint-exceptionlist-windows-v1-5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735', + 'endpoint-exceptionlist-linux-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + 'endpoint-exceptionlist-macos-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + 'endpoint-exceptionlist-windows-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', ]); }); test('Manifest returns true if contains artifact', async () => { const found = manifest1.contains( - 'endpoint-exceptionlist-macos-v1-5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735' + 'endpoint-exceptionlist-macos-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3' ); expect(found).toEqual(true); }); test('Manifest can be created from list of artifacts', async () => { - const oldManifest = new Manifest( - new Date(), - ManifestConstants.SCHEMA_VERSION, - ManifestConstants.INITIAL_VERSION - ); + const oldManifest = new Manifest(ManifestConstants.SCHEMA_VERSION); const manifest = Manifest.fromArtifacts(artifacts, 'v1', oldManifest); expect( manifest.contains( - 'endpoint-exceptionlist-linux-v1-5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735' + 'endpoint-exceptionlist-linux-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3' ) ).toEqual(true); expect( manifest.contains( - 'endpoint-exceptionlist-macos-v1-5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735' + 'endpoint-exceptionlist-macos-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3' ) ).toEqual(true); expect( manifest.contains( - 'endpoint-exceptionlist-windows-v1-5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735' + 'endpoint-exceptionlist-windows-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3' ) ).toEqual(true); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.ts index 576ecb08d6923..6ece2bf0f48e8 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.ts @@ -4,15 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ +import { createHash } from 'crypto'; import { validate } from '../../../../common/validate'; -import { InternalArtifactSchema, InternalManifestSchema } from '../../schemas/artifacts'; +import { + InternalArtifactSchema, + InternalManifestSchema, + internalArtifactCompleteSchema, + InternalArtifactCompleteSchema, +} from '../../schemas/artifacts'; import { manifestSchemaVersion, ManifestSchemaVersion, } from '../../../../common/endpoint/schema/common'; import { ManifestSchema, manifestSchema } from '../../../../common/endpoint/schema/manifest'; -import { ManifestConstants } from './common'; import { ManifestEntry } from './manifest_entry'; +import { maybeCompressArtifact, isCompressed } from './lists'; +import { getArtifactId } from './common'; export interface ManifestDiff { type: string; @@ -20,15 +27,13 @@ export interface ManifestDiff { } export class Manifest { - private created: Date; private entries: Record; private schemaVersion: ManifestSchemaVersion; // For concurrency control - private version: string; + private version: string | undefined; - constructor(created: Date, schemaVersion: string, version: string) { - this.created = created; + constructor(schemaVersion: string, version?: string) { this.entries = {}; this.version = version; @@ -38,20 +43,24 @@ export class Manifest { ); if (errors != null || validated === null) { - throw new Error(`Invalid manifest version: ${schemaVersion}`); + throw new Error(`Invalid manifest schema version: ${schemaVersion}`); } this.schemaVersion = validated; } + public static getDefault(schemaVersion: string) { + return new Manifest(schemaVersion); + } + public static fromArtifacts( - artifacts: InternalArtifactSchema[], + artifacts: InternalArtifactCompleteSchema[], schemaVersion: string, oldManifest: Manifest ): Manifest { - const manifest = new Manifest(new Date(), schemaVersion, oldManifest.getVersion()); + const manifest = new Manifest(schemaVersion, oldManifest.getSoVersion()); artifacts.forEach((artifact) => { - const id = `${artifact.identifier}-${artifact.decodedSha256}`; + const id = getArtifactId(artifact); const existingArtifact = oldManifest.getArtifact(id); if (existingArtifact) { manifest.addEntry(existingArtifact); @@ -62,15 +71,70 @@ export class Manifest { return manifest; } + public static fromPkgConfig(manifestPkgConfig: ManifestSchema): Manifest | null { + if (manifestSchema.is(manifestPkgConfig)) { + const manifest = new Manifest(manifestPkgConfig.schema_version); + for (const [identifier, artifactRecord] of Object.entries(manifestPkgConfig.artifacts)) { + const artifact = { + identifier, + compressionAlgorithm: artifactRecord.compression_algorithm, + encryptionAlgorithm: artifactRecord.encryption_algorithm, + decodedSha256: artifactRecord.decoded_sha256, + decodedSize: artifactRecord.decoded_size, + encodedSha256: artifactRecord.encoded_sha256, + encodedSize: artifactRecord.encoded_size, + }; + manifest.addEntry(artifact); + } + return manifest; + } else { + return null; + } + } + + public async compressArtifact(id: string): Promise { + try { + const artifact = this.getArtifact(id); + if (artifact == null) { + throw new Error(`Corrupted manifest detected. Artifact ${id} not in manifest.`); + } + + const compressedArtifact = await maybeCompressArtifact(artifact); + if (!isCompressed(compressedArtifact)) { + throw new Error(`Unable to compress artifact: ${id}`); + } else if (!internalArtifactCompleteSchema.is(compressedArtifact)) { + throw new Error(`Incomplete artifact detected: ${id}`); + } + this.addEntry(compressedArtifact); + } catch (err) { + return err; + } + return null; + } + + public equals(manifest: Manifest): boolean { + return this.getSha256() === manifest.getSha256(); + } + + public getSha256(): string { + let sha256 = createHash('sha256'); + Object.keys(this.entries) + .sort() + .forEach((docId) => { + sha256 = sha256.update(docId); + }); + return sha256.digest('hex'); + } + public getSchemaVersion(): ManifestSchemaVersion { return this.schemaVersion; } - public getVersion(): string { + public getSoVersion(): string | undefined { return this.version; } - public setVersion(version: string) { + public setSoVersion(version: string) { this.version = version; } @@ -87,8 +151,12 @@ export class Manifest { return this.entries; } + public getEntry(artifactId: string): ManifestEntry | undefined { + return this.entries[artifactId]; + } + public getArtifact(artifactId: string): InternalArtifactSchema | undefined { - return this.entries[artifactId]?.getArtifact(); + return this.getEntry(artifactId)?.getArtifact(); } public diff(manifest: Manifest): ManifestDiff[] { @@ -111,7 +179,7 @@ export class Manifest { public toEndpointFormat(): ManifestSchema { const manifestObj: ManifestSchema = { - manifest_version: this.version ?? ManifestConstants.INITIAL_VERSION, + manifest_version: this.getSha256(), schema_version: this.schemaVersion, artifacts: {}, }; @@ -130,7 +198,6 @@ export class Manifest { public toSavedObject(): InternalManifestSchema { return { - created: this.created.getTime(), ids: Object.keys(this.entries), }; } diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest_entry.test.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest_entry.test.ts index 7ea2a07210c55..d7bd57547de0a 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest_entry.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest_entry.test.ts @@ -14,7 +14,7 @@ describe('manifest_entry', () => { let manifestEntry: ManifestEntry; beforeAll(async () => { - artifact = await getInternalArtifactMock('windows', 'v1'); + artifact = await getInternalArtifactMock('windows', 'v1', { compress: true }); manifestEntry = new ManifestEntry(artifact); }); @@ -24,7 +24,7 @@ describe('manifest_entry', () => { test('Correct doc_id is returned', () => { expect(manifestEntry.getDocId()).toEqual( - 'endpoint-exceptionlist-windows-v1-5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735' + 'endpoint-exceptionlist-windows-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3' ); }); @@ -34,21 +34,21 @@ describe('manifest_entry', () => { test('Correct sha256 is returned', () => { expect(manifestEntry.getEncodedSha256()).toEqual( - '5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735' + '975382ab55d019cbab0bbac207a54e2a7d489fad6e8f6de34fc6402e5ef37b1e' ); expect(manifestEntry.getDecodedSha256()).toEqual( - '5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735' + '96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3' ); }); test('Correct size is returned', () => { - expect(manifestEntry.getEncodedSize()).toEqual(430); - expect(manifestEntry.getDecodedSize()).toEqual(430); + expect(manifestEntry.getEncodedSize()).toEqual(147); + expect(manifestEntry.getDecodedSize()).toEqual(432); }); test('Correct url is returned', () => { expect(manifestEntry.getUrl()).toEqual( - '/api/endpoint/artifacts/download/endpoint-exceptionlist-windows-v1/5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735' + '/api/endpoint/artifacts/download/endpoint-exceptionlist-windows-v1/96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3' ); }); @@ -58,17 +58,15 @@ describe('manifest_entry', () => { test('Correct record is returned', () => { expect(manifestEntry.getRecord()).toEqual({ - compression_algorithm: 'none', + compression_algorithm: 'zlib', encryption_algorithm: 'none', - decoded_sha256: '5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735', - encoded_sha256: '5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735', - decoded_size: 430, - encoded_size: 430, + decoded_sha256: '96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + encoded_sha256: '975382ab55d019cbab0bbac207a54e2a7d489fad6e8f6de34fc6402e5ef37b1e', + decoded_size: 432, + encoded_size: 147, relative_url: - '/api/endpoint/artifacts/download/endpoint-exceptionlist-windows-v1/5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735', + '/api/endpoint/artifacts/download/endpoint-exceptionlist-windows-v1/96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', }); }); - - // TODO: add test for entry with compression }); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest_entry.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest_entry.ts index b35e0c2b9ad6e..b6c103e24f024 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest_entry.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest_entry.ts @@ -7,6 +7,7 @@ import { InternalArtifactSchema } from '../../schemas/artifacts'; import { CompressionAlgorithm } from '../../../../common/endpoint/schema/common'; import { ManifestEntrySchema } from '../../../../common/endpoint/schema/manifest'; +import { getArtifactId } from './common'; export class ManifestEntry { private artifact: InternalArtifactSchema; @@ -16,7 +17,7 @@ export class ManifestEntry { } public getDocId(): string { - return `${this.getIdentifier()}-${this.getDecodedSha256()}`; + return getArtifactId(this.artifact); } public getIdentifier(): string { diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/mocks.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/mocks.ts new file mode 100644 index 0000000000000..097151ee835ba --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/mocks.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InternalArtifactCompleteSchema } from '../../schemas/artifacts'; +import { + getInternalArtifactMock, + getInternalArtifactMockWithDiffs, + getEmptyInternalArtifactMock, +} from '../../schemas/artifacts/saved_objects.mock'; +import { ArtifactConstants } from './common'; +import { Manifest } from './manifest'; + +export const getMockArtifacts = async (opts?: { compress: boolean }) => { + return Promise.all( + ArtifactConstants.SUPPORTED_OPERATING_SYSTEMS.map>( + async (os) => { + return getInternalArtifactMock(os, 'v1', opts); + } + ) + ); +}; + +export const getMockArtifactsWithDiff = async (opts?: { compress: boolean }) => { + return Promise.all( + ArtifactConstants.SUPPORTED_OPERATING_SYSTEMS.map>( + async (os) => { + if (os === 'linux') { + return getInternalArtifactMockWithDiffs(os, 'v1'); + } + return getInternalArtifactMock(os, 'v1', opts); + } + ) + ); +}; + +export const getEmptyMockArtifacts = async (opts?: { compress: boolean }) => { + return Promise.all( + ArtifactConstants.SUPPORTED_OPERATING_SYSTEMS.map>( + async (os) => { + return getEmptyInternalArtifactMock(os, 'v1', opts); + } + ) + ); +}; + +export const getMockManifest = async (opts?: { compress: boolean }) => { + const manifest = new Manifest('v1'); + const artifacts = await getMockArtifacts(opts); + artifacts.forEach((artifact) => manifest.addEntry(artifact)); + return manifest; +}; + +export const getMockManifestWithDiffs = async (opts?: { compress: boolean }) => { + const manifest = new Manifest('v1'); + const artifacts = await getMockArtifactsWithDiff(opts); + artifacts.forEach((artifact) => manifest.addEntry(artifact)); + return manifest; +}; + +export const getEmptyMockManifest = async (opts?: { compress: boolean }) => { + const manifest = new Manifest('v1'); + const artifacts = await getEmptyMockArtifacts(opts); + artifacts.forEach((artifact) => manifest.addEntry(artifact)); + return manifest; +}; diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/task.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/task.ts index 583f4499f591b..ba164059866ea 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/task.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/task.ts @@ -11,7 +11,8 @@ import { TaskManagerStartContract, } from '../../../../../task_manager/server'; import { EndpointAppContext } from '../../types'; -import { reportErrors } from './common'; +import { reportErrors, ManifestConstants } from './common'; +import { InternalArtifactCompleteSchema } from '../../schemas/artifacts'; export const ManifestTaskConstants = { TIMEOUT: '1m', @@ -89,37 +90,66 @@ export class ManifestTask { return; } - let errors: Error[] = []; try { - // get snapshot based on exception-list-agnostic SOs - // with diffs from last dispatched manifest - const snapshot = await manifestManager.getSnapshot(); - if (snapshot && snapshot.diffs.length > 0) { - // create new artifacts - errors = await manifestManager.syncArtifacts(snapshot, 'add'); - if (errors.length) { - reportErrors(this.logger, errors); - throw new Error('Error writing new artifacts.'); - } - // write to ingest-manager package config - errors = await manifestManager.dispatch(snapshot.manifest); - if (errors.length) { - reportErrors(this.logger, errors); - throw new Error('Error dispatching manifest.'); + // Last manifest we computed, which was saved to ES + const oldManifest = await manifestManager.getLastComputedManifest( + ManifestConstants.SCHEMA_VERSION + ); + if (oldManifest == null) { + this.logger.debug('User manifest not available yet.'); + return; + } + + // New computed manifest based on current state of exception list + const newManifest = await manifestManager.buildNewManifest( + ManifestConstants.SCHEMA_VERSION, + oldManifest + ); + const diffs = newManifest.diff(oldManifest); + + // Compress new artifacts + const adds = diffs.filter((diff) => diff.type === 'add').map((diff) => diff.id); + for (const artifactId of adds) { + const compressError = await newManifest.compressArtifact(artifactId); + if (compressError) { + throw compressError; } - // commit latest manifest state to user-artifact-manifest SO - const error = await manifestManager.commit(snapshot.manifest); + } + + // Persist new artifacts + const artifacts = adds + .map((artifactId) => newManifest.getArtifact(artifactId)) + .filter((artifact): artifact is InternalArtifactCompleteSchema => artifact !== undefined); + if (artifacts.length !== adds.length) { + throw new Error('Invalid artifact encountered.'); + } + const persistErrors = await manifestManager.pushArtifacts(artifacts); + if (persistErrors.length) { + reportErrors(this.logger, persistErrors); + throw new Error('Unable to persist new artifacts.'); + } + + // Commit latest manifest state, if different + if (diffs.length) { + const error = await manifestManager.commit(newManifest); if (error) { - reportErrors(this.logger, [error]); - throw new Error('Error committing manifest.'); - } - // clean up old artifacts - errors = await manifestManager.syncArtifacts(snapshot, 'delete'); - if (errors.length) { - reportErrors(this.logger, errors); - throw new Error('Error cleaning up outdated artifacts.'); + throw error; } } + + // Try dispatching to ingest-manager package configs + const dispatchErrors = await manifestManager.tryDispatch(newManifest); + if (dispatchErrors.length) { + reportErrors(this.logger, dispatchErrors); + throw new Error('Error dispatching manifest.'); + } + + // Try to clean up superceded artifacts + const deletes = diffs.filter((diff) => diff.type === 'delete').map((diff) => diff.id); + const deleteErrors = await manifestManager.deleteArtifacts(deletes); + if (deleteErrors.length) { + reportErrors(this.logger, deleteErrors); + } } catch (err) { this.logger.error(err); } diff --git a/x-pack/plugins/security_solution/server/endpoint/mocks.ts b/x-pack/plugins/security_solution/server/endpoint/mocks.ts index 6a8c26e08d9dd..9ca447d53bf45 100644 --- a/x-pack/plugins/security_solution/server/endpoint/mocks.ts +++ b/x-pack/plugins/security_solution/server/endpoint/mocks.ts @@ -6,8 +6,6 @@ import { ILegacyScopedClusterClient, SavedObjectsClientContract } from 'kibana/server'; import { loggingSystemMock, savedObjectsServiceMock } from 'src/core/server/mocks'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { loggerMock } from 'src/core/server/logging/logger.mock'; import { xpackMocks } from '../../../../mocks'; import { AgentService, @@ -15,28 +13,24 @@ import { ExternalCallback, } from '../../../ingest_manager/server'; import { createPackageConfigServiceMock } from '../../../ingest_manager/server/mocks'; -import { ConfigType } from '../config'; import { createMockConfig } from '../lib/detection_engine/routes/__mocks__'; import { EndpointAppContextService, EndpointAppContextServiceStartContract, } from './endpoint_app_context_services'; -import { - ManifestManagerMock, - getManifestManagerMock, -} from './services/artifacts/manifest_manager/manifest_manager.mock'; +import { ManifestManager } from './services/artifacts/manifest_manager/manifest_manager'; +import { getManifestManagerMock } from './services/artifacts/manifest_manager/manifest_manager.mock'; import { EndpointAppContext } from './types'; /** * Creates a mocked EndpointAppContext. */ export const createMockEndpointAppContext = ( - mockManifestManager?: ManifestManagerMock + mockManifestManager?: ManifestManager ): EndpointAppContext => { return { logFactory: loggingSystemMock.create(), - // @ts-ignore - config: createMockConfig() as ConfigType, + config: () => Promise.resolve(createMockConfig()), service: createMockEndpointAppContextService(mockManifestManager), }; }; @@ -45,16 +39,15 @@ export const createMockEndpointAppContext = ( * Creates a mocked EndpointAppContextService */ export const createMockEndpointAppContextService = ( - mockManifestManager?: ManifestManagerMock + mockManifestManager?: ManifestManager ): jest.Mocked => { - return { + return ({ start: jest.fn(), stop: jest.fn(), getAgentService: jest.fn(), - // @ts-ignore - getManifestManager: mockManifestManager ?? jest.fn(), + getManifestManager: jest.fn().mockReturnValue(mockManifestManager ?? jest.fn()), getScopedSavedObjectsClient: jest.fn(), - }; + } as unknown) as jest.Mocked; }; /** @@ -65,7 +58,7 @@ export const createMockEndpointAppContextServiceStartContract = (): jest.Mocked< > => { return { agentService: createMockAgentService(), - logger: loggerMock.create(), + logger: loggingSystemMock.create().get('mock_endpoint_app_context'), savedObjectsStart: savedObjectsServiceMock.createStartContract(), manifestManager: getManifestManagerMock(), registerIngestCallback: jest.fn< diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.ts b/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.ts index 1b364a04a4272..218f7c059da48 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.ts @@ -20,7 +20,7 @@ import { DownloadArtifactRequestParamsSchema, downloadArtifactRequestParamsSchema, downloadArtifactResponseSchema, - InternalArtifactSchema, + InternalArtifactCompleteSchema, } from '../../schemas/artifacts'; import { EndpointAppContext } from '../../types'; @@ -86,8 +86,8 @@ export function registerDownloadExceptionListRoute( } else { logger.debug(`Cache MISS artifact ${id}`); return scopedSOClient - .get(ArtifactConstants.SAVED_OBJECT_TYPE, id) - .then((artifact: SavedObject) => { + .get(ArtifactConstants.SAVED_OBJECT_TYPE, id) + .then((artifact: SavedObject) => { const body = Buffer.from(artifact.attributes.body, 'base64'); cache.set(id, body); return buildAndValidateResponse(artifact.attributes.identifier, body); diff --git a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/lists.mock.ts b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/lists.mock.ts index 343b192163479..2cef1f3be69c1 100644 --- a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/lists.mock.ts +++ b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/lists.mock.ts @@ -15,13 +15,13 @@ export const getTranslatedExceptionListMock = (): WrappedTranslatedExceptionList { entries: [ { - field: 'some.not.nested.field', + field: 'some.nested.field', operator: 'included', type: 'exact_cased', value: 'some value', }, ], - field: 'some.field', + field: 'some.parentField', type: 'nested', }, { diff --git a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/saved_objects.mock.ts b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/saved_objects.mock.ts index 183a819807ed2..d95627601a183 100644 --- a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/saved_objects.mock.ts +++ b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/saved_objects.mock.ts @@ -4,37 +4,53 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ArtifactConstants, buildArtifact } from '../../lib/artifacts'; +import { buildArtifact, maybeCompressArtifact, isCompressed } from '../../lib/artifacts'; import { getTranslatedExceptionListMock } from './lists.mock'; -import { InternalArtifactSchema, InternalManifestSchema } from './saved_objects'; +import { + InternalManifestSchema, + internalArtifactCompleteSchema, + InternalArtifactCompleteSchema, +} from './saved_objects'; + +const compressArtifact = async (artifact: InternalArtifactCompleteSchema) => { + const compressedArtifact = await maybeCompressArtifact(artifact); + if (!isCompressed(compressedArtifact)) { + throw new Error(`Unable to compress artifact: ${artifact.identifier}`); + } else if (!internalArtifactCompleteSchema.is(compressedArtifact)) { + throw new Error(`Incomplete artifact detected: ${artifact.identifier}`); + } + return compressedArtifact; +}; export const getInternalArtifactMock = async ( os: string, - schemaVersion: string -): Promise => { - return buildArtifact(getTranslatedExceptionListMock(), os, schemaVersion); + schemaVersion: string, + opts?: { compress: boolean } +): Promise => { + const artifact = await buildArtifact(getTranslatedExceptionListMock(), os, schemaVersion); + return opts?.compress ? compressArtifact(artifact) : artifact; }; -export const getInternalArtifactMockWithDiffs = async ( +export const getEmptyInternalArtifactMock = async ( os: string, - schemaVersion: string -): Promise => { - const mock = getTranslatedExceptionListMock(); - mock.entries.pop(); - return buildArtifact(mock, os, schemaVersion); + schemaVersion: string, + opts?: { compress: boolean } +): Promise => { + const artifact = await buildArtifact({ entries: [] }, os, schemaVersion); + return opts?.compress ? compressArtifact(artifact) : artifact; }; -export const getInternalArtifactsMock = async ( +export const getInternalArtifactMockWithDiffs = async ( os: string, - schemaVersion: string -): Promise => { - // @ts-ignore - return ArtifactConstants.SUPPORTED_OPERATING_SYSTEMS.map(async () => { - await buildArtifact(getTranslatedExceptionListMock(), os, schemaVersion); - }); + schemaVersion: string, + opts?: { compress: boolean } +): Promise => { + const mock = getTranslatedExceptionListMock(); + mock.entries.pop(); + const artifact = await buildArtifact(mock, os, schemaVersion); + return opts?.compress ? compressArtifact(artifact) : artifact; }; export const getInternalManifestMock = (): InternalManifestSchema => ({ - created: Date.now(), ids: [], }); diff --git a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/saved_objects.ts b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/saved_objects.ts index aa11f4409269a..4dea916dcb436 100644 --- a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/saved_objects.ts +++ b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/saved_objects.ts @@ -16,7 +16,7 @@ import { created } from './common'; export const body = t.string; // base64 -export const internalArtifactSchema = t.exact( +export const internalArtifactRecordSchema = t.exact( t.type({ identifier, compressionAlgorithm, @@ -25,18 +25,49 @@ export const internalArtifactSchema = t.exact( decodedSize: size, encodedSha256: sha256, encodedSize: size, - created, - body, }) ); +export type InternalArtifactRecordSchema = t.TypeOf; +export const internalArtifactAdditionalFields = { + body, +}; + +export const internalArtifactSchema = t.intersection([ + internalArtifactRecordSchema, + t.partial(internalArtifactAdditionalFields), +]); export type InternalArtifactSchema = t.TypeOf; +export const internalArtifactCompleteSchema = t.intersection([ + internalArtifactRecordSchema, + t.exact(t.type(internalArtifactAdditionalFields)), +]); +export type InternalArtifactCompleteSchema = t.TypeOf; + +export const internalArtifactCreateSchema = t.intersection([ + internalArtifactCompleteSchema, + t.exact( + t.type({ + created, + }) + ), +]); +export type InternalArtifactCreateSchema = t.TypeOf; + export const internalManifestSchema = t.exact( t.type({ - created, ids: t.array(identifier), }) ); - export type InternalManifestSchema = t.TypeOf; + +export const internalManifestCreateSchema = t.intersection([ + internalManifestSchema, + t.exact( + t.type({ + created, + }) + ), +]); +export type InternalManifestCreateSchema = t.TypeOf; diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.test.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.test.ts index 3e3b12c04d65c..0787231e242cb 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.test.ts @@ -5,7 +5,7 @@ */ import { savedObjectsClientMock } from 'src/core/server/mocks'; -import { ArtifactConstants } from '../../lib/artifacts'; +import { ArtifactConstants, getArtifactId } from '../../lib/artifacts'; import { getInternalArtifactMock } from '../../schemas/artifacts/saved_objects.mock'; import { getArtifactClientMock } from './artifact_client.mock'; import { ArtifactClient } from './artifact_client'; @@ -31,8 +31,11 @@ describe('artifact_client', () => { await artifactClient.createArtifact(artifact); expect(savedObjectsClient.create).toHaveBeenCalledWith( ArtifactConstants.SAVED_OBJECT_TYPE, - artifact, - { id: artifactClient.getArtifactId(artifact) } + { + ...artifact, + created: expect.any(Number), + }, + { id: getArtifactId(artifact) } ); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.ts index ca53a891c4d6b..6138b4fb7e6dc 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.ts @@ -5,8 +5,11 @@ */ import { SavedObject, SavedObjectsClientContract } from 'src/core/server'; -import { ArtifactConstants } from '../../lib/artifacts'; -import { InternalArtifactSchema } from '../../schemas/artifacts'; +import { ArtifactConstants, getArtifactId } from '../../lib/artifacts'; +import { + InternalArtifactCompleteSchema, + InternalArtifactCreateSchema, +} from '../../schemas/artifacts'; export class ArtifactClient { private savedObjectsClient: SavedObjectsClientContract; @@ -15,24 +18,23 @@ export class ArtifactClient { this.savedObjectsClient = savedObjectsClient; } - public getArtifactId(artifact: InternalArtifactSchema) { - return `${artifact.identifier}-${artifact.decodedSha256}`; - } - - public async getArtifact(id: string): Promise> { - return this.savedObjectsClient.get( + public async getArtifact(id: string): Promise> { + return this.savedObjectsClient.get( ArtifactConstants.SAVED_OBJECT_TYPE, id ); } public async createArtifact( - artifact: InternalArtifactSchema - ): Promise> { - return this.savedObjectsClient.create( + artifact: InternalArtifactCompleteSchema + ): Promise> { + return this.savedObjectsClient.create( ArtifactConstants.SAVED_OBJECT_TYPE, - artifact, - { id: this.getArtifactId(artifact) } + { + ...artifact, + created: Date.now(), + }, + { id: getArtifactId(artifact) } ); } diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_client.test.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_client.test.ts index fe3f193bc8ff5..6db29289e983d 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_client.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_client.test.ts @@ -38,7 +38,10 @@ describe('manifest_client', () => { await manifestClient.createManifest(manifest); expect(savedObjectsClient.create).toHaveBeenCalledWith( ManifestConstants.SAVED_OBJECT_TYPE, - manifest, + { + ...manifest, + created: expect.any(Number), + }, { id: manifestClient.getManifestId() } ); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_client.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_client.ts index 45182841e56fc..385f115e6301a 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_client.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_client.ts @@ -15,7 +15,7 @@ import { } from '../../../../common/endpoint/schema/common'; import { validate } from '../../../../common/validate'; import { ManifestConstants } from '../../lib/artifacts'; -import { InternalManifestSchema } from '../../schemas/artifacts'; +import { InternalManifestSchema, InternalManifestCreateSchema } from '../../schemas/artifacts'; interface UpdateManifestOpts { version: string; @@ -57,9 +57,12 @@ export class ManifestClient { public async createManifest( manifest: InternalManifestSchema ): Promise> { - return this.savedObjectsClient.create( + return this.savedObjectsClient.create( ManifestConstants.SAVED_OBJECT_TYPE, - manifest, + { + ...manifest, + created: Date.now(), + }, { id: this.getManifestId() } ); } diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts index 3e4fee8871b8a..08cdb9816a1c1 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts @@ -6,53 +6,34 @@ import { savedObjectsClientMock, loggingSystemMock } from 'src/core/server/mocks'; import { Logger } from 'src/core/server'; -import { createPackageConfigMock } from '../../../../../../ingest_manager/common/mocks'; +import { + createPackageConfigWithManifestMock, + createPackageConfigWithInitialManifestMock, +} from '../../../../../../ingest_manager/common/mocks'; import { PackageConfigServiceInterface } from '../../../../../../ingest_manager/server'; import { createPackageConfigServiceMock } from '../../../../../../ingest_manager/server/mocks'; -import { getFoundExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/found_exception_list_item_schema.mock'; import { listMock } from '../../../../../../lists/server/mocks'; -import { - ExceptionsCache, - Manifest, - buildArtifact, - getFullEndpointExceptionList, -} from '../../../lib/artifacts'; -import { ManifestConstants } from '../../../lib/artifacts/common'; -import { InternalArtifactSchema } from '../../../schemas/artifacts'; +import { ExceptionsCache } from '../../../lib/artifacts'; import { getArtifactClientMock } from '../artifact_client.mock'; import { getManifestClientMock } from '../manifest_client.mock'; import { ManifestManager } from './manifest_manager'; +import { + getMockManifest, + getMockArtifactsWithDiff, + getEmptyMockArtifacts, +} from '../../../lib/artifacts/mocks'; -async function mockBuildExceptionListArtifacts( - os: string, - schemaVersion: string -): Promise { - const mockExceptionClient = listMock.getExceptionListClient(); - const first = getFoundExceptionListItemSchemaMock(); - mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); - const exceptions = await getFullEndpointExceptionList(mockExceptionClient, os, schemaVersion); - return [await buildArtifact(exceptions, os, schemaVersion)]; -} - -export class ManifestManagerMock extends ManifestManager { - protected buildExceptionListArtifacts = jest - .fn() - .mockResolvedValue(mockBuildExceptionListArtifacts('linux', 'v1')); - - public getLastDispatchedManifest = jest - .fn() - .mockResolvedValue(new Manifest(new Date(), 'v1', ManifestConstants.INITIAL_VERSION)); - - protected getManifestClient = jest - .fn() - .mockReturnValue(getManifestClientMock(this.savedObjectsClient)); +export enum ManifestManagerMockType { + InitialSystemState, + NormalFlow, } export const getManifestManagerMock = (opts?: { + mockType?: ManifestManagerMockType; cache?: ExceptionsCache; packageConfigService?: jest.Mocked; savedObjectsClient?: ReturnType; -}): ManifestManagerMock => { +}): ManifestManager => { let cache = new ExceptionsCache(5); if (opts?.cache !== undefined) { cache = opts.cache; @@ -64,7 +45,11 @@ export const getManifestManagerMock = (opts?: { } packageConfigService.list = jest.fn().mockResolvedValue({ total: 1, - items: [{ version: 'abcd', ...createPackageConfigMock() }], + items: [ + { version: 'policy-1-version', ...createPackageConfigWithManifestMock() }, + { version: 'policy-2-version', ...createPackageConfigWithInitialManifestMock() }, + { version: 'policy-3-version', ...createPackageConfigWithInitialManifestMock() }, + ], }); let savedObjectsClient = savedObjectsClientMock.create(); @@ -72,6 +57,32 @@ export const getManifestManagerMock = (opts?: { savedObjectsClient = opts.savedObjectsClient; } + class ManifestManagerMock extends ManifestManager { + protected buildExceptionListArtifacts = jest.fn().mockImplementation(() => { + const mockType = opts?.mockType ?? ManifestManagerMockType.NormalFlow; + switch (mockType) { + case ManifestManagerMockType.InitialSystemState: + return getEmptyMockArtifacts(); + case ManifestManagerMockType.NormalFlow: + return getMockArtifactsWithDiff(); + } + }); + + public getLastComputedManifest = jest.fn().mockImplementation(() => { + const mockType = opts?.mockType ?? ManifestManagerMockType.NormalFlow; + switch (mockType) { + case ManifestManagerMockType.InitialSystemState: + return null; + case ManifestManagerMockType.NormalFlow: + return getMockManifest({ compress: true }); + } + }); + + protected getManifestClient = jest + .fn() + .mockReturnValue(getManifestClientMock(this.savedObjectsClient)); + } + const manifestManager = new ManifestManagerMock({ artifactClient: getArtifactClientMock(savedObjectsClient), cache, diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts index 80d325ece765c..ff331f7d017f4 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts @@ -10,40 +10,71 @@ import { createPackageConfigServiceMock } from '../../../../../../ingest_manager import { ArtifactConstants, ManifestConstants, - Manifest, ExceptionsCache, + isCompleteArtifact, } from '../../../lib/artifacts'; import { getManifestManagerMock } from './manifest_manager.mock'; describe('manifest_manager', () => { describe('ManifestManager sanity checks', () => { - test('ManifestManager can snapshot manifest', async () => { + test('ManifestManager can retrieve and diff manifests', async () => { const manifestManager = getManifestManagerMock(); - const snapshot = await manifestManager.getSnapshot(); - expect(snapshot!.diffs).toEqual([ + const oldManifest = await manifestManager.getLastComputedManifest( + ManifestConstants.SCHEMA_VERSION + ); + const newManifest = await manifestManager.buildNewManifest( + ManifestConstants.SCHEMA_VERSION, + oldManifest! + ); + expect(newManifest.diff(oldManifest!)).toEqual([ { id: - 'endpoint-exceptionlist-linux-v1-1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', + 'endpoint-exceptionlist-linux-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + type: 'delete', + }, + { + id: + 'endpoint-exceptionlist-linux-v1-0a5a2013a79f9e60682472284a1be45ab1ff68b9b43426d00d665016612c15c8', type: 'add', }, ]); - expect(snapshot!.manifest).toBeInstanceOf(Manifest); }); test('ManifestManager populates cache properly', async () => { const cache = new ExceptionsCache(5); const manifestManager = getManifestManagerMock({ cache }); - const snapshot = await manifestManager.getSnapshot(); - expect(snapshot!.diffs).toEqual([ + const oldManifest = await manifestManager.getLastComputedManifest( + ManifestConstants.SCHEMA_VERSION + ); + const newManifest = await manifestManager.buildNewManifest( + ManifestConstants.SCHEMA_VERSION, + oldManifest! + ); + const diffs = newManifest.diff(oldManifest!); + expect(diffs).toEqual([ { id: - 'endpoint-exceptionlist-linux-v1-1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', + 'endpoint-exceptionlist-linux-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + type: 'delete', + }, + { + id: + 'endpoint-exceptionlist-linux-v1-0a5a2013a79f9e60682472284a1be45ab1ff68b9b43426d00d665016612c15c8', type: 'add', }, ]); - await manifestManager.syncArtifacts(snapshot!, 'add'); - const diff = snapshot!.diffs[0]; - const entry = JSON.parse(inflateSync(cache.get(diff!.id)! as Buffer).toString()); + + const newArtifactId = diffs[1].id; + await newManifest.compressArtifact(newArtifactId); + const artifact = newManifest.getArtifact(newArtifactId)!; + + if (isCompleteArtifact(artifact)) { + await manifestManager.pushArtifacts([artifact]); // caches the artifact + } else { + throw new Error('Artifact is missing a body.'); + } + + const entry = JSON.parse(inflateSync(cache.get(newArtifactId)! as Buffer).toString()); expect(entry).toEqual({ entries: [ { @@ -52,7 +83,7 @@ describe('manifest_manager', () => { { entries: [ { - field: 'nested.field', + field: 'some.nested.field', operator: 'included', type: 'exact_cased', value: 'some value', @@ -73,28 +104,77 @@ describe('manifest_manager', () => { }); }); + test('ManifestManager cannot dispatch incomplete (uncompressed) artifact', async () => { + const packageConfigService = createPackageConfigServiceMock(); + const manifestManager = getManifestManagerMock({ packageConfigService }); + const oldManifest = await manifestManager.getLastComputedManifest( + ManifestConstants.SCHEMA_VERSION + ); + const newManifest = await manifestManager.buildNewManifest( + ManifestConstants.SCHEMA_VERSION, + oldManifest! + ); + const dispatchErrors = await manifestManager.tryDispatch(newManifest); + expect(dispatchErrors.length).toEqual(1); + expect(dispatchErrors[0].message).toEqual('Invalid manifest'); + }); + test('ManifestManager can dispatch manifest', async () => { const packageConfigService = createPackageConfigServiceMock(); const manifestManager = getManifestManagerMock({ packageConfigService }); - const snapshot = await manifestManager.getSnapshot(); - const dispatchErrors = await manifestManager.dispatch(snapshot!.manifest); + const oldManifest = await manifestManager.getLastComputedManifest( + ManifestConstants.SCHEMA_VERSION + ); + const newManifest = await manifestManager.buildNewManifest( + ManifestConstants.SCHEMA_VERSION, + oldManifest! + ); + const diffs = newManifest.diff(oldManifest!); + const newArtifactId = diffs[1].id; + await newManifest.compressArtifact(newArtifactId); + + const dispatchErrors = await manifestManager.tryDispatch(newManifest); + expect(dispatchErrors).toEqual([]); - const entries = snapshot!.manifest.getEntries(); - const artifact = Object.values(entries)[0].getArtifact(); + + // 2 policies updated... 1 is already up-to-date + expect(packageConfigService.update.mock.calls.length).toEqual(2); + expect( packageConfigService.update.mock.calls[0][2].inputs[0].config!.artifact_manifest.value ).toEqual({ - manifest_version: ManifestConstants.INITIAL_VERSION, + manifest_version: '520f6cf88b3f36a065c6ca81058d5f8690aadadf6fe857f8dec4cc37589e7283', schema_version: 'v1', artifacts: { - [artifact.identifier]: { - compression_algorithm: 'none', + 'endpoint-exceptionlist-linux-v1': { + compression_algorithm: 'zlib', encryption_algorithm: 'none', - decoded_sha256: artifact.decodedSha256, - encoded_sha256: artifact.encodedSha256, - decoded_size: artifact.decodedSize, - encoded_size: artifact.encodedSize, - relative_url: `/api/endpoint/artifacts/download/${artifact.identifier}/${artifact.decodedSha256}`, + decoded_sha256: '0a5a2013a79f9e60682472284a1be45ab1ff68b9b43426d00d665016612c15c8', + encoded_sha256: '57941169bb2c5416f9bd7224776c8462cb9a2be0fe8b87e6213e77a1d29be824', + decoded_size: 292, + encoded_size: 131, + relative_url: + '/api/endpoint/artifacts/download/endpoint-exceptionlist-linux-v1/0a5a2013a79f9e60682472284a1be45ab1ff68b9b43426d00d665016612c15c8', + }, + 'endpoint-exceptionlist-macos-v1': { + compression_algorithm: 'zlib', + encryption_algorithm: 'none', + decoded_sha256: '96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + encoded_sha256: '975382ab55d019cbab0bbac207a54e2a7d489fad6e8f6de34fc6402e5ef37b1e', + decoded_size: 432, + encoded_size: 147, + relative_url: + '/api/endpoint/artifacts/download/endpoint-exceptionlist-macos-v1/96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + }, + 'endpoint-exceptionlist-windows-v1': { + compression_algorithm: 'zlib', + encryption_algorithm: 'none', + decoded_sha256: '96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + encoded_sha256: '975382ab55d019cbab0bbac207a54e2a7d489fad6e8f6de34fc6402e5ef37b1e', + decoded_size: 432, + encoded_size: 147, + relative_url: + '/api/endpoint/artifacts/download/endpoint-exceptionlist-windows-v1/96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', }, }, }); @@ -103,29 +183,20 @@ describe('manifest_manager', () => { test('ManifestManager fails to dispatch on conflict', async () => { const packageConfigService = createPackageConfigServiceMock(); const manifestManager = getManifestManagerMock({ packageConfigService }); - const snapshot = await manifestManager.getSnapshot(); - packageConfigService.update.mockRejectedValue({ status: 409 }); - const dispatchErrors = await manifestManager.dispatch(snapshot!.manifest); + const oldManifest = await manifestManager.getLastComputedManifest( + ManifestConstants.SCHEMA_VERSION + ); + const newManifest = await manifestManager.buildNewManifest( + ManifestConstants.SCHEMA_VERSION, + oldManifest! + ); + const diffs = newManifest.diff(oldManifest!); + const newArtifactId = diffs[1].id; + await newManifest.compressArtifact(newArtifactId); + + packageConfigService.update.mockRejectedValueOnce({ status: 409 }); + const dispatchErrors = await manifestManager.tryDispatch(newManifest); expect(dispatchErrors).toEqual([{ status: 409 }]); - const entries = snapshot!.manifest.getEntries(); - const artifact = Object.values(entries)[0].getArtifact(); - expect( - packageConfigService.update.mock.calls[0][2].inputs[0].config!.artifact_manifest.value - ).toEqual({ - manifest_version: ManifestConstants.INITIAL_VERSION, - schema_version: 'v1', - artifacts: { - [artifact.identifier]: { - compression_algorithm: 'none', - encryption_algorithm: 'none', - decoded_sha256: artifact.decodedSha256, - encoded_sha256: artifact.encodedSha256, - decoded_size: artifact.decodedSize, - encoded_size: artifact.encodedSize, - relative_url: `/api/endpoint/artifacts/download/${artifact.identifier}/${artifact.decodedSha256}`, - }, - }, - }); }); test('ManifestManager can commit manifest', async () => { @@ -134,37 +205,43 @@ describe('manifest_manager', () => { savedObjectsClient, }); - const snapshot = await manifestManager.getSnapshot(); - await manifestManager.syncArtifacts(snapshot!, 'add'); - - const diff = { - id: 'abcd', - type: 'delete', - }; - snapshot!.diffs.push(diff); - - const dispatched = await manifestManager.dispatch(snapshot!.manifest); - expect(dispatched).toEqual([]); + const oldManifest = await manifestManager.getLastComputedManifest( + ManifestConstants.SCHEMA_VERSION + ); + const newManifest = await manifestManager.buildNewManifest( + ManifestConstants.SCHEMA_VERSION, + oldManifest! + ); + const diffs = newManifest.diff(oldManifest!); + const oldArtifactId = diffs[0].id; + const newArtifactId = diffs[1].id; + await newManifest.compressArtifact(newArtifactId); - await manifestManager.commit(snapshot!.manifest); + const artifact = newManifest.getArtifact(newArtifactId)!; + if (isCompleteArtifact(artifact)) { + await manifestManager.pushArtifacts([artifact]); + } else { + throw new Error('Artifact is missing a body.'); + } - await manifestManager.syncArtifacts(snapshot!, 'delete'); + await manifestManager.commit(newManifest); + await manifestManager.deleteArtifacts([oldArtifactId]); // created new artifact expect(savedObjectsClient.create.mock.calls[0][0]).toEqual( ArtifactConstants.SAVED_OBJECT_TYPE ); - // deleted old artifact - expect(savedObjectsClient.delete).toHaveBeenCalledWith( - ArtifactConstants.SAVED_OBJECT_TYPE, - 'abcd' - ); - // committed new manifest expect(savedObjectsClient.create.mock.calls[1][0]).toEqual( ManifestConstants.SAVED_OBJECT_TYPE ); + + // deleted old artifact + expect(savedObjectsClient.delete).toHaveBeenCalledWith( + ArtifactConstants.SAVED_OBJECT_TYPE, + oldArtifactId + ); }); }); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts index c8cad32ab746e..2501f07cb26e0 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts @@ -5,10 +5,11 @@ */ import { Logger, SavedObjectsClientContract } from 'src/core/server'; -import { createHash } from 'crypto'; import { PackageConfigServiceInterface } from '../../../../../../ingest_manager/server'; import { ExceptionListClient } from '../../../../../../lists/server'; import { ManifestSchemaVersion } from '../../../../../common/endpoint/schema/common'; +import { manifestDispatchSchema } from '../../../../../common/endpoint/schema/manifest'; + import { ArtifactConstants, ManifestConstants, @@ -17,11 +18,14 @@ import { getFullEndpointExceptionList, ExceptionsCache, ManifestDiff, + getArtifactId, } from '../../../lib/artifacts'; -import { InternalArtifactSchema } from '../../../schemas/artifacts'; +import { + InternalArtifactCompleteSchema, + internalArtifactCompleteSchema, +} from '../../../schemas/artifacts'; import { ArtifactClient } from '../artifact_client'; import { ManifestClient } from '../manifest_client'; -import { compressExceptionList } from '../../../lib/artifacts/lists'; export interface ManifestManagerContext { savedObjectsClient: SavedObjectsClientContract; @@ -73,82 +77,86 @@ export class ManifestManager { * state of exception-list-agnostic SOs. * * @param schemaVersion The schema version of the artifact - * @returns {Promise} An array of uncompressed artifacts built from exception-list-agnostic SOs. + * @returns {Promise} An array of uncompressed artifacts built from exception-list-agnostic SOs. * @throws Throws/rejects if there are errors building the list. */ protected async buildExceptionListArtifacts( schemaVersion: string - ): Promise { - // TODO: should wrap in try/catch? - return ArtifactConstants.SUPPORTED_OPERATING_SYSTEMS.reduce( - async (acc: Promise, os) => { - const exceptionList = await getFullEndpointExceptionList( - this.exceptionListClient, - os, - schemaVersion - ); - const artifacts = await acc; - const artifact = await buildArtifact(exceptionList, os, schemaVersion); - artifacts.push(artifact); - return Promise.resolve(artifacts); - }, - Promise.resolve([]) - ); + ): Promise { + return ArtifactConstants.SUPPORTED_OPERATING_SYSTEMS.reduce< + Promise + >(async (acc, os) => { + const exceptionList = await getFullEndpointExceptionList( + this.exceptionListClient, + os, + schemaVersion + ); + const artifacts = await acc; + const artifact = await buildArtifact(exceptionList, os, schemaVersion); + return Promise.resolve([...artifacts, artifact]); + }, Promise.resolve([])); } /** - * Writes new artifact SOs based on provided snapshot. + * Writes new artifact SO. * - * @param snapshot A ManifestSnapshot to use for writing the artifacts. - * @returns {Promise} Any errors encountered. + * @param artifact An InternalArtifactCompleteSchema representing the artifact. + * @returns {Promise} An error, if encountered, or null. */ - private async writeArtifacts(snapshot: ManifestSnapshot): Promise { - const errors: Error[] = []; - for (const diff of snapshot.diffs) { - const artifact = snapshot.manifest.getArtifact(diff.id); - if (artifact === undefined) { - throw new Error( - `Corrupted manifest detected. Diff contained artifact ${diff.id} not in manifest.` - ); + protected async pushArtifact(artifact: InternalArtifactCompleteSchema): Promise { + const artifactId = getArtifactId(artifact); + try { + // Write the artifact SO + await this.artifactClient.createArtifact(artifact); + + // Cache the compressed body of the artifact + this.cache.set(artifactId, Buffer.from(artifact.body, 'base64')); + } catch (err) { + if (err.status === 409) { + this.logger.debug(`Tried to create artifact ${artifactId}, but it already exists.`); + } else { + return err; } + } - const compressedArtifact = await compressExceptionList(Buffer.from(artifact.body, 'base64')); - artifact.body = compressedArtifact.toString('base64'); - artifact.encodedSize = compressedArtifact.byteLength; - artifact.compressionAlgorithm = 'zlib'; - artifact.encodedSha256 = createHash('sha256').update(compressedArtifact).digest('hex'); + return null; + } - try { - // Write the artifact SO - await this.artifactClient.createArtifact(artifact); - // Cache the compressed body of the artifact - this.cache.set(diff.id, Buffer.from(artifact.body, 'base64')); - } catch (err) { - if (err.status === 409) { - this.logger.debug(`Tried to create artifact ${diff.id}, but it already exists.`); - } else { - // TODO: log error here? + /** + * Writes new artifact SOs. + * + * @param artifacts An InternalArtifactCompleteSchema array representing the artifacts. + * @returns {Promise} Any errors encountered. + */ + public async pushArtifacts(artifacts: InternalArtifactCompleteSchema[]): Promise { + const errors: Error[] = []; + for (const artifact of artifacts) { + if (internalArtifactCompleteSchema.is(artifact)) { + const err = await this.pushArtifact(artifact); + if (err) { errors.push(err); } + } else { + errors.push(new Error(`Incomplete artifact: ${getArtifactId(artifact)}`)); } } return errors; } /** - * Deletes old artifact SOs based on provided snapshot. + * Deletes outdated artifact SOs. + * + * The artifact may still remain in the cache. * - * @param snapshot A ManifestSnapshot to use for deleting the artifacts. + * @param artifactIds The IDs of the artifact to delete.. * @returns {Promise} Any errors encountered. */ - private async deleteArtifacts(snapshot: ManifestSnapshot): Promise { + public async deleteArtifacts(artifactIds: string[]): Promise { const errors: Error[] = []; - for (const diff of snapshot.diffs) { + for (const artifactId of artifactIds) { try { - // Delete the artifact SO - await this.artifactClient.deleteArtifact(diff.id); - // TODO: should we delete the cache entry here? - this.logger.info(`Cleaned up artifact ${diff.id}`); + await this.artifactClient.deleteArtifact(artifactId); + this.logger.info(`Cleaned up artifact ${artifactId}`); } catch (err) { errors.push(err); } @@ -157,14 +165,14 @@ export class ManifestManager { } /** - * Returns the last dispatched manifest based on the current state of the + * Returns the last computed manifest based on the state of the * user-artifact-manifest SO. * * @param schemaVersion The schema version of the manifest. - * @returns {Promise} The last dispatched manifest, or null if does not exist. + * @returns {Promise} The last computed manifest, or null if does not exist. * @throws Throws/rejects if there is an unexpected error retrieving the manifest. */ - public async getLastDispatchedManifest(schemaVersion: string): Promise { + public async getLastComputedManifest(schemaVersion: string): Promise { try { const manifestClient = this.getManifestClient(schemaVersion); const manifestSo = await manifestClient.getManifest(); @@ -173,11 +181,7 @@ export class ManifestManager { throw new Error('No version returned for manifest.'); } - const manifest = new Manifest( - new Date(manifestSo.attributes.created), - schemaVersion, - manifestSo.version - ); + const manifest = new Manifest(schemaVersion, manifestSo.version); for (const id of manifestSo.attributes.ids) { const artifactSo = await this.artifactClient.getArtifact(id); @@ -193,89 +197,42 @@ export class ManifestManager { } /** - * Snapshots a manifest based on current state of exception-list-agnostic SOs. + * Builds a new manifest based on the current user exception list. * - * @param opts Optional parameters for snapshot retrieval. - * @param opts.initialize Initialize a new Manifest when no manifest SO can be retrieved. - * @returns {Promise} A snapshot of the manifest, or null if not initialized. + * @param schemaVersion The schema version of the manifest. + * @param baselineManifest A baseline manifest to use for initializing pre-existing artifacts. + * @returns {Promise} A new Manifest object reprenting the current exception list. */ - public async getSnapshot(opts?: ManifestSnapshotOpts): Promise { - try { - let oldManifest: Manifest | null; - - // Get the last-dispatched manifest - oldManifest = await this.getLastDispatchedManifest(ManifestConstants.SCHEMA_VERSION); - - if (oldManifest === null && opts !== undefined && opts.initialize) { - oldManifest = new Manifest( - new Date(), - ManifestConstants.SCHEMA_VERSION, - ManifestConstants.INITIAL_VERSION - ); // create empty manifest - } else if (oldManifest == null) { - this.logger.debug('Manifest does not exist yet. Waiting...'); - return null; - } - - // Build new exception list artifacts - const artifacts = await this.buildExceptionListArtifacts(ArtifactConstants.SCHEMA_VERSION); - - // Build new manifest - const newManifest = Manifest.fromArtifacts( - artifacts, - ManifestConstants.SCHEMA_VERSION, - oldManifest - ); - - // Get diffs - const diffs = newManifest.diff(oldManifest); + public async buildNewManifest( + schemaVersion: string, + baselineManifest?: Manifest + ): Promise { + // Build new exception list artifacts + const artifacts = await this.buildExceptionListArtifacts(ArtifactConstants.SCHEMA_VERSION); + + // Build new manifest + const manifest = Manifest.fromArtifacts( + artifacts, + ManifestConstants.SCHEMA_VERSION, + baselineManifest ?? Manifest.getDefault(schemaVersion) + ); - return { - manifest: newManifest, - diffs, - }; - } catch (err) { - this.logger.error(err); - return null; - } + return manifest; } /** - * Syncs artifacts based on provided snapshot. - * - * Creates artifacts that do not yet exist and cleans up old artifacts that have been - * superceded by this snapshot. + * Dispatches the manifest by writing it to the endpoint package config, if different + * from the manifest already in the config. * - * @param snapshot A ManifestSnapshot to use for sync. + * @param manifest The Manifest to dispatch. * @returns {Promise} Any errors encountered. */ - public async syncArtifacts( - snapshot: ManifestSnapshot, - diffType: 'add' | 'delete' - ): Promise { - const filteredDiffs = snapshot.diffs.filter((diff) => { - return diff.type === diffType; - }); - - const tmpSnapshot = { ...snapshot }; - tmpSnapshot.diffs = filteredDiffs; - - if (diffType === 'add') { - return this.writeArtifacts(tmpSnapshot); - } else if (diffType === 'delete') { - return this.deleteArtifacts(tmpSnapshot); + public async tryDispatch(manifest: Manifest): Promise { + const serializedManifest = manifest.toEndpointFormat(); + if (!manifestDispatchSchema.is(serializedManifest)) { + return [new Error('Invalid manifest')]; } - return [new Error(`Unsupported diff type: ${diffType}`)]; - } - - /** - * Dispatches the manifest by writing it to the endpoint package config. - * - * @param manifest The Manifest to dispatch. - * @returns {Promise} Any errors encountered. - */ - public async dispatch(manifest: Manifest): Promise { let paging = true; let page = 1; const errors: Error[] = []; @@ -293,16 +250,25 @@ export class ManifestManager { const artifactManifest = newPackageConfig.inputs[0].config.artifact_manifest ?? { value: {}, }; - artifactManifest.value = manifest.toEndpointFormat(); - newPackageConfig.inputs[0].config.artifact_manifest = artifactManifest; - - try { - await this.packageConfigService.update(this.savedObjectsClient, id, newPackageConfig); - this.logger.debug( - `Updated package config ${id} with manifest version ${manifest.getVersion()}` - ); - } catch (err) { - errors.push(err); + + const oldManifest = + Manifest.fromPkgConfig(artifactManifest.value) ?? + Manifest.getDefault(ManifestConstants.SCHEMA_VERSION); + if (!manifest.equals(oldManifest)) { + newPackageConfig.inputs[0].config.artifact_manifest = { + value: serializedManifest, + }; + + try { + await this.packageConfigService.update(this.savedObjectsClient, id, newPackageConfig); + this.logger.debug( + `Updated package config ${id} with manifest version ${manifest.getSha256()}` + ); + } catch (err) { + errors.push(err); + } + } else { + this.logger.debug(`No change in package config: ${id}`); } } else { errors.push(new Error(`Package config ${id} has no config.`)); @@ -317,46 +283,32 @@ export class ManifestManager { } /** - * Commits a manifest to indicate that it has been dispatched. + * Commits a manifest to indicate that a new version has been computed. * * @param manifest The Manifest to commit. - * @returns {Promise} An error if encountered, or null if successful. + * @returns {Promise} An error, if encountered, or null. */ public async commit(manifest: Manifest): Promise { try { const manifestClient = this.getManifestClient(manifest.getSchemaVersion()); // Commit the new manifest - if (manifest.getVersion() === ManifestConstants.INITIAL_VERSION) { - await manifestClient.createManifest(manifest.toSavedObject()); + const manifestSo = manifest.toSavedObject(); + const version = manifest.getSoVersion(); + + if (version == null) { + await manifestClient.createManifest(manifestSo); } else { - const version = manifest.getVersion(); - if (version === ManifestConstants.INITIAL_VERSION) { - throw new Error('Updating existing manifest with baseline version. Bad state.'); - } - await manifestClient.updateManifest(manifest.toSavedObject(), { + await manifestClient.updateManifest(manifestSo, { version, }); } - this.logger.info(`Committed manifest ${manifest.getVersion()}`); + this.logger.info(`Committed manifest ${manifest.getSha256()}`); } catch (err) { return err; } return null; } - - /** - * Confirms that a packageConfig exists with provided name. - */ - public async confirmPackageConfigExists(name: string) { - // TODO: what if there are multiple results? uh oh. - const { total } = await this.packageConfigService.list(this.savedObjectsClient, { - page: 1, - perPage: 20, - kuery: `ingest-package-configs.name:${name}`, - }); - return total > 0; - } } From 466380e3b6e5541041d6479d28f9fdf336ff5a8b Mon Sep 17 00:00:00 2001 From: Spencer Date: Fri, 17 Jul 2020 13:53:54 -0700 Subject: [PATCH 05/45] [kbn/dev-utils] add RunWithCommands utility (#72311) Co-authored-by: spalger --- packages/kbn-dev-utils/src/index.ts | 2 +- packages/kbn-dev-utils/src/run/cleanup.ts | 94 +++++++++ packages/kbn-dev-utils/src/run/flags.test.ts | 18 +- packages/kbn-dev-utils/src/run/flags.ts | 79 ++++--- packages/kbn-dev-utils/src/run/help.test.ts | 199 ++++++++++++++++++ packages/kbn-dev-utils/src/run/help.ts | 150 +++++++++++++ packages/kbn-dev-utils/src/run/index.ts | 7 +- packages/kbn-dev-utils/src/run/run.ts | 119 ++++------- .../src/run/run_with_commands.test.ts | 77 +++++++ .../src/run/run_with_commands.ts | 136 ++++++++++++ 10 files changed, 743 insertions(+), 138 deletions(-) create mode 100644 packages/kbn-dev-utils/src/run/cleanup.ts create mode 100644 packages/kbn-dev-utils/src/run/help.test.ts create mode 100644 packages/kbn-dev-utils/src/run/help.ts create mode 100644 packages/kbn-dev-utils/src/run/run_with_commands.test.ts create mode 100644 packages/kbn-dev-utils/src/run/run_with_commands.ts diff --git a/packages/kbn-dev-utils/src/index.ts b/packages/kbn-dev-utils/src/index.ts index 3e9e6238df9dc..582526f939e42 100644 --- a/packages/kbn-dev-utils/src/index.ts +++ b/packages/kbn-dev-utils/src/index.ts @@ -33,9 +33,9 @@ export { KBN_P12_PATH, KBN_P12_PASSWORD, } from './certs'; -export { run, createFailError, createFlagError, combineErrors, isFailError, Flags } from './run'; export { REPO_ROOT } from './repo_root'; export { KbnClient } from './kbn_client'; +export * from './run'; export * from './axios'; export * from './stdio'; export * from './ci_stats_reporter'; diff --git a/packages/kbn-dev-utils/src/run/cleanup.ts b/packages/kbn-dev-utils/src/run/cleanup.ts new file mode 100644 index 0000000000000..84c3bbcb591d2 --- /dev/null +++ b/packages/kbn-dev-utils/src/run/cleanup.ts @@ -0,0 +1,94 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { inspect } from 'util'; + +import exitHook from 'exit-hook'; + +import { ToolingLog } from '../tooling_log'; +import { isFailError } from './fail'; + +export type CleanupTask = () => void; + +export class Cleanup { + static setup(log: ToolingLog, helpText: string) { + const onUnhandledRejection = (error: any) => { + log.error('UNHANDLED PROMISE REJECTION'); + log.error( + error instanceof Error + ? error + : new Error(`non-Error type rejection value: ${inspect(error)}`) + ); + process.exit(1); + }; + + process.on('unhandledRejection', onUnhandledRejection); + + const cleanup = new Cleanup(log, helpText, [ + () => process.removeListener('unhandledRejection', onUnhandledRejection), + ]); + + cleanup.add(exitHook(() => cleanup.execute())); + + return cleanup; + } + + constructor( + private readonly log: ToolingLog, + public helpText: string, + private readonly tasks: CleanupTask[] + ) {} + + add(task: CleanupTask) { + this.tasks.push(task); + } + + execute(topLevelError?: any) { + const tasks = this.tasks.slice(0); + this.tasks.length = 0; + + for (const task of tasks) { + try { + task(); + } catch (error) { + this.onError(error); + } + } + + if (topLevelError) { + this.onError(topLevelError); + } + } + + private onError(error: any) { + if (isFailError(error)) { + this.log.error(error.message); + + if (error.showHelp) { + this.log.write(this.helpText); + } + + process.exitCode = error.exitCode; + } else { + this.log.error('UNHANDLED ERROR'); + this.log.error(error); + process.exitCode = 1; + } + } +} diff --git a/packages/kbn-dev-utils/src/run/flags.test.ts b/packages/kbn-dev-utils/src/run/flags.test.ts index c730067a84f46..f6ff70b7abeb4 100644 --- a/packages/kbn-dev-utils/src/run/flags.test.ts +++ b/packages/kbn-dev-utils/src/run/flags.test.ts @@ -22,14 +22,12 @@ import { getFlags } from './flags'; it('gets flags correctly', () => { expect( getFlags(['-a', '--abc=bcd', '--foo=bar', '--no-bar', '--foo=baz', '--box', 'yes', '-zxy'], { - flags: { - boolean: ['x'], - string: ['abc'], - alias: { - x: 'extra', - }, - allowUnexpected: true, + boolean: ['x'], + string: ['abc'], + alias: { + x: 'extra', }, + allowUnexpected: true, }) ).toMatchInlineSnapshot(` Object { @@ -60,10 +58,8 @@ it('gets flags correctly', () => { it('guesses types for unexpected flags', () => { expect( getFlags(['-abc', '--abc=bcd', '--no-foo', '--bar'], { - flags: { - allowUnexpected: true, - guessTypesForUnexpectedFlags: true, - }, + allowUnexpected: true, + guessTypesForUnexpectedFlags: true, }) ).toMatchInlineSnapshot(` Object { diff --git a/packages/kbn-dev-utils/src/run/flags.ts b/packages/kbn-dev-utils/src/run/flags.ts index c809a40d8512b..12642bceca15a 100644 --- a/packages/kbn-dev-utils/src/run/flags.ts +++ b/packages/kbn-dev-utils/src/run/flags.ts @@ -17,12 +17,9 @@ * under the License. */ -import { relative } from 'path'; - -import dedent from 'dedent'; import getopts from 'getopts'; -import { Options } from './run'; +import { RunOptions } from './run'; export interface Flags { verbose: boolean; @@ -36,23 +33,52 @@ export interface Flags { [key: string]: undefined | boolean | string | string[]; } -export function getFlags(argv: string[], options: Options): Flags { +export interface FlagOptions { + allowUnexpected?: boolean; + guessTypesForUnexpectedFlags?: boolean; + help?: string; + alias?: { [key: string]: string | string[] }; + boolean?: string[]; + string?: string[]; + default?: { [key: string]: any }; +} + +export function mergeFlagOptions(global: FlagOptions = {}, local: FlagOptions = {}): FlagOptions { + return { + alias: { + ...global.alias, + ...local.alias, + }, + boolean: [...(global.boolean || []), ...(local.boolean || [])], + string: [...(global.string || []), ...(local.string || [])], + default: { + ...global.alias, + ...local.alias, + }, + + help: local.help, + + allowUnexpected: !!(global.allowUnexpected || local.allowUnexpected), + guessTypesForUnexpectedFlags: !!(global.allowUnexpected || local.allowUnexpected), + }; +} + +export function getFlags(argv: string[], flagOptions: RunOptions['flags'] = {}): Flags { const unexpectedNames = new Set(); - const flagOpts = options.flags || {}; const { verbose, quiet, silent, debug, help, _, ...others } = getopts(argv, { - string: flagOpts.string, - boolean: [...(flagOpts.boolean || []), 'verbose', 'quiet', 'silent', 'debug', 'help'], + string: flagOptions.string, + boolean: [...(flagOptions.boolean || []), 'verbose', 'quiet', 'silent', 'debug', 'help'], alias: { - ...(flagOpts.alias || {}), + ...flagOptions.alias, v: 'verbose', }, - default: flagOpts.default, + default: flagOptions.default, unknown: (name: string) => { unexpectedNames.add(name); - return flagOpts.guessTypesForUnexpectedFlags; + return !!flagOptions.guessTypesForUnexpectedFlags; }, - } as any); + }); const unexpected: string[] = []; for (const unexpectedName of unexpectedNames) { @@ -119,32 +145,3 @@ export function getFlags(argv: string[], options: Options): Flags { ...others, }; } - -export function getHelp(options: Options) { - const usage = options.usage || `node ${relative(process.cwd(), process.argv[1])}`; - - const optionHelp = ( - dedent(options?.flags?.help || '') + - '\n' + - dedent` - --verbose, -v Log verbosely - --debug Log debug messages (less than verbose) - --quiet Only log errors - --silent Don't log anything - --help Show this message - ` - ) - .split('\n') - .filter(Boolean) - .join('\n '); - - return ` - ${usage} - - ${dedent(options.description || 'Runs a dev task') - .split('\n') - .join('\n ')} - - Options: - ${optionHelp + '\n\n'}`; -} diff --git a/packages/kbn-dev-utils/src/run/help.test.ts b/packages/kbn-dev-utils/src/run/help.test.ts new file mode 100644 index 0000000000000..27be7ad28b81a --- /dev/null +++ b/packages/kbn-dev-utils/src/run/help.test.ts @@ -0,0 +1,199 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { getCommandLevelHelp, getHelp, getHelpForAllCommands } from './help'; +import { Command } from './run_with_commands'; + +const fooCommand: Command = { + description: ` + Some thing that we wrote to help us execute things. + + Example: + + foo = bar = baz + + Are you getting it? + `, + name: 'foo', + run: () => {}, + flags: { + help: ` + --foo Some flag + --bar Another flag + Secondary info + --baz, -b Hey hello + `, + }, + usage: 'foo [...names]', +}; + +const barCommand: Command = { + description: ` + Some other thing that we wrote to help us execute things. + `, + name: 'bar', + run: () => {}, + flags: { + help: ` + --baz, -b Hey hello + `, + }, + usage: 'bar [...names]', +}; + +describe('getHelp()', () => { + it('returns the expected output', () => { + expect( + getHelp({ + description: fooCommand.description, + flagHelp: fooCommand.flags?.help, + usage: ` + node scripts/foo --bar --baz + `, + }) + ).toMatchInlineSnapshot(` + " + node scripts/foo --bar --baz + + Some thing that we wrote to help us execute things. + + Example: + + foo = bar = baz + + Are you getting it? + + Options: + --foo Some flag + --bar Another flag + Secondary info + --baz, -b Hey hello + --verbose, -v Log verbosely + --debug Log debug messages (less than verbose) + --quiet Only log errors + --silent Don't log anything + --help Show this message + + " + `); + }); +}); + +describe('getCommandLevelHelp()', () => { + it('returns the expected output', () => { + expect( + getCommandLevelHelp({ + command: fooCommand, + globalFlagHelp: ` + --global-flag some flag that applies to all commands + `, + }) + ).toMatchInlineSnapshot(` + " + node node_modules/jest-worker/build/workers/processChild.js foo [...names] + + Some thing that we wrote to help us execute things. + + Example: + + foo = bar = baz + + Are you getting it? + + Command-specific options: + --foo Some flag + --bar Another flag + Secondary info + --baz, -b Hey hello + + Global options: + --global-flag some flag that applies to all commands + --verbose, -v Log verbosely + --debug Log debug messages (less than verbose) + --quiet Only log errors + --silent Don't log anything + --help Show this message + + To see the help for other commands run: + node node_modules/jest-worker/build/workers/processChild.js help [command] + + To see the list of commands run: + node node_modules/jest-worker/build/workers/processChild.js --help + + " + `); + }); +}); + +describe('getHelpForAllCommands()', () => { + it('returns the expected output', () => { + expect( + getHelpForAllCommands({ + commands: [fooCommand, barCommand], + globalFlagHelp: ` + --global-flag some flag that applies to all commands + `, + usage: ` + node scripts/my_cli + `, + }) + ).toMatchInlineSnapshot(` + " + node scripts/my_cli [command] [...args] + + Runs a dev task + + Commands: + foo [...names] + Some thing that we wrote to help us execute things. + + Example: + + foo = bar = baz + + Are you getting it? + + Options: + --foo Some flag + --bar Another flag + Secondary info + --baz, -b Hey hello + + bar [...names] + Some other thing that we wrote to help us execute things. + + Options: + --baz, -b Hey hello + + + Global options: + --global-flag some flag that applies to all commands + --verbose, -v Log verbosely + --debug Log debug messages (less than verbose) + --quiet Only log errors + --silent Don't log anything + --help Show this message + + To show the help information about a specific command run: + node scripts/my_cli help [command] + + " + `); + }); +}); diff --git a/packages/kbn-dev-utils/src/run/help.ts b/packages/kbn-dev-utils/src/run/help.ts new file mode 100644 index 0000000000000..351c01da5ebe2 --- /dev/null +++ b/packages/kbn-dev-utils/src/run/help.ts @@ -0,0 +1,150 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import Path from 'path'; + +import 'core-js/features/string/repeat'; +import dedent from 'dedent'; + +import { Command } from './run_with_commands'; + +const DEFAULT_GLOBAL_USAGE = `node ${Path.relative(process.cwd(), process.argv[1])}`; +export const GLOBAL_FLAGS = dedent` + --verbose, -v Log verbosely + --debug Log debug messages (less than verbose) + --quiet Only log errors + --silent Don't log anything + --help Show this message +`; + +export function indent(str: string, depth: number) { + const prefix = ' '.repeat(depth); + return str + .split('\n') + .map((line, i) => `${i > 0 ? `\n${prefix}` : ''}${line}`) + .join(''); +} + +export function joinAndTrimLines(...strings: Array) { + return strings.filter(Boolean).join('\n').split('\n').filter(Boolean).join(`\n`); +} + +export function getHelp({ + description, + usage, + flagHelp, +}: { + description?: string; + usage?: string; + flagHelp?: string; +}) { + const optionHelp = joinAndTrimLines(dedent(flagHelp || ''), GLOBAL_FLAGS); + + return ` + ${dedent(usage || '') || DEFAULT_GLOBAL_USAGE} + + ${indent(dedent(description || 'Runs a dev task'), 2)} + + Options: + ${indent(optionHelp, 4)}\n\n`; +} + +export function getCommandLevelHelp({ + usage, + globalFlagHelp, + command, +}: { + usage?: string; + globalFlagHelp?: string; + command: Command; +}) { + const globalUsage = dedent(usage || '') || DEFAULT_GLOBAL_USAGE; + const globalHelp = joinAndTrimLines(dedent(globalFlagHelp || ''), GLOBAL_FLAGS); + + const commandUsage = dedent(command.usage || '') || `${command.name} [...args]`; + const commandFlags = joinAndTrimLines(dedent(command.flags?.help || '')); + + return ` + ${globalUsage} ${commandUsage} + + ${indent(dedent(command.description || 'Runs a dev task'), 2)} + + Command-specific options: + ${indent(commandFlags, 4)} + + Global options: + ${indent(globalHelp, 4)} + + To see the help for other commands run: + ${globalUsage} help [command] + + To see the list of commands run: + ${globalUsage} --help\n\n`; +} + +export function getHelpForAllCommands({ + description, + usage, + globalFlagHelp, + commands, +}: { + description?: string; + usage?: string; + globalFlagHelp?: string; + commands: Array>; +}) { + const globalUsage = dedent(usage || '') || DEFAULT_GLOBAL_USAGE; + const globalHelp = joinAndTrimLines(dedent(globalFlagHelp || ''), GLOBAL_FLAGS); + + const commandsHelp = commands + .map((command) => { + const options = command.flags?.help + ? '\n' + + dedent` + Options: + ${indent( + joinAndTrimLines(dedent(command.flags?.help || '')), + ' '.length + )} + ` + + '\n' + : ''; + + return [ + dedent(command.usage || '') || command.name, + ` ${indent(dedent(command.description || 'Runs a dev task'), 2)}`, + ...([indent(options, 2)] || []), + ].join('\n'); + }) + .join('\n'); + + return ` + ${globalUsage} [command] [...args] + + ${indent(dedent(description || 'Runs a dev task'), 2)} + + Commands: + ${indent(commandsHelp, 4)} + + Global options: + ${indent(globalHelp, 4)} + + To show the help information about a specific command run: + ${globalUsage} help [command]\n\n`; +} diff --git a/packages/kbn-dev-utils/src/run/index.ts b/packages/kbn-dev-utils/src/run/index.ts index 5e1a42deefffb..070ce740bf202 100644 --- a/packages/kbn-dev-utils/src/run/index.ts +++ b/packages/kbn-dev-utils/src/run/index.ts @@ -17,6 +17,7 @@ * under the License. */ -export { run } from './run'; -export { Flags } from './flags'; -export { createFailError, createFlagError, combineErrors, isFailError } from './fail'; +export * from './run'; +export * from './run_with_commands'; +export * from './flags'; +export * from './fail'; diff --git a/packages/kbn-dev-utils/src/run/run.ts b/packages/kbn-dev-utils/src/run/run.ts index 029d428565163..2a844bcbc27eb 100644 --- a/packages/kbn-dev-utils/src/run/run.ts +++ b/packages/kbn-dev-utils/src/run/run.ts @@ -17,48 +17,37 @@ * under the License. */ -import { inspect } from 'util'; - -// @ts-ignore @types are outdated and module is super simple -import exitHook from 'exit-hook'; - import { pickLevelFromFlags, ToolingLog, LogLevel } from '../tooling_log'; -import { createFlagError, isFailError } from './fail'; -import { Flags, getFlags, getHelp } from './flags'; +import { createFlagError } from './fail'; +import { Flags, getFlags, FlagOptions } from './flags'; import { ProcRunner, withProcRunner } from '../proc_runner'; +import { getHelp } from './help'; +import { CleanupTask, Cleanup } from './cleanup'; -type CleanupTask = () => void; -type RunFn = (args: { +export interface RunContext { log: ToolingLog; flags: Flags; procRunner: ProcRunner; addCleanupTask: (task: CleanupTask) => void; -}) => Promise | void; +} +export type RunFn = (context: RunContext) => Promise | void; -export interface Options { +export interface RunOptions { usage?: string; description?: string; log?: { defaultLevel?: LogLevel; }; - flags?: { - allowUnexpected?: boolean; - guessTypesForUnexpectedFlags?: boolean; - help?: string; - alias?: { [key: string]: string | string[] }; - boolean?: string[]; - string?: string[]; - default?: { [key: string]: any }; - }; + flags?: FlagOptions; } -export async function run(fn: RunFn, options: Options = {}) { - const flags = getFlags(process.argv.slice(2), options); - - if (flags.help) { - process.stderr.write(getHelp(options)); - process.exit(1); - } +export async function run(fn: RunFn, options: RunOptions = {}) { + const flags = getFlags(process.argv.slice(2), options.flags); + const helpText = getHelp({ + description: options.description, + usage: options.usage, + flagHelp: options.flags?.help, + }); const log = new ToolingLog({ level: pickLevelFromFlags(flags, { @@ -67,67 +56,33 @@ export async function run(fn: RunFn, options: Options = {}) { writeTo: process.stdout, }); - process.on('unhandledRejection', (error) => { - log.error('UNHANDLED PROMISE REJECTION'); - log.error( - error instanceof Error - ? error - : new Error(`non-Error type rejection value: ${inspect(error)}`) - ); - process.exit(1); - }); - - const handleErrorWithoutExit = (error: any) => { - if (isFailError(error)) { - log.error(error.message); - - if (error.showHelp) { - log.write(getHelp(options)); - } - - process.exitCode = error.exitCode; - } else { - log.error('UNHANDLED ERROR'); - log.error(error); - process.exitCode = 1; - } - }; - - const doCleanup = () => { - const tasks = cleanupTasks.slice(0); - cleanupTasks.length = 0; + if (flags.help) { + log.write(helpText); + process.exit(); + } - for (const task of tasks) { - try { - task(); - } catch (error) { - handleErrorWithoutExit(error); - } - } - }; + const cleanup = Cleanup.setup(log, helpText); - const unhookExit: CleanupTask = exitHook(doCleanup); - const cleanupTasks: CleanupTask[] = [unhookExit]; + if (!options.flags?.allowUnexpected && flags.unexpected.length) { + const error = createFlagError(`Unknown flag(s) "${flags.unexpected.join('", "')}"`); + cleanup.execute(error); + return; + } try { - if (!options.flags?.allowUnexpected && flags.unexpected.length) { - throw createFlagError(`Unknown flag(s) "${flags.unexpected.join('", "')}"`); - } - - try { - await withProcRunner(log, async (procRunner) => { - await fn({ - log, - flags, - procRunner, - addCleanupTask: (task: CleanupTask) => cleanupTasks.push(task), - }); + await withProcRunner(log, async (procRunner) => { + await fn({ + log, + flags, + procRunner, + addCleanupTask: cleanup.add.bind(cleanup), }); - } finally { - doCleanup(); - } + }); } catch (error) { - handleErrorWithoutExit(error); + cleanup.execute(error); + // process.exitCode is set by `cleanup` when necessary process.exit(); + } finally { + cleanup.execute(); } } diff --git a/packages/kbn-dev-utils/src/run/run_with_commands.test.ts b/packages/kbn-dev-utils/src/run/run_with_commands.test.ts new file mode 100644 index 0000000000000..eb7708998751c --- /dev/null +++ b/packages/kbn-dev-utils/src/run/run_with_commands.test.ts @@ -0,0 +1,77 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { RunWithCommands } from './run_with_commands'; +import { ToolingLog, ToolingLogCollectingWriter } from '../tooling_log'; +import { ProcRunner } from '../proc_runner'; + +const testLog = new ToolingLog(); +const testLogWriter = new ToolingLogCollectingWriter(); +testLog.setWriters([testLogWriter]); + +const testCli = new RunWithCommands({ + usage: 'node scripts/test_cli [...options]', + description: 'test cli', + extendContext: async () => { + return { + extraContext: true, + }; + }, + globalFlags: { + boolean: ['some-bool'], + help: ` + --some-bool description + `, + }, +}); + +beforeEach(() => { + process.argv = ['node', 'scripts/test_cli', 'foo', '--some-bool']; + jest.clearAllMocks(); +}); + +it('extends the context using extendContext()', async () => { + const context: any = await new Promise((resolve) => { + testCli.command({ name: 'foo', description: 'some command', run: resolve }).execute(); + }); + + expect(context).toEqual({ + log: expect.any(ToolingLog), + flags: expect.any(Object), + addCleanupTask: expect.any(Function), + procRunner: expect.any(ProcRunner), + extraContext: true, + }); + + expect(context.flags).toMatchInlineSnapshot(` + Object { + "_": Array [ + "foo", + ], + "debug": false, + "help": false, + "quiet": false, + "silent": false, + "some-bool": true, + "unexpected": Array [], + "v": false, + "verbose": false, + } + `); +}); diff --git a/packages/kbn-dev-utils/src/run/run_with_commands.ts b/packages/kbn-dev-utils/src/run/run_with_commands.ts new file mode 100644 index 0000000000000..9fb069e4b2d35 --- /dev/null +++ b/packages/kbn-dev-utils/src/run/run_with_commands.ts @@ -0,0 +1,136 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ToolingLog, pickLevelFromFlags } from '../tooling_log'; +import { RunContext, RunOptions } from './run'; +import { getFlags, FlagOptions, mergeFlagOptions } from './flags'; +import { Cleanup } from './cleanup'; +import { getHelpForAllCommands, getCommandLevelHelp } from './help'; +import { createFlagError } from './fail'; +import { withProcRunner } from '../proc_runner'; + +export type CommandRunFn = (context: RunContext & T) => Promise | void; + +export interface Command { + name: string; + run: CommandRunFn; + description: RunOptions['description']; + usage?: RunOptions['usage']; + flags?: FlagOptions; +} + +export interface RunWithCommandsOptions { + log?: RunOptions['log']; + description?: RunOptions['description']; + usage?: RunOptions['usage']; + globalFlags?: FlagOptions; + extendContext?(context: RunContext): Promise | T; +} + +export class RunWithCommands { + constructor( + private readonly options: RunWithCommandsOptions, + private readonly commands: Array> = [] + ) {} + + command(options: Command) { + return new RunWithCommands(this.options, this.commands.concat(options)); + } + + async execute() { + const globalFlags = getFlags(process.argv.slice(2), { + allowUnexpected: true, + }); + + const isHelpCommand = globalFlags._[0] === 'help'; + const commandName = isHelpCommand ? globalFlags._[1] : globalFlags._[0]; + const command = this.commands.find((c) => c.name === commandName); + const log = new ToolingLog({ + level: pickLevelFromFlags(globalFlags, { + default: this.options.log?.defaultLevel, + }), + writeTo: process.stdout, + }); + + const globalHelp = getHelpForAllCommands({ + description: this.options.description, + usage: this.options.usage, + globalFlagHelp: this.options.globalFlags?.help, + commands: this.commands, + }); + const cleanup = Cleanup.setup(log, globalHelp); + + if (!command) { + if (globalFlags.help) { + log.write(globalHelp); + process.exit(); + } + + const error = createFlagError( + commandName ? `unknown command [${commandName}]` : `missing command name` + ); + cleanup.execute(error); + process.exit(1); + } + + const commandFlagOptions = mergeFlagOptions(this.options.globalFlags, command.flags); + const commandFlags = getFlags(process.argv.slice(2), commandFlagOptions); + const commandHelp = getCommandLevelHelp({ + usage: this.options.usage, + globalFlagHelp: this.options.globalFlags?.help, + command, + }); + cleanup.helpText = commandHelp; + + if (commandFlags.help || isHelpCommand) { + cleanup.execute(); + log.write(commandHelp); + process.exit(); + } + + if (!commandFlagOptions.allowUnexpected && commandFlags.unexpected.length) { + cleanup.execute(createFlagError(`Unknown flag(s) "${commandFlags.unexpected.join('", "')}"`)); + return; + } + + try { + await withProcRunner(log, async (procRunner) => { + const context: RunContext = { + log, + flags: commandFlags, + procRunner, + addCleanupTask: cleanup.add, + }; + + const extendedContext = { + ...context, + ...(this.options.extendContext ? await this.options.extendContext(context) : ({} as T)), + }; + + await command.run(extendedContext); + }); + } catch (error) { + cleanup.execute(error); + // exitCode is set by `cleanup` when necessary + process.exit(); + } finally { + cleanup.execute(); + } + } +} From f487867fda2f3c5386d28525dd72958af5fbc81a Mon Sep 17 00:00:00 2001 From: "Devin W. Hurley" Date: Fri, 17 Jul 2020 17:13:34 -0400 Subject: [PATCH 06/45] [SIEM] Updates consumer in export_rule archive (#72324) --- .../es_archives/export_rule/data.json.gz | Bin 1911 -> 1931 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/x-pack/test/security_solution_cypress/es_archives/export_rule/data.json.gz b/x-pack/test/security_solution_cypress/es_archives/export_rule/data.json.gz index 3c1a0f663a8eeb58dbe0626b067a086f52f975ef..373251d9e4f93d8097029345aa45aba959dd7a08 100644 GIT binary patch literal 1931 zcmV;62Xy!!iwFpN;}Kr~17u-zVJ>QOZ*Bn9Sy@x#Iu?HCS9tPtmv&;ud#3IKTLYIR z&ZcOR0rP zQ2QVk9uO6B(g59I2*>4As>POSO?7>VDL8Jp<)Rq+A^0 zu@tQjiH~B`)29@ch%9*7K`FGC7UWXENfI+y{MT&p-yceJDbfYoPjdSh!J|4O{TNYJ zE3>8K6hBFhqlA3Ai*BMXp9W(Z96u+2?j~VpzkhL^-UiX({gZwijjfxZ?uQpAV*Gr0 zeE1wb+~2y*L(R&_ooziLqEQx~efcax_bG0CaW9W{@BGUP-S{*5tdYUrr{ntuxec$` z?a@u=V(i_thkPW?+C$6KlcV-t=YSfY2JH(keu%WaTVdd!VeefXSXbo!D;1$(+%-}& zW}hvy)4#g!95wE*j!i+2u59h}^8Sf_y=GQ)eR#s#G+mA8Mq^p=B#z|1-2mnhl znvf+{6SI0d@qdZ*?$7zyrB@+~vw@y)pMy!%b5+A__NmkKW(F!cS zAT%U#e~%`?usf-VSWsGF3NZ^ZlFH0nZG!}09N?Uboe{>@?wbQ~nJ~i117_=43FSC@ zg(pi!(zG~vR1I5WRlCRtaF(%Ij6kVk%i8ng8mWJ%JVP;?6y+Zkr8&GI;XdSqxv#fFjq$1JB-2_rP_ z6pf{&=$cj~VvgprtrP`)G;eCr3uOuRP@48cCaa+~;5={?q zaj#`K!Wo8gU7wK|VZ%<4WYRE8TTVI$PoehK>wzkp^m^UbT%-j})oq%FYT5=?y(WD1 zP1{6n;5k8P))r(hz=!q&&N31%#fYH?=s@985(YVlf!v%XkF}-5O?==3**Vv*|4QoDrHrCvw|XriNZovS=4T13^8=_K%Vu{b~(n>0Orwp>cR8Vd$E6co>u0<;w9 zSTImXwbrP%RyOP+Hk?G*v`p6vu%-sK>#DkrVdAj7rm7hzLP6x%x)znYK1coeB&_v% z`R0(~JsJKU30rSZoRX4R?`-9r^Tn6)=>!2z__EN{L>exhnLBf6)JCMAAz9>~XMmSy zqGC*{Id!kDElMw~$eDSw_)gT!mJ7>zXvO(&*IftB|D65%j{N_=v-559(Q$SdsuT`G z74_ppI^Tk$Ax!|@o?PB$YFweXidk_BZI&sR;~ z)l}aO4K;Lh13AdThHqBX7nV-_sV!E-9Fp56^{QmpIL#r{j`of;hY33({gz$V>Y9pZ zIzak1Nj*_h&Eb4Dy%2Jwa827OCmXo*G08@VxA?Q%;kvv3)~+(!@ttj@(6cpw3`RWM zs!k(sf^NN=xq7L|uG?F9ITbHyoU8QifG`FA_y8+~7fLSmk4k$WEWaFmgf=2F{Q5=b zpa5jS;3=vT%eIGI&VsYyQpIr}<_lFGnQCzdhw{Ld<25%qj^7lp*C2n@vTdFWR-VvO z@vdZ<%ph-g>zIj`l*4${L)CeGN$6hpaO3{vyz`hY`j;#F*H36%o~Ajv=QSxre^A8BCHKFKxU|FmnGyFaq#^D9DB_xK&wYMS z$i2Gfn!iBE%Xa+W`4+tA0>)e0vrPS08gD0t&1J>*qp2_Ga6f;y=t9g7#AMD|Jt+54 zx;P%o`}s#iC|p|dUy4?f&oN_@clISbXGt>LL&5L>EqSWWIXWT$$k>XXqncXXGo8wl zOiAcv>l%|QIK*L@m0^?xgWZVZY&8_Xr)zarTmHm1i_MB1a&qC_YV)_b5)?acdLxiq RS$ef{{sZ~)cm47m007H2!|VV6 literal 1911 zcmV--2Z;C|iwFP!000026V+K+bK5u)e)q57>S>Np0>xWf^WZDC*LP&cj<=>%APGva zAd(?KpOyc<4T@4oiL_-osmZGB2cZvie|?|;b%lg|2cfP?a=Gsl3(v#^-mt$tJATV zWJjlC)6n9*)2;pv(?5+)Pu%D(L|fOHPW+Cwb-rVs)7!5s3v~UalNb^IY#ROH`E7r% zb9=sTWNh!;LWgI!kL>FuH^a-b%i&*tEqi%-ACXkLN*LyX`ehCuu*@b?GADkPQt{Z2 zStdcQ@TCL`5^Tz7w7RH^RAj4~d7+`6VfHk$?V83-jlkAp!^R}S9wRbFBF#vNFv$ej zWDE!)#B7lg97J5;$cOMY=RkxfdFt*yBylQ~G-IT^b%Iko;gz(KSY=`qrxl$Qt-#`C zlm#>zZn4-OUrg&G${4FK1%&%4O>zJmZIA#w_$24zN0f^f_sxMgi#Zkaj_{XRDH9}p zg(rZF?3-phQ(j4A^3J-EZT_TYv z7(&FB3E&K=7h&;`FRv!pACrbn?r8)<1+5VphNEGIhk5plGm<`5v0>CF5znbr!UR9` zizd@kG=!=|BJe`Cm7<_c7ELYsqHJ9QoFs!RmDSK136b#`fCo5v<$3i%cAQkRoq)C z2}w$zTwl(}l=5-kk5g%wiRwwm;3?GJ%X*;7rhQ-Ykb{xmRh_P(tA?c$)$PJp&#(;a z_^#~-Mr%p-1bjHXBWX&5T8ucpgANqbk_gB_4CLl4zHikMSMfoNWaoUDg-!^g5vjLi zM!ledktPOcqLbt$S4CX3h^i;Eso?8!aey*DZF<^9P3pC{KO#ZK$YSHA5p*mVE~Hv> z(t1%gxWIfojj&-Fj_VVo`j+FUnnqyyFx{?-bR1$ov@H#V<*qOAa4`v6gF(JIr1(IF ze?WuY+Y_gxWZpY_dFO1^o=yI7Ua$(s3H=O@g+p*#N&)L6i$p3E}8_%nck@Le)WpEg(co@af`5qh% zNeu7~9`wYCM;b(D1m~tq)g}2J?%xdN6yWhnL z#KoY%tOlF!JTu%EiSyZL^*vuiM^G~Gz~3=R3%fKe2t3>J)h>2D)$kmodRCyTfvxG- z#wO7{qoTgBbmmWMxgr*jTsNtkl40dEhfv$-9cd2Jc1VXktBu-7#Vi?NZJnf^s;TC1 zKAWCoa-?t!%PuDyxb!hjCs_2vlicC*V*9OKWxnG(+e)ElZ2&opc%)aIM&1P7%WmfC zr6#Lwt>NWN{JF8xFGiFr@Fzw@DZEf}see`41LgVU=p(d|EG4gBbao0rmJFVuI<;(l z*ySuZA1+lA4Pm}e<&mit_i-o>V%lzZmE-tL0ecPdS1nuT`JnNHmWnqe%X9{L!@I{^ zyrdk)s{yXg>ouW!-NTjpm;KITy69hy>|Z~jak&WDn(KB=!#0rXYX5ALS`AelfQ13z zJ*Z|>g)3qT58!5jXV;|1Pn2-Lg9KguQ6j)GB`^q(3 zIR~0CuzpuIlZ25g;vG`aF?GAGTgq>T`xnPY9~4H%MA;+$IR0JPh9sPjj-x{)4GrmS zO|z6fRBB2Kv7b<@Gc@M@mLQ%lEkrbnMCn5DZMB#fy)HCx!+b|f;GzbX{XEwO8HZR5 zN{<_u^}3En^icod?e3L*7n)g;6tA2MFL%AtLqPcfjYgRu{50yrgi2)fKppF@X_Y?Z z$sx)n9!XEbZ7^vpV~Q$+UvZ194}jBlE&XQw`^PIdtegmCJAO(c*rxC4)-RF>BdF@v zBCffXQI5FtgCbroxqmX^(hloqM%*NUUJpQaVntB-W?-< xLDFU@fKNkh2h~6E&12KBLryNdYc_wKD?zdIsy70;m8n%L=YRStDGUA`001%rx*-4n From f0d3cb96a4bc453150b74ecef923e5423571c647 Mon Sep 17 00:00:00 2001 From: Chris Roberson Date: Fri, 17 Jul 2020 17:15:25 -0400 Subject: [PATCH 07/45] [Monitoring] Fix issue with ES node detail status (#72298) * Fix issue with ES node detail status * Add test --- .../monitoring/public/alerts/status.tsx | 4 ++ .../components/elasticsearch/node/advanced.js | 4 +- .../elasticsearch/node_detail_status/index.js | 9 ++- .../public/directives/main/index.html | 1 + .../elasticsearch/node/advanced/index.js | 17 +++++- .../monitoring/elasticsearch/node_detail.js | 59 +++++++++++++++++++ .../monitoring/elasticsearch_node_detail.js | 4 ++ 7 files changed, 92 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/monitoring/public/alerts/status.tsx b/x-pack/plugins/monitoring/public/alerts/status.tsx index d15dcc9974863..9c262884d7257 100644 --- a/x-pack/plugins/monitoring/public/alerts/status.tsx +++ b/x-pack/plugins/monitoring/public/alerts/status.tsx @@ -20,6 +20,10 @@ interface Props { export const AlertsStatus: React.FC = (props: Props) => { const { alerts, showBadge = false, showOnlyCount = false } = props; + if (!alerts) { + return null; + } + let atLeastOneDanger = false; const count = Object.values(alerts).reduce((cnt, alertStatus) => { if (alertStatus.states.length) { diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/node/advanced.js b/x-pack/plugins/monitoring/public/components/elasticsearch/node/advanced.js index 6fea34ed9c901..b2a17515bbb96 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/node/advanced.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/node/advanced.js @@ -19,7 +19,7 @@ import { NodeDetailStatus } from '../node_detail_status'; import { MonitoringTimeseriesContainer } from '../../chart'; import { FormattedMessage } from '@kbn/i18n/react'; -export const AdvancedNode = ({ nodeSummary, metrics, ...props }) => { +export const AdvancedNode = ({ nodeSummary, metrics, alerts, ...props }) => { const metricsToShow = [ metrics.node_gc, metrics.node_gc_time, @@ -50,7 +50,7 @@ export const AdvancedNode = ({ nodeSummary, metrics, ...props }) => { - + diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/node_detail_status/index.js b/x-pack/plugins/monitoring/public/components/elasticsearch/node_detail_status/index.js index 18533b3bd4b5e..85b4d0daddade 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/node_detail_status/index.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/node_detail_status/index.js @@ -9,8 +9,9 @@ import { SummaryStatus } from '../../summary_status'; import { NodeStatusIcon } from '../node'; import { formatMetric } from '../../../lib/format_number'; import { i18n } from '@kbn/i18n'; +import { AlertsStatus } from '../../../alerts/status'; -export function NodeDetailStatus({ stats, alerts }) { +export function NodeDetailStatus({ stats, alerts = {} }) { const { transport_address: transportAddress, usedHeap, @@ -29,8 +30,10 @@ export function NodeDetailStatus({ stats, alerts }) { const metrics = [ { - label: 'Alerts', - value: {Object.values(alerts).length}, + label: i18n.translate('xpack.monitoring.elasticsearch.nodeDetailStatus.alerts', { + defaultMessage: 'Alerts', + }), + value: , }, { label: i18n.translate('xpack.monitoring.elasticsearch.nodeDetailStatus.transportAddress', { diff --git a/x-pack/plugins/monitoring/public/directives/main/index.html b/x-pack/plugins/monitoring/public/directives/main/index.html index 39d357813b3f2..fabd207d72b1f 100644 --- a/x-pack/plugins/monitoring/public/directives/main/index.html +++ b/x-pack/plugins/monitoring/public/directives/main/index.html @@ -90,6 +90,7 @@ { + describe('Active Nodes', () => { + const { setup, tearDown } = getLifecycleMethods(getService, getPageObjects); + + before(async () => { + await setup('monitoring/singlecluster-three-nodes-shard-relocation', { + from: 'Oct 5, 2017 @ 20:31:48.354', + to: 'Oct 5, 2017 @ 20:35:12.176', + }); + + // go to nodes listing + await overview.clickEsNodes(); + expect(await nodesList.isOnListing()).to.be(true); + }); + + after(async () => { + await tearDown(); + }); + + afterEach(async () => { + await PageObjects.monitoring.clickBreadcrumb('~breadcrumbEsNodes'); // return back for next test + }); + + it('should show node summary of master node with 20 indices and 38 shards', async () => { + await nodesList.clickRowByResolver('jUT5KdxfRbORSCWkb5zjmA'); + await nodeDetail.clickAdvanced(); + + expect(await nodeDetail.getSummary()).to.eql({ + transportAddress: 'Transport Address\n127.0.0.1:9300', + jvmHeap: 'JVM Heap\n29%', + freeDiskSpace: 'Free Disk Space\n173.9 GB (37.42%)', + documentCount: 'Documents\n24.8k', + dataSize: 'Data\n50.4 MB', + indicesCount: 'Indices\n20', + shardsCount: 'Shards\n38', + nodeType: 'Type\nMaster Node', + status: 'Status: Online', + }); + }); + + it('should show node summary of data node with 4 indices and 4 shards', async () => { + await nodesList.clickRowByResolver('bwQWH-7IQY-mFPpfoaoFXQ'); + await nodeDetail.clickAdvanced(); + + expect(await nodeDetail.getSummary()).to.eql({ + transportAddress: 'Transport Address\n127.0.0.1:9302', + jvmHeap: 'JVM Heap\n17%', + freeDiskSpace: 'Free Disk Space\n173.9 GB (37.42%)', + documentCount: 'Documents\n240', + dataSize: 'Data\n1.4 MB', + indicesCount: 'Indices\n4', + shardsCount: 'Shards\n4', + nodeType: 'Type\nNode', + status: 'Status: Online', + }); + }); + }); + }); }); } diff --git a/x-pack/test/functional/services/monitoring/elasticsearch_node_detail.js b/x-pack/test/functional/services/monitoring/elasticsearch_node_detail.js index 2cfa7628c0c4b..41b69403829f7 100644 --- a/x-pack/test/functional/services/monitoring/elasticsearch_node_detail.js +++ b/x-pack/test/functional/services/monitoring/elasticsearch_node_detail.js @@ -19,6 +19,10 @@ export function MonitoringElasticsearchNodeDetailProvider({ getService }) { const SUBJ_SUMMARY_STATUS = `${SUBJ_SUMMARY} > statusIcon`; return new (class ElasticsearchNodeDetail { + async clickAdvanced() { + return testSubjects.click('esNodeDetailAdvancedLink'); + } + async getSummary() { return { transportAddress: await testSubjects.getVisibleText(SUBJ_SUMMARY_TRANSPORT_ADDRESS), From 7519c1f8c33ffea28b9e8daacdbebe05080125f3 Mon Sep 17 00:00:00 2001 From: Lee Drengenberg Date: Fri, 17 Jul 2020 16:42:56 -0500 Subject: [PATCH 08/45] use WORKSPACE env var for stack_functional_integration tests, fix navigate path (#71908) Co-authored-by: Elastic Machine --- .../configs/config.stack_functional_integration_base.js | 8 ++++++-- .../test/functional/apps/sample_data/e_commerce.js | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/x-pack/test/stack_functional_integration/configs/config.stack_functional_integration_base.js b/x-pack/test/stack_functional_integration/configs/config.stack_functional_integration_base.js index a34d158496ba0..96d338a04b01b 100644 --- a/x-pack/test/stack_functional_integration/configs/config.stack_functional_integration_base.js +++ b/x-pack/test/stack_functional_integration/configs/config.stack_functional_integration_base.js @@ -12,12 +12,16 @@ import { esTestConfig, kbnTestConfig } from '@kbn/test'; const reportName = 'Stack Functional Integration Tests'; const testsFolder = '../test/functional/apps'; -const stateFilePath = '../../../../../integration-test/qa/envvars.sh'; -const prepend = (testFile) => require.resolve(`${testsFolder}/${testFile}`); const log = new ToolingLog({ level: 'info', writeTo: process.stdout, }); +log.info(`WORKSPACE in config file ${process.env.WORKSPACE}`); +const stateFilePath = process.env.WORKSPACE + ? `${process.env.WORKSPACE}/qa/envvars.sh` + : '../../../../../integration-test/qa/envvars.sh'; + +const prepend = (testFile) => require.resolve(`${testsFolder}/${testFile}`); export default async ({ readConfigFile }) => { const defaultConfigs = await readConfigFile(require.resolve('../../functional/config')); diff --git a/x-pack/test/stack_functional_integration/test/functional/apps/sample_data/e_commerce.js b/x-pack/test/stack_functional_integration/test/functional/apps/sample_data/e_commerce.js index 306f30133f6ee..0286f6984e89e 100644 --- a/x-pack/test/stack_functional_integration/test/functional/apps/sample_data/e_commerce.js +++ b/x-pack/test/stack_functional_integration/test/functional/apps/sample_data/e_commerce.js @@ -12,7 +12,7 @@ export default function ({ getService, getPageObjects }) { before(async () => { await browser.setWindowSize(1200, 800); - await PageObjects.common.navigateToUrl('home', '/home/tutorial_directory/sampleData', { + await PageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', { useActualUrl: true, insertTimestamp: false, }); From 7aa600bff7250655dbbb80b026c67eed46f2969c Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Fri, 17 Jul 2020 14:53:04 -0700 Subject: [PATCH 09/45] [DOCS] Removes occurrences of X-Pack Security and Reporting (#72302) --- docs/dev-tools/grokdebugger/index.asciidoc | 2 +- docs/settings/monitoring-settings.asciidoc | 2 +- docs/setup/install.asciidoc | 4 ++-- .../monitoring/monitoring-kibana.asciidoc | 15 +++++++------ docs/user/reporting/chromium-sandbox.asciidoc | 12 +++++----- .../reporting/configuring-reporting.asciidoc | 10 ++++----- .../user/reporting/development/index.asciidoc | 10 +++++---- docs/user/reporting/gs-index.asciidoc | 2 +- docs/user/reporting/index.asciidoc | 2 +- docs/user/reporting/script-example.asciidoc | 3 ++- docs/user/reporting/watch-example.asciidoc | 2 +- docs/user/security/reporting.asciidoc | 16 +++++++------- docs/user/security/securing-kibana.asciidoc | 22 +++++++++---------- 13 files changed, 54 insertions(+), 48 deletions(-) diff --git a/docs/dev-tools/grokdebugger/index.asciidoc b/docs/dev-tools/grokdebugger/index.asciidoc index 5162e806edd07..994836de7a1a2 100644 --- a/docs/dev-tools/grokdebugger/index.asciidoc +++ b/docs/dev-tools/grokdebugger/index.asciidoc @@ -32,7 +32,7 @@ in ingest node and Logstash. This example walks you through using the *Grok Debugger*. This tool is automatically enabled in {kib}. -NOTE: If you're using {security}, you must have the `manage_pipeline` +NOTE: If you're using {stack-security-features}, you must have the `manage_pipeline` permission to use the Grok Debugger. . Open the menu, go to *Dev Tools*, then click *Grok Debugger*. diff --git a/docs/settings/monitoring-settings.asciidoc b/docs/settings/monitoring-settings.asciidoc index 48b5b5eb5d0c0..5b8fa0725d96b 100644 --- a/docs/settings/monitoring-settings.asciidoc +++ b/docs/settings/monitoring-settings.asciidoc @@ -7,7 +7,7 @@ By default, the Monitoring application is enabled, but data collection is disabled. When you first start {kib} monitoring, you are prompted to -enable data collection. If you are using {security}, you must be +enable data collection. If you are using {stack-security-features}, you must be signed in as a user with the `cluster:manage` privilege to enable data collection. The built-in `superuser` role has this privilege and the built-in `elastic` user has this role. diff --git a/docs/setup/install.asciidoc b/docs/setup/install.asciidoc index 73036da8f1390..cb47210cb3f08 100644 --- a/docs/setup/install.asciidoc +++ b/docs/setup/install.asciidoc @@ -53,8 +53,8 @@ Formulae are available from the Elastic Homebrew tap for installing {kib} on mac <> IMPORTANT: If your Elasticsearch installation is protected by -{ref}/elasticsearch-security.html[{security}] see -{kibana-ref}/using-kibana-with-security.html[Configuring security in Kibana] for +{ref}/elasticsearch-security.html[{stack-security-features}] see +{kibana-ref}/using-kibana-with-security.html[Configuring security in {kib}] for additional setup instructions. include::install/targz.asciidoc[] diff --git a/docs/user/monitoring/monitoring-kibana.asciidoc b/docs/user/monitoring/monitoring-kibana.asciidoc index b9ec3982eb3c5..bb8b3e5d42851 100644 --- a/docs/user/monitoring/monitoring-kibana.asciidoc +++ b/docs/user/monitoring/monitoring-kibana.asciidoc @@ -20,9 +20,10 @@ node in the production cluster. By default, it is is disabled (`false`). + -- NOTE: You can specify this setting in either the `elasticsearch.yml` on each -node or across the cluster as a dynamic cluster setting. If {es} -{security-features} are enabled, you must have `monitor` cluster privileges to -view the cluster settings and `manage` cluster privileges to change them. +node or across the cluster as a dynamic cluster setting. If +{stack-security-features} are enabled, you must have `monitor` cluster +privileges to view the cluster settings and `manage` cluster privileges to +change them. -- @@ -33,7 +34,7 @@ view the cluster settings and `manage` cluster privileges to change them. -- By default, if you are running {kib} locally, go to `http://localhost:5601/`. -If {es} {security-features} are enabled, log in. +If {security-features} are enabled, log in. -- ... Open the menu, then go to *Stack Monitoring*. If data collection is @@ -80,13 +81,13 @@ monitoring cluster prevents production cluster outages from impacting your ability to access your monitoring data. It also prevents monitoring activities from impacting the performance of your production cluster. -If {security} is enabled on the production cluster, use an HTTPS URL such -as `https://:9200` in this setting. +If {security-features} are enabled on the production cluster, use an HTTPS +URL such as `https://:9200` in this setting. =============================== -- -. If the Elastic {security-features} are enabled on the production cluster: +. If {security-features} are enabled on the production cluster: .. Verify that there is a valid user ID and password in the `elasticsearch.username` and diff --git a/docs/user/reporting/chromium-sandbox.asciidoc b/docs/user/reporting/chromium-sandbox.asciidoc index bfef5b8b86c6b..dcb421261c067 100644 --- a/docs/user/reporting/chromium-sandbox.asciidoc +++ b/docs/user/reporting/chromium-sandbox.asciidoc @@ -2,14 +2,16 @@ [[reporting-chromium-sandbox]] === Chromium sandbox -When {reporting} uses the Chromium browser for generating PDF reports, it's recommended to use the sandbox for -an additional layer of security. The Chromium sandbox uses operating system provided mechanisms to ensure that -code execution cannot make persistent changes to the computer or access confidential information. The specific -sandboxing techniques differ for each operating system. +When {report-features} uses the Chromium browser for generating PDF reports, +it's recommended to use the sandbox for an additional layer of security. The +Chromium sandbox uses operating system provided mechanisms to ensure that +code execution cannot make persistent changes to the computer or access +confidential information. The specific sandboxing techniques differ for each +operating system. ==== Linux sandbox The Linux sandbox depends on user namespaces, which were introduced with the 3.8 Linux kernel. However, many -distributions don't have user namespaces enabled by default, or they require the CAP_SYS_ADMIN capability. {reporting} +distributions don't have user namespaces enabled by default, or they require the CAP_SYS_ADMIN capability. The {report-features} will automatically disable the sandbox when it is running on Debian and CentOS as additional steps are required to enable unprivileged usernamespaces. In these situations, you'll see the following message in your {kib} startup logs: `Chromium sandbox provides an additional layer of protection, but is not supported for your OS. diff --git a/docs/user/reporting/configuring-reporting.asciidoc b/docs/user/reporting/configuring-reporting.asciidoc index 7489e2cf51f61..ca2d79bb2dec0 100644 --- a/docs/user/reporting/configuring-reporting.asciidoc +++ b/docs/user/reporting/configuring-reporting.asciidoc @@ -2,8 +2,8 @@ [[configuring-reporting]] == Reporting configuration -You can configure settings in `kibana.yml` to control how {reporting} -communicates with the {kib} server, manages background jobs, and captures +You can configure settings in `kibana.yml` to control how the {report-features} +communicate with the {kib} server, manages background jobs, and captures screenshots. See <> for the complete list of settings. @@ -11,9 +11,9 @@ list of settings. [[encryption-keys]] === Encryption keys for multiple {kib} instances -By default, a new encryption key is generated for {reporting} each time -you start {kib}. This means if a static encryption key is not persisted in the -{kib} configuration, any pending reports will fail when you restart {kib}. +By default, a new encryption key is generated for the {report-features} each +time you start {kib}. This means if a static encryption key is not persisted in +the {kib} configuration, any pending reports will fail when you restart {kib}. If you are load balancing across multiple {kib} instances, they need to have the same reporting encryption key. Otherwise, report generation will fail if a diff --git a/docs/user/reporting/development/index.asciidoc b/docs/user/reporting/development/index.asciidoc index a64e540da0c70..4e86c803bd82d 100644 --- a/docs/user/reporting/development/index.asciidoc +++ b/docs/user/reporting/development/index.asciidoc @@ -1,9 +1,11 @@ [role="xpack"] [[reporting-integration]] == Reporting integration -Integrating a {kib} application with {reporting} requires a minimum amount of code, and the goal is to not have to -modify the Reporting code as we add additional applications. Instead, applications abide by a contract that Reporting -uses to determine the information that is required to export CSVs and PDFs. +Integrating a {kib} application with the {report-features} requires a minimum +amount of code, and the goal is to not have to modify the reporting code as we +add additional applications. Instead, applications abide by a contract that +{report-features} use to determine the information that is required to export +CSVs and PDFs. [IMPORTANT] ============================================== @@ -18,7 +20,7 @@ X-Pack uses the `share` plugin of the Kibana platform to register actions in the [float] === Generate job URL -To generate a new {reporting} job, different export types require different `jobParams` that are Rison encoded into a URL +To generate a new reporting job, different export types require different `jobParams` that are Rison encoded into a URL that abide by the following convention: `/api/reporting/generate?jobParams=${rison.encode(jobParams)}`. If you use the aforementioned <> then this detail will be abstracted away, but if you provide a custom UI for generating the report, you will have to generate the URL and create a POST request to the URL. diff --git a/docs/user/reporting/gs-index.asciidoc b/docs/user/reporting/gs-index.asciidoc index 87918ee76340e..46c1fd38b7d69 100644 --- a/docs/user/reporting/gs-index.asciidoc +++ b/docs/user/reporting/gs-index.asciidoc @@ -21,7 +21,7 @@ You can also <>. IMPORTANT: Reports are stored in the `.reporting-*` indices. Any user with access to these indices has access to every report generated by all users. -To use {reporting} in a production environment, +To use {report-features} in a production environment, <>. -- diff --git a/docs/user/reporting/index.asciidoc b/docs/user/reporting/index.asciidoc index 6acdbbe3f0a99..e4e4b461ac2bd 100644 --- a/docs/user/reporting/index.asciidoc +++ b/docs/user/reporting/index.asciidoc @@ -19,7 +19,7 @@ image::user/reporting/images/share-button.png["Share"] [float] == Setup -{reporting} is automatically enabled in {kib}. It runs a custom build of the Chromium web browser, which +The {report-features} are automatically enabled in {kib}. It runs a custom build of the Chromium web browser, which runs on the server in headless mode to load {kib} and capture the rendered {kib} charts as images. Chromium is an open-source project not related to Elastic, but the Chromium binary for {kib} has been custom-built by Elastic to ensure it diff --git a/docs/user/reporting/script-example.asciidoc b/docs/user/reporting/script-example.asciidoc index 88f48ad1d3182..94301fc6fb448 100644 --- a/docs/user/reporting/script-example.asciidoc +++ b/docs/user/reporting/script-example.asciidoc @@ -19,7 +19,8 @@ curl \ // CONSOLE <1> `POST` method is required. -<2> Provide user credentials for a user with permission to access Kibana and X-Pack reporting. +<2> Provide user credentials for a user with permission to access Kibana and +{report-features}. <3> The `kbn-version` header is required for all `POST` requests to Kibana. **The value must match the dotted-numeral version of the Kibana instance.** <4> The POST URL. You can copy and paste the URL for any report from the Kibana UI. diff --git a/docs/user/reporting/watch-example.asciidoc b/docs/user/reporting/watch-example.asciidoc index 627e31017230c..253722fefecc0 100644 --- a/docs/user/reporting/watch-example.asciidoc +++ b/docs/user/reporting/watch-example.asciidoc @@ -52,7 +52,7 @@ report from the Kibana UI. <3> Optional, default is 40 <4> Optional, default is 15s <5> Provide user credentials for a user with permission to access Kibana and -{reporting}. +the {report-features}. //For more information, see <>. //<>. diff --git a/docs/user/security/reporting.asciidoc b/docs/user/security/reporting.asciidoc index 30340e1db989a..4e02759ce99cb 100644 --- a/docs/user/security/reporting.asciidoc +++ b/docs/user/security/reporting.asciidoc @@ -5,8 +5,8 @@ Reporting operates by creating and updating documents in {es} in response to user actions in {kib}. -To use {reporting} with {security} enabled, you need to -<>. +To use {report-features} with {security-features} enabled, you need to +<>. If you are automatically generating reports with {ref}/xpack-alerting.html[{watcher}], you also need to configure {watcher} to trust the {kib} server's certificate. @@ -118,10 +118,10 @@ reporting_user: === Secure the reporting endpoints In a production environment, you should restrict access to -the {reporting} endpoints to authorized users. This requires that you: +the reporting endpoints to authorized users. This requires that you: -. Enable {security} on your {es} cluster. For more information, -see {ref}/security-getting-started.html[Getting Started with Security]. +. Enable {stack-security-features} on your {es} cluster. For more information, +see {ref}/security-getting-started.html[Getting started with security]. . Configure TLS/SSL encryption for the {kib} server. For more information, see <>. . Specify the {kib} server's CA certificate chain in `elasticsearch.yml`: @@ -150,13 +150,13 @@ For more information, see {ref}/notification-settings.html#ssl-notification-sett -- . Add one or more users who have the permissions -necessary to use {kib} and {reporting}. For more information, see +necessary to use {kib} and {report-features}. For more information, see <>. -Once you've enabled SSL for {kib}, all requests to the {reporting} endpoints +Once you've enabled SSL for {kib}, all requests to the reporting endpoints must include valid credentials. For example, see the following page which includes a watch that submits requests as the built-in `elastic` user: <>. For more information about configuring watches, see -{ref}/how-watcher-works.html[How Watcher works]. +{ref}/how-watcher-works.html[How {watcher} works]. diff --git a/docs/user/security/securing-kibana.asciidoc b/docs/user/security/securing-kibana.asciidoc index b30acd0ed2e53..0177ac94bd402 100644 --- a/docs/user/security/securing-kibana.asciidoc +++ b/docs/user/security/securing-kibana.asciidoc @@ -5,21 +5,21 @@ Configure security ++++ -{kib} users have to log in when {security} is enabled on your cluster. You -configure {security} roles for your {kib} users to control what data those users -can access. +{kib} users have to log in when {stack-security-features} are enabled on your +cluster. You configure roles for your {kib} users to control what data those +users can access. Most requests made through {kib} to {es} are authenticated by using the credentials of the logged-in user. There are, however, a few internal requests that the {kib} server needs to make to the {es} cluster. For this reason, you must configure credentials for the {kib} server to use for those requests. -With {security} enabled, if you load a {kib} dashboard that accesses data in an -index that you are not authorized to view, you get an error that indicates the -index does not exist. {security} do not currently provide a way to control which -users can load which dashboards. +With {security-features} enabled, if you load a {kib} dashboard that accesses +data in an index that you are not authorized to view, you get an error that +indicates the index does not exist. The {security-features} do not currently +provide a way to control which users can load which dashboards. -To use {kib} with {security}: +To use {kib} with {security-features}: . {ref}/configuring-security.html[Configure security in {es}]. @@ -38,8 +38,8 @@ elasticsearch.password: "kibanapassword" The {kib} server submits requests as this user to access the cluster monitoring APIs and the `.kibana` index. The server does _not_ need access to user indices. -The password for the built-in `kibana_system` user is typically set as part of the -{security} configuration process on {es}. For more information, see +The password for the built-in `kibana_system` user is typically set as part of +the security configuration process on {es}. For more information, see {ref}/built-in-users.html[Built-in users]. -- @@ -53,7 +53,7 @@ as the encryption key. xpack.security.encryptionKey: "something_at_least_32_characters" -------------------------------------------------------------------------------- -For more information, see <>. +For more information, see <>. -- . Optional: Set a timeout to expire idle sessions. By default, a session stays From dc7db09533f62d43a70e2a903c89e19b81ae8287 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Fri, 17 Jul 2020 16:29:23 -0600 Subject: [PATCH 10/45] [Maps] convert SavedGisMap to TS (#72286) * [Maps] convert SavedGisMap to TS * i18n translate new map title --- .../{saved_gis_map.js => saved_gis_map.ts} | 65 ++++++++++++------- .../maps/public/selectors/map_selectors.ts | 4 +- 2 files changed, 42 insertions(+), 27 deletions(-) rename x-pack/plugins/maps/public/routing/bootstrap/services/{saved_gis_map.js => saved_gis_map.ts} (64%) diff --git a/x-pack/plugins/maps/public/routing/bootstrap/services/saved_gis_map.js b/x-pack/plugins/maps/public/routing/bootstrap/services/saved_gis_map.ts similarity index 64% rename from x-pack/plugins/maps/public/routing/bootstrap/services/saved_gis_map.js rename to x-pack/plugins/maps/public/routing/bootstrap/services/saved_gis_map.ts index f8c783f673bab..4b474424bcdab 100644 --- a/x-pack/plugins/maps/public/routing/bootstrap/services/saved_gis_map.js +++ b/x-pack/plugins/maps/public/routing/bootstrap/services/saved_gis_map.ts @@ -5,7 +5,13 @@ */ import _ from 'lodash'; -import { createSavedObjectClass } from '../../../../../../../src/plugins/saved_objects/public'; +import { SavedObjectReference } from 'kibana/public'; +import { i18n } from '@kbn/i18n'; +import { + createSavedObjectClass, + SavedObject, + SavedObjectKibanaServices, +} from '../../../../../../../src/plugins/saved_objects/public'; import { getTimeFilters, getMapZoom, @@ -18,65 +24,74 @@ import { } from '../../../selectors/map_selectors'; import { getIsLayerTOCOpen, getOpenTOCDetails } from '../../../selectors/ui_selectors'; import { copyPersistentState } from '../../../reducers/util'; +// @ts-expect-error import { extractReferences, injectReferences } from '../../../../common/migrations/references'; import { getExistingMapPath, MAP_SAVED_OBJECT_TYPE } from '../../../../common/constants'; +// @ts-expect-error import { getStore } from '../../store_operations'; +import { MapStoreState } from '../../../reducers/store'; +import { LayerDescriptor } from '../../../../common/descriptor_types'; + +export interface ISavedGisMap extends SavedObject { + layerListJSON?: string; + mapStateJSON?: string; + uiStateJSON?: string; + getLayerList(): LayerDescriptor[]; + syncWithStore(): void; +} -export function createSavedGisMapClass(services) { +export function createSavedGisMapClass(services: SavedObjectKibanaServices) { const SavedObjectClass = createSavedObjectClass(services); - class SavedGisMap extends SavedObjectClass { - static type = MAP_SAVED_OBJECT_TYPE; + class SavedGisMap extends SavedObjectClass implements ISavedGisMap { + public static type = MAP_SAVED_OBJECT_TYPE; // Mappings are used to place object properties into saved object _source - static mapping = { + public static mapping = { title: 'text', description: 'text', mapStateJSON: 'text', layerListJSON: 'text', uiStateJSON: 'text', }; - static fieldOrder = ['title', 'description']; - static searchSource = false; + public static fieldOrder = ['title', 'description']; + public static searchSource = false; - constructor(id) { + public showInRecentlyAccessed = true; + public layerListJSON?: string; + public mapStateJSON?: string; + public uiStateJSON?: string; + + constructor(id: string) { super({ type: SavedGisMap.type, mapping: SavedGisMap.mapping, searchSource: SavedGisMap.searchSource, extractReferences, - injectReferences: (savedObject, references) => { + injectReferences: (savedObject: ISavedGisMap, references: SavedObjectReference[]) => { const { attributes } = injectReferences({ attributes: { layerListJSON: savedObject.layerListJSON }, references, }); savedObject.layerListJSON = attributes.layerListJSON; - - const indexPatternIds = references - .filter((reference) => { - return reference.type === 'index-pattern'; - }) - .map((reference) => { - return reference.id; - }); - savedObject.indexPatternIds = _.uniq(indexPatternIds); }, // if this is null/undefined then the SavedObject will be assigned the defaults - id: id, + id, // default values that will get assigned if the doc is new defaults: { - title: 'New Map', + title: i18n.translate('xpack.maps.newMapTitle', { + defaultMessage: 'New Map', + }), description: '', }, }); - this.showInRecentlyAccessed = true; - } - getFullPath() { - return getExistingMapPath(this.id); + this.getFullPath = () => { + return getExistingMapPath(this.id!); + }; } getLayerList() { @@ -84,7 +99,7 @@ export function createSavedGisMapClass(services) { } syncWithStore() { - const state = getStore().getState(); + const state: MapStoreState = getStore().getState(); const layerList = getLayerListRaw(state); const layerListConfigOnly = copyPersistentState(layerList); this.layerListJSON = JSON.stringify(layerListConfigOnly); diff --git a/x-pack/plugins/maps/public/selectors/map_selectors.ts b/x-pack/plugins/maps/public/selectors/map_selectors.ts index fe2cfec3c761c..e082398a02a9e 100644 --- a/x-pack/plugins/maps/public/selectors/map_selectors.ts +++ b/x-pack/plugins/maps/public/selectors/map_selectors.ts @@ -52,6 +52,7 @@ import { ISource } from '../classes/sources/source'; import { ITMSSource } from '../classes/sources/tms_source'; import { IVectorSource } from '../classes/sources/vector_source'; import { ILayer } from '../classes/layers/layer'; +import { ISavedGisMap } from '../routing/bootstrap/services/saved_gis_map'; function createLayerInstance( layerDescriptor: LayerDescriptor, @@ -419,12 +420,11 @@ export const areLayersLoaded = createSelector( export function hasUnsavedChanges( state: MapStoreState, - savedMap: unknown, + savedMap: ISavedGisMap, initialLayerListConfig: LayerDescriptor[] ) { const layerListConfigOnly = copyPersistentState(getLayerListRaw(state)); - // @ts-expect-error const savedLayerList = savedMap.getLayerList(); return !savedLayerList From ef875cf9fe3ff27f9014a7c5d7418c27acbbacef Mon Sep 17 00:00:00 2001 From: Spencer Date: Fri, 17 Jul 2020 16:41:52 -0700 Subject: [PATCH 11/45] [esArchiver] actually re-delete the .kibana index if we lose recreate race (#72354) Co-authored-by: spalger --- src/es_archiver/lib/indices/create_index_stream.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/es_archiver/lib/indices/create_index_stream.ts b/src/es_archiver/lib/indices/create_index_stream.ts index 5629f95c7c9c6..fa4c95dc73166 100644 --- a/src/es_archiver/lib/indices/create_index_stream.ts +++ b/src/es_archiver/lib/indices/create_index_stream.ts @@ -20,7 +20,6 @@ import { Transform, Readable } from 'stream'; import { inspect } from 'util'; -import { get, once } from 'lodash'; import { Client } from 'elasticsearch'; import { ToolingLog } from '@kbn/dev-utils'; @@ -54,7 +53,7 @@ export function createCreateIndexStream({ // If we're trying to import Kibana index docs, we need to ensure that // previous indices are removed so we're starting w/ a clean slate for // migrations. This only needs to be done once per archive load operation. - const deleteKibanaIndicesOnce = once(deleteKibanaIndices); + let kibanaIndexAlreadyDeleted = false; async function handleDoc(stream: Readable, record: DocRecord) { if (skipDocsFromIndices.has(record.value.index)) { @@ -70,8 +69,9 @@ export function createCreateIndexStream({ async function attemptToCreate(attemptNumber = 1) { try { - if (isKibana) { - await deleteKibanaIndicesOnce({ client, stats, log }); + if (isKibana && !kibanaIndexAlreadyDeleted) { + await deleteKibanaIndices({ client, stats, log }); + kibanaIndexAlreadyDeleted = true; } await client.indices.create({ @@ -90,6 +90,7 @@ export function createCreateIndexStream({ err?.body?.error?.reason?.includes('index exists with the same name as the alias') && attemptNumber < 3 ) { + kibanaIndexAlreadyDeleted = false; const aliasStr = inspect(aliases); log.info( `failed to create aliases [${aliasStr}] because ES indicated an index/alias already exists, trying again` @@ -98,10 +99,7 @@ export function createCreateIndexStream({ return; } - if ( - get(err, 'body.error.type') !== 'resource_already_exists_exception' || - attemptNumber >= 3 - ) { + if (err?.body?.error?.type !== 'resource_already_exists_exception' || attemptNumber >= 3) { throw err; } From 105e3a6c7ecc39442f10c8ec030f6297e0b56cd4 Mon Sep 17 00:00:00 2001 From: Dmitry Lemeshko Date: Sat, 18 Jul 2020 17:47:53 +0200 Subject: [PATCH 12/45] update chromedriver to 84 (#72228) Co-authored-by: Elastic Machine --- package.json | 4 ++-- yarn.lock | 32 ++++++++++++++++++++++++-------- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index a22871e314bae..ceb3ac4cca937 100644 --- a/package.json +++ b/package.json @@ -322,7 +322,7 @@ "@types/browserslist-useragent": "^3.0.0", "@types/chance": "^1.0.0", "@types/cheerio": "^0.22.10", - "@types/chromedriver": "^2.38.0", + "@types/chromedriver": "^81.0.0", "@types/classnames": "^2.2.9", "@types/color": "^3.0.0", "@types/d3": "^3.5.43", @@ -411,7 +411,7 @@ "chai": "3.5.0", "chance": "1.0.18", "cheerio": "0.22.0", - "chromedriver": "^83.0.0", + "chromedriver": "^84.0.0", "classnames": "2.2.6", "dedent": "^0.7.0", "delete-empty": "^2.0.0", diff --git a/yarn.lock b/yarn.lock index e5975efe0b7d5..3924655b5e43e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4784,10 +4784,10 @@ resolved "https://registry.yarnpkg.com/@types/chroma-js/-/chroma-js-2.0.0.tgz#b0fc98c8625d963f14e8138e0a7961103303ab22" integrity sha512-iomunXsXjDxhm2y1OeJt8NwmgC7RyNkPAOddlYVGsbGoX8+1jYt84SG4/tf6RWcwzROLx1kPXPE95by1s+ebIg== -"@types/chromedriver@^2.38.0": - version "2.38.0" - resolved "https://registry.yarnpkg.com/@types/chromedriver/-/chromedriver-2.38.0.tgz#971032b73eb7f44036f4f5bed59a7fd5b468014f" - integrity sha512-vcPGkZt1y2YVXKAY8SwCvU0u9mgw9+7tBV4HGb0YX/6bu1WXbb61bf8Y/N+xNCYwEj/Ug1UAMnhCcsSohXzRXw== +"@types/chromedriver@^81.0.0": + version "81.0.0" + resolved "https://registry.yarnpkg.com/@types/chromedriver/-/chromedriver-81.0.0.tgz#d7c97bd2b1de34270f44e60f4eee43bfdba3a8e2" + integrity sha512-Oqwo24DPn5lYI66aA74ApKrfAqVFEjC66raiB/2eHhhryYiumlMpRTR/++riaRcXmfrLXrIiNTtE+Op4vGCIFQ== dependencies: "@types/node" "*" @@ -6726,6 +6726,13 @@ agent-base@^4.1.0: dependencies: es6-promisify "^5.0.0" +agent-base@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee" + integrity sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg== + dependencies: + es6-promisify "^5.0.0" + agentkeepalive@^3.4.1: version "3.4.1" resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-3.4.1.tgz#aa95aebc3a749bca5ed53e3880a09f5235b48f0c" @@ -9970,15 +9977,16 @@ chrome-trace-event@^1.0.2: dependencies: tslib "^1.9.0" -chromedriver@^83.0.0: - version "83.0.0" - resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-83.0.0.tgz#75d7d838e58014658c3990089464166fef951926" - integrity sha512-AePp9ykma+z4aKPRqlbzvVlc22VsQ6+rgF+0aL3B5onHOncK18dWSkLrSSJMczP/mXILN9ohGsvpuTwoRSj6OQ== +chromedriver@^84.0.0: + version "84.0.0" + resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-84.0.0.tgz#980d72bf0990bbfbce282074d15448296c55d89d" + integrity sha512-fNX9eT1C38D1W8r5ss9ty42eDK+GIkCZVKukfeDs0XSBeKfyT0o/vbMdPr9MUkWQ+vIcFAS5hFGp9E3+xoaMeQ== dependencies: "@testim/chrome-version" "^1.0.7" axios "^0.19.2" del "^5.1.0" extract-zip "^2.0.0" + https-proxy-agent "^2.2.4" mkdirp "^1.0.4" tcp-port-used "^1.0.1" @@ -17448,6 +17456,14 @@ https-proxy-agent@2.2.1, https-proxy-agent@^2.2.1: agent-base "^4.1.0" debug "^3.1.0" +https-proxy-agent@^2.2.4: + version "2.2.4" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz#4ee7a737abd92678a293d9b34a1af4d0d08c787b" + integrity sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg== + dependencies: + agent-base "^4.3.0" + debug "^3.1.0" + https-proxy-agent@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz#702b71fb5520a132a66de1f67541d9e62154d82b" From a28463d82a38bac9b04470c8836c14536a528220 Mon Sep 17 00:00:00 2001 From: Marco Liberati Date: Mon, 20 Jul 2020 10:22:05 +0200 Subject: [PATCH 13/45] Fix float percentiles line chart (#71902) Co-authored-by: Elastic Machine --- .../public/vislib/lib/types/point_series.js | 18 +- .../vislib/lib/types/point_series.test.js | 20 +- .../types/testdata_linechart_percentile.json | 608 +++++++++--------- ...data_linechart_percentile_float_value.json | 463 +++++++++++++ ...nechart_percentile_float_value_result.json | 456 +++++++++++++ .../testdata_linechart_percentile_result.json | 333 +++++++++- 6 files changed, 1586 insertions(+), 312 deletions(-) create mode 100644 src/plugins/vis_type_vislib/public/vislib/lib/types/testdata_linechart_percentile_float_value.json create mode 100644 src/plugins/vis_type_vislib/public/vislib/lib/types/testdata_linechart_percentile_float_value_result.json diff --git a/src/plugins/vis_type_vislib/public/vislib/lib/types/point_series.js b/src/plugins/vis_type_vislib/public/vislib/lib/types/point_series.js index 438c071d74532..03b5af2572d94 100644 --- a/src/plugins/vis_type_vislib/public/vislib/lib/types/point_series.js +++ b/src/plugins/vis_type_vislib/public/vislib/lib/types/point_series.js @@ -21,7 +21,23 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; function getSeriId(seri) { - return seri.id && seri.id.indexOf('.') !== -1 ? seri.id.split('.')[0] : seri.id; + if (!seri.id) { + return; + } + // Ideally the format should be either ID or "ID.SERIES" + // but for some values the SERIES components gets a bit more complex + + // Float values are serialized as strings tuples (i.e. ['99.1']) rather than regular numbers (99.1) + // so the complete ids are in the format ID.['SERIES']: hence the specific brackets handler + const bracketsMarker = seri.id.indexOf('['); + if (bracketsMarker > -1) { + return seri.id.substring(0, bracketsMarker); + } + // Here's the dot check is enough + if (seri.id.indexOf('.') > -1) { + return seri.id.split('.')[0]; + } + return seri.id; } const createSeriesFromParams = (cfg, seri) => { diff --git a/src/plugins/vis_type_vislib/public/vislib/lib/types/point_series.test.js b/src/plugins/vis_type_vislib/public/vislib/lib/types/point_series.test.js index 62ff873f28134..b46054f3cd983 100644 --- a/src/plugins/vis_type_vislib/public/vislib/lib/types/point_series.test.js +++ b/src/plugins/vis_type_vislib/public/vislib/lib/types/point_series.test.js @@ -20,6 +20,8 @@ import stackedSeries from '../../../fixtures/mock_data/date_histogram/_stacked_s import { vislibPointSeriesTypes } from './point_series'; import percentileTestdata from './testdata_linechart_percentile.json'; import percentileTestdataResult from './testdata_linechart_percentile_result.json'; +import percentileTestdataFloatValue from './testdata_linechart_percentile_float_value.json'; +import percentileTestdataFloatValueResult from './testdata_linechart_percentile_float_value_result.json'; const maxBucketData = { get: (prop) => { @@ -215,18 +217,26 @@ describe('Point Series Config Type Class Test Suite', function () { }); describe('line chart', function () { - beforeEach(function () { + function prepareData({ cfg, data }) { const percentileDataObj = { get: (prop) => { return maxBucketData[prop] || maxBucketData.data[prop] || null; }, getLabels: () => [], - data: percentileTestdata.data, + data: data, }; - parsedConfig = vislibPointSeriesTypes.line(percentileTestdata.cfg, percentileDataObj); - }); + const parsedConfig = vislibPointSeriesTypes.line(cfg, percentileDataObj); + return parsedConfig; + } + it('should render a percentile line chart', function () { - expect(JSON.stringify(parsedConfig)).toEqual(JSON.stringify(percentileTestdataResult)); + const parsedConfig = prepareData(percentileTestdata); + expect(parsedConfig).toMatchObject(percentileTestdataResult); + }); + + it('should render a percentile line chart when value is float', function () { + const parsedConfig = prepareData(percentileTestdataFloatValue); + expect(parsedConfig).toMatchObject(percentileTestdataFloatValueResult); }); }); }); diff --git a/src/plugins/vis_type_vislib/public/vislib/lib/types/testdata_linechart_percentile.json b/src/plugins/vis_type_vislib/public/vislib/lib/types/testdata_linechart_percentile.json index 818d9133938fa..d52cb18727c05 100644 --- a/src/plugins/vis_type_vislib/public/vislib/lib/types/testdata_linechart_percentile.json +++ b/src/plugins/vis_type_vislib/public/vislib/lib/types/testdata_linechart_percentile.json @@ -140,320 +140,320 @@ } }, "yAxisLabel": "", - "series": [ - { - "id": "1.1", - "rawId": "col-1-1.1", - "label": "1st percentile of AvgTicketPrice", - "values": [ - { - "x": 1557460800000, - "y": 116.33676605224609, - "extraMetrics": [], - "xRaw": { - "table": { - "columns": [ - { - "id": "col-0-2", - "name": "timestamp per day" - }, - { - "id": "col-1-1.1", - "name": "1st percentile of AvgTicketPrice" - }, - { - "id": "col-2-1.50", - "name": "50th percentile of AvgTicketPrice" - } - ], - "rows": [ - { - "col-0-2": 1557460800000, - "col-1-1.1": 116, - "col-2-1.50": 658 - }, - { - "col-0-2": 1557547200000, - "col-1-1.1": 223, - "col-2-1.50": 756 - } - ] - }, - "column": 0, - "row": 0, - "value": 1557460800000 + "hits": 2 + }, + "series": [ + { + "id": "1.1", + "rawId": "col-1-1.1", + "label": "1st percentile of AvgTicketPrice", + "values": [ + { + "x": 1557460800000, + "y": 116.33676605224609, + "extraMetrics": [], + "xRaw": { + "table": { + "columns": [ + { + "id": "col-0-2", + "name": "timestamp per day" + }, + { + "id": "col-1-1.1", + "name": "1st percentile of AvgTicketPrice" + }, + { + "id": "col-2-1.50", + "name": "50th percentile of AvgTicketPrice" + } + ], + "rows": [ + { + "col-0-2": 1557460800000, + "col-1-1.1": 116, + "col-2-1.50": 658 + }, + { + "col-0-2": 1557547200000, + "col-1-1.1": 223, + "col-2-1.50": 756 + } + ] }, - "yRaw": { - "table": { - "columns": [ - { - "id": "col-0-2", - "name": "timestamp per day" - }, - { - "id": "col-1-1.1", - "name": "1st percentile of AvgTicketPrice" - }, - { - "id": "col-2-1.50", - "name": "50th percentile of AvgTicketPrice" - } - ], - "rows": [ - { - "col-0-2": 1557460800000, - "col-1-1.1": 116, - "col-2-1.50": 658 - }, - { - "col-0-2": 1557547200000, - "col-1-1.1": 223, - "col-2-1.50": 756 - } - ] - }, - "column": 1, - "row": 0, - "value": 116.33676605224609 + "column": 0, + "row": 0, + "value": 1557460800000 + }, + "yRaw": { + "table": { + "columns": [ + { + "id": "col-0-2", + "name": "timestamp per day" + }, + { + "id": "col-1-1.1", + "name": "1st percentile of AvgTicketPrice" + }, + { + "id": "col-2-1.50", + "name": "50th percentile of AvgTicketPrice" + } + ], + "rows": [ + { + "col-0-2": 1557460800000, + "col-1-1.1": 116, + "col-2-1.50": 658 + }, + { + "col-0-2": 1557547200000, + "col-1-1.1": 223, + "col-2-1.50": 756 + } + ] }, - "parent": null, - "series": "1st percentile of AvgTicketPrice", - "seriesId": "col-1-1.1" + "column": 1, + "row": 0, + "value": 116.33676605224609 }, - { - "x": 1557547200000, - "y": 223, - "extraMetrics": [], - "xRaw": { - "table": { - "columns": [ - { - "id": "col-0-2", - "name": "timestamp per day" - }, - { - "id": "col-1-1.1", - "name": "1st percentile of AvgTicketPrice" - }, - { - "id": "col-2-1.50", - "name": "50th percentile of AvgTicketPrice" - } - ], - "rows": [ - { - "col-0-2": 1557460800000, - "col-1-1.1": 116, - "col-2-1.50": 658 - }, - { - "col-0-2": 1557547200000, - "col-1-1.1": 223, - "col-2-1.50": 756 - } - ] - }, - "column": 0, - "row": 1, - "value": 1557547200000 + "parent": null, + "series": "1st percentile of AvgTicketPrice", + "seriesId": "col-1-1.1" + }, + { + "x": 1557547200000, + "y": 223, + "extraMetrics": [], + "xRaw": { + "table": { + "columns": [ + { + "id": "col-0-2", + "name": "timestamp per day" + }, + { + "id": "col-1-1.1", + "name": "1st percentile of AvgTicketPrice" + }, + { + "id": "col-2-1.50", + "name": "50th percentile of AvgTicketPrice" + } + ], + "rows": [ + { + "col-0-2": 1557460800000, + "col-1-1.1": 116, + "col-2-1.50": 658 + }, + { + "col-0-2": 1557547200000, + "col-1-1.1": 223, + "col-2-1.50": 756 + } + ] }, - "yRaw": { - "table": { - "columns": [ - { - "id": "col-0-2", - "name": "timestamp per day" - }, - { - "id": "col-1-1.1", - "name": "1st percentile of AvgTicketPrice" - }, - { - "id": "col-2-1.50", - "name": "50th percentile of AvgTicketPrice" - } - ], - "rows": [ - { - "col-0-2": 1557460800000, - "col-1-1.1": 116, - "col-2-1.50": 658 - }, - { - "col-0-2": 1557547200000, - "col-1-1.1": 223, - "col-2-1.50": 756 - } - ] - }, - "column": 1, - "row": 1, - "value": 223 + "column": 0, + "row": 1, + "value": 1557547200000 + }, + "yRaw": { + "table": { + "columns": [ + { + "id": "col-0-2", + "name": "timestamp per day" + }, + { + "id": "col-1-1.1", + "name": "1st percentile of AvgTicketPrice" + }, + { + "id": "col-2-1.50", + "name": "50th percentile of AvgTicketPrice" + } + ], + "rows": [ + { + "col-0-2": 1557460800000, + "col-1-1.1": 116, + "col-2-1.50": 658 + }, + { + "col-0-2": 1557547200000, + "col-1-1.1": 223, + "col-2-1.50": 756 + } + ] }, - "parent": null, - "series": "1st percentile of AvgTicketPrice", - "seriesId": "col-1-1.1" - } - ] - }, - { - "id": "1.50", - "rawId": "col-2-1.50", - "label": "50th percentile of AvgTicketPrice", - "values": [ - { - "x": 1557460800000, - "y": 658.8453063964844, - "extraMetrics": [], - "xRaw": { - "table": { - "columns": [ - { - "id": "col-0-2", - "name": "timestamp per day" - }, - { - "id": "col-1-1.1", - "name": "1st percentile of AvgTicketPrice" - }, - { - "id": "col-2-1.50", - "name": "50th percentile of AvgTicketPrice" - } - ], - "rows": [ - { - "col-0-2": 1557460800000, - "col-1-1.1": 116, - "col-2-1.50": 658 - }, - { - "col-0-2": 1557547200000, - "col-1-1.1": 223, - "col-2-1.50": 756 - } - ] - }, - "column": 0, - "row": 0, - "value": 1557460800000 + "column": 1, + "row": 1, + "value": 223 + }, + "parent": null, + "series": "1st percentile of AvgTicketPrice", + "seriesId": "col-1-1.1" + } + ] + }, + { + "id": "1.50", + "rawId": "col-2-1.50", + "label": "50th percentile of AvgTicketPrice", + "values": [ + { + "x": 1557460800000, + "y": 658.8453063964844, + "extraMetrics": [], + "xRaw": { + "table": { + "columns": [ + { + "id": "col-0-2", + "name": "timestamp per day" + }, + { + "id": "col-1-1.1", + "name": "1st percentile of AvgTicketPrice" + }, + { + "id": "col-2-1.50", + "name": "50th percentile of AvgTicketPrice" + } + ], + "rows": [ + { + "col-0-2": 1557460800000, + "col-1-1.1": 116, + "col-2-1.50": 658 + }, + { + "col-0-2": 1557547200000, + "col-1-1.1": 223, + "col-2-1.50": 756 + } + ] }, - "yRaw": { - "table": { - "columns": [ - { - "id": "col-0-2", - "name": "timestamp per day" - }, - { - "id": "col-1-1.1", - "name": "1st percentile of AvgTicketPrice" - }, - { - "id": "col-2-1.50", - "name": "50th percentile of AvgTicketPrice" - } - ], - "rows": [ - { - "col-0-2": 1557460800000, - "col-1-1.1": 116, - "col-2-1.50": 658 - }, - { - "col-0-2": 1557547200000, - "col-1-1.1": 223, - "col-2-1.50": 756 - } - ] - }, - "column": 2, - "row": 0, - "value": 658 + "column": 0, + "row": 0, + "value": 1557460800000 + }, + "yRaw": { + "table": { + "columns": [ + { + "id": "col-0-2", + "name": "timestamp per day" + }, + { + "id": "col-1-1.1", + "name": "1st percentile of AvgTicketPrice" + }, + { + "id": "col-2-1.50", + "name": "50th percentile of AvgTicketPrice" + } + ], + "rows": [ + { + "col-0-2": 1557460800000, + "col-1-1.1": 116, + "col-2-1.50": 658 + }, + { + "col-0-2": 1557547200000, + "col-1-1.1": 223, + "col-2-1.50": 756 + } + ] }, - "parent": null, - "series": "50th percentile of AvgTicketPrice", - "seriesId": "col-2-1.50" + "column": 2, + "row": 0, + "value": 658 }, - { - "x": 1557547200000, - "y": 756, - "extraMetrics": [], - "xRaw": { - "table": { - "columns": [ - { - "id": "col-0-2", - "name": "timestamp per day" - }, - { - "id": "col-1-1.1", - "name": "1st percentile of AvgTicketPrice" - }, - { - "id": "col-2-1.50", - "name": "50th percentile of AvgTicketPrice" - } - ], - "rows": [ - { - "col-0-2": 1557460800000, - "col-1-1.1": 116, - "col-2-1.50": 658 - }, - { - "col-0-2": 1557547200000, - "col-1-1.1": 223, - "col-2-1.50": 756 - } - ] - }, - "column": 0, - "row": 1, - "value": 1557547200000 + "parent": null, + "series": "50th percentile of AvgTicketPrice", + "seriesId": "col-2-1.50" + }, + { + "x": 1557547200000, + "y": 756, + "extraMetrics": [], + "xRaw": { + "table": { + "columns": [ + { + "id": "col-0-2", + "name": "timestamp per day" + }, + { + "id": "col-1-1.1", + "name": "1st percentile of AvgTicketPrice" + }, + { + "id": "col-2-1.50", + "name": "50th percentile of AvgTicketPrice" + } + ], + "rows": [ + { + "col-0-2": 1557460800000, + "col-1-1.1": 116, + "col-2-1.50": 658 + }, + { + "col-0-2": 1557547200000, + "col-1-1.1": 223, + "col-2-1.50": 756 + } + ] }, - "yRaw": { - "table": { - "columns": [ - { - "id": "col-0-2", - "name": "timestamp per day" - }, - { - "id": "col-1-1.1", - "name": "1st percentile of AvgTicketPrice" - }, - { - "id": "col-2-1.50", - "name": "50th percentile of AvgTicketPrice" - } - ], - "rows": [ - { - "col-0-2": 1557460800000, - "col-1-1.1": 116, - "col-2-1.50": 658 - }, - { - "col-0-2": 1557547200000, - "col-1-1.1": 223, - "col-2-1.50": 756 - } - ] - }, - "column": 2, - "row": 1, - "value": 756.2283554077148 + "column": 0, + "row": 1, + "value": 1557547200000 + }, + "yRaw": { + "table": { + "columns": [ + { + "id": "col-0-2", + "name": "timestamp per day" + }, + { + "id": "col-1-1.1", + "name": "1st percentile of AvgTicketPrice" + }, + { + "id": "col-2-1.50", + "name": "50th percentile of AvgTicketPrice" + } + ], + "rows": [ + { + "col-0-2": 1557460800000, + "col-1-1.1": 116, + "col-2-1.50": 658 + }, + { + "col-0-2": 1557547200000, + "col-1-1.1": 223, + "col-2-1.50": 756 + } + ] }, - "parent": null, - "series": "50th percentile of AvgTicketPrice", - "seriesId": "col-2-1.50" - } - ] - } - ], - "hits": 2 - }, + "column": 2, + "row": 1, + "value": 756.2283554077148 + }, + "parent": null, + "series": "50th percentile of AvgTicketPrice", + "seriesId": "col-2-1.50" + } + ] + } + ], "type": "series", "labels": [ "1st percentile of AvgTicketPrice", diff --git a/src/plugins/vis_type_vislib/public/vislib/lib/types/testdata_linechart_percentile_float_value.json b/src/plugins/vis_type_vislib/public/vislib/lib/types/testdata_linechart_percentile_float_value.json new file mode 100644 index 0000000000000..6e1a707229974 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/vislib/lib/types/testdata_linechart_percentile_float_value.json @@ -0,0 +1,463 @@ +{ + "cfg": { + "addLegend": true, + "addTimeMarker": false, + "addTooltip": true, + "categoryAxes": [ + { + "id": "CategoryAxis-1", + "labels": { + "show": true, + "truncate": 100 + }, + "position": "bottom", + "scale": { + "type": "linear" + }, + "show": true, + "style": {}, + "title": {}, + "type": "category" + } + ], + "dimensions": { + "x": { + "accessor": 0, + "format": { + "id": "date", + "params": { + "pattern": "YYYY-MM-DD" + } + }, + "params": { + "date": true, + "interval": "P1D", + "format": "YYYY-MM-DD", + "bounds": { + "min": "2019-05-10T04:00:00.000Z", + "max": "2019-05-12T10:18:57.342Z" + } + }, + "aggType": "date_histogram" + }, + "y": [ + { + "accessor": 1, + "format": { + "id": "number", + "params": { + "pattern": "$0,0.[00]" + } + }, + "params": {}, + "aggType": "percentiles" + }, + { + "accessor": 2, + "format": { + "id": "number", + "params": { + "pattern": "$0,0.[00]" + } + }, + "params": {}, + "aggType": "percentiles" + } + ] + }, + "grid": { + "categoryLines": false, + "style": { + "color": "#eee" + } + }, + "legendPosition": "right", + "seriesParams": [ + { + "data": { + "id": "1", + "label": "Percentiles of AvgTicketPrice" + }, + "drawLinesBetweenPoints": true, + "interpolate": "cardinal", + "mode": "normal", + "show": "true", + "showCircles": true, + "type": "line", + "valueAxis": "ValueAxis-1" + } + ], + "times": [], + "type": "area", + "valueAxes": [ + { + "id": "ValueAxis-1", + "labels": { + "filter": false, + "rotate": 0, + "show": true, + "truncate": 100 + }, + "name": "LeftAxis-1", + "position": "left", + "scale": { + "mode": "normal", + "type": "linear" + }, + "show": true, + "style": {}, + "title": { + "text": "Percentiles of AvgTicketPrice" + }, + "type": "value" + } + ] + }, + "data": { + "uiState": {}, + "data": { + "xAxisOrderedValues": [ + 1557460800000, + 1557547200000 + ], + "xAxisFormat": { + "id": "date", + "params": { + "pattern": "YYYY-MM-DD" + } + }, + "xAxisLabel": "timestamp per day", + "ordered": { + "interval": "P1D", + "date": true, + "min": 1557460800000, + "max": 1557656337342 + }, + "yAxisFormat": { + "id": "number", + "params": { + "pattern": "$0,0.[00]" + } + }, + "yAxisLabel": "", + "hits": 2 + }, + "series": [ + { + "id": "1.['1.1']", + "rawId": "col-1-1.['1.1']", + "label": "1.1th percentile of AvgTicketPrice", + "values": [ + { + "x": 1557460800000, + "y": 116.33676605224609, + "extraMetrics": [], + "xRaw": { + "table": { + "columns": [ + { + "id": "col-0-2", + "name": "timestamp per day" + }, + { + "id": "col-1-1.['1.1']", + "name": "1.1th percentile of AvgTicketPrice" + }, + { + "id": "col-2-1.50", + "name": "50th percentile of AvgTicketPrice" + } + ], + "rows": [ + { + "col-0-2": 1557460800000, + "col-1-1.['1.1']": 116, + "col-2-1.50": 658 + }, + { + "col-0-2": 1557547200000, + "col-1-1.['1.1']": 223, + "col-2-1.50": 756 + } + ] + }, + "column": 0, + "row": 0, + "value": 1557460800000 + }, + "yRaw": { + "table": { + "columns": [ + { + "id": "col-0-2", + "name": "timestamp per day" + }, + { + "id": "col-1-1.['1.1']", + "name": "1.1th percentile of AvgTicketPrice" + }, + { + "id": "col-2-1.50", + "name": "50th percentile of AvgTicketPrice" + } + ], + "rows": [ + { + "col-0-2": 1557460800000, + "col-1-1.['1.1']": 116, + "col-2-1.50": 658 + }, + { + "col-0-2": 1557547200000, + "col-1-1.['1.1']": 223, + "col-2-1.50": 756 + } + ] + }, + "column": 1, + "row": 0, + "value": 116.33676605224609 + }, + "parent": null, + "series": "1.1th percentile of AvgTicketPrice", + "seriesId": "col-1-1.['1.1']" + }, + { + "x": 1557547200000, + "y": 223, + "extraMetrics": [], + "xRaw": { + "table": { + "columns": [ + { + "id": "col-0-2", + "name": "timestamp per day" + }, + { + "id": "col-1-1.['1.1']", + "name": "1.1th percentile of AvgTicketPrice" + }, + { + "id": "col-2-1.50", + "name": "50th percentile of AvgTicketPrice" + } + ], + "rows": [ + { + "col-0-2": 1557460800000, + "col-1-1.['1.1']": 116, + "col-2-1.50": 658 + }, + { + "col-0-2": 1557547200000, + "col-1-1.['1.1']": 223, + "col-2-1.50": 756 + } + ] + }, + "column": 0, + "row": 1, + "value": 1557547200000 + }, + "yRaw": { + "table": { + "columns": [ + { + "id": "col-0-2", + "name": "timestamp per day" + }, + { + "id": "col-1-1.['1.1']", + "name": "1.1th percentile of AvgTicketPrice" + }, + { + "id": "col-2-1.50", + "name": "50th percentile of AvgTicketPrice" + } + ], + "rows": [ + { + "col-0-2": 1557460800000, + "col-1-1.['1.1']": 116, + "col-2-1.50": 658 + }, + { + "col-0-2": 1557547200000, + "col-1-1.['1.1']": 223, + "col-2-1.50": 756 + } + ] + }, + "column": 1, + "row": 1, + "value": 223 + }, + "parent": null, + "series": "1.1th percentile of AvgTicketPrice", + "seriesId": "col-1-1.['1.1']" + } + ] + }, + { + "id": "1.50", + "rawId": "col-2-1.50", + "label": "50th percentile of AvgTicketPrice", + "values": [ + { + "x": 1557460800000, + "y": 658.8453063964844, + "extraMetrics": [], + "xRaw": { + "table": { + "columns": [ + { + "id": "col-0-2", + "name": "timestamp per day" + }, + { + "id": "col-1-1.['1.1']", + "name": "1.1th percentile of AvgTicketPrice" + }, + { + "id": "col-2-1.50", + "name": "50th percentile of AvgTicketPrice" + } + ], + "rows": [ + { + "col-0-2": 1557460800000, + "col-1-1.['1.1']": 116, + "col-2-1.50": 658 + }, + { + "col-0-2": 1557547200000, + "col-1-1.['1.1']": 223, + "col-2-1.50": 756 + } + ] + }, + "column": 0, + "row": 0, + "value": 1557460800000 + }, + "yRaw": { + "table": { + "columns": [ + { + "id": "col-0-2", + "name": "timestamp per day" + }, + { + "id": "col-1-1.['1.1']", + "name": "1.1th percentile of AvgTicketPrice" + }, + { + "id": "col-2-1.50", + "name": "50th percentile of AvgTicketPrice" + } + ], + "rows": [ + { + "col-0-2": 1557460800000, + "col-1-1.['1.1']": 116, + "col-2-1.50": 658 + }, + { + "col-0-2": 1557547200000, + "col-1-1.['1.1']": 223, + "col-2-1.50": 756 + } + ] + }, + "column": 2, + "row": 0, + "value": 658 + }, + "parent": null, + "series": "50th percentile of AvgTicketPrice", + "seriesId": "col-2-1.50" + }, + { + "x": 1557547200000, + "y": 756, + "extraMetrics": [], + "xRaw": { + "table": { + "columns": [ + { + "id": "col-0-2", + "name": "timestamp per day" + }, + { + "id": "col-1-1.['1.1']", + "name": "1.1th percentile of AvgTicketPrice" + }, + { + "id": "col-2-1.50", + "name": "50th percentile of AvgTicketPrice" + } + ], + "rows": [ + { + "col-0-2": 1557460800000, + "col-1-1.['1.1']": 116, + "col-2-1.50": 658 + }, + { + "col-0-2": 1557547200000, + "col-1-1.['1.1']": 223, + "col-2-1.50": 756 + } + ] + }, + "column": 0, + "row": 1, + "value": 1557547200000 + }, + "yRaw": { + "table": { + "columns": [ + { + "id": "col-0-2", + "name": "timestamp per day" + }, + { + "id": "col-1-1.['1.1']", + "name": "1.1th percentile of AvgTicketPrice" + }, + { + "id": "col-2-1.50", + "name": "50th percentile of AvgTicketPrice" + } + ], + "rows": [ + { + "col-0-2": 1557460800000, + "col-1-1.['1.1']": 116, + "col-2-1.50": 658 + }, + { + "col-0-2": 1557547200000, + "col-1-1.['1.1']": 223, + "col-2-1.50": 756 + } + ] + }, + "column": 2, + "row": 1, + "value": 756.2283554077148 + }, + "parent": null, + "series": "50th percentile of AvgTicketPrice", + "seriesId": "col-2-1.50" + } + ] + } + ], + "type": "series", + "labels": [ + "1.1th percentile of AvgTicketPrice", + "50th percentile of AvgTicketPrice" + ] + } +} \ No newline at end of file diff --git a/src/plugins/vis_type_vislib/public/vislib/lib/types/testdata_linechart_percentile_float_value_result.json b/src/plugins/vis_type_vislib/public/vislib/lib/types/testdata_linechart_percentile_float_value_result.json new file mode 100644 index 0000000000000..f7dd18f5eb712 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/vislib/lib/types/testdata_linechart_percentile_float_value_result.json @@ -0,0 +1,456 @@ +{ + "addLegend": true, + "addTimeMarker": false, + "addTooltip": true, + "categoryAxes": [ + { + "id": "CategoryAxis-1", + "labels": { + "show": true, + "truncate": 100 + }, + "position": "bottom", + "scale": { + "type": "linear" + }, + "show": true, + "style": {}, + "title": { + "text": "Date Histogram" + }, + "type": "category" + } + ], + "dimensions": { + "x": { + "accessor": 0, + "format": { + "id": "date", + "params": { + "pattern": "YYYY-MM-DD" + } + }, + "params": { + "date": true, + "interval": "P1D", + "format": "YYYY-MM-DD", + "bounds": { + "min": "2019-05-10T04:00:00.000Z", + "max": "2019-05-12T10:18:57.342Z" + } + }, + "aggType": "date_histogram" + }, + "y": [ + { + "accessor": 1, + "format": { + "id": "number", + "params": { + "pattern": "$0,0.[00]" + } + }, + "params": {}, + "aggType": "percentiles" + }, + { + "accessor": 2, + "format": { + "id": "number", + "params": { + "pattern": "$0,0.[00]" + } + }, + "params": {}, + "aggType": "percentiles" + } + ] + }, + "grid": { + "categoryLines": false, + "style": { + "color": "#eee" + } + }, + "legendPosition": "right", + "seriesParams": [ + { + "data": { + "id": "1", + "label": "Percentiles of AvgTicketPrice" + }, + "drawLinesBetweenPoints": true, + "interpolate": "cardinal", + "mode": "normal", + "show": "true", + "showCircles": true, + "type": "line", + "valueAxis": "ValueAxis-1" + } + ], + "times": [], + "type": "point_series", + "valueAxes": [ + { + "id": "ValueAxis-1", + "labels": { + "filter": false, + "rotate": 0, + "show": true, + "truncate": 100 + }, + "name": "LeftAxis-1", + "position": "left", + "scale": { + "mode": "normal", + "type": "linear" + }, + "show": true, + "style": {}, + "title": { + "text": "Percentiles of AvgTicketPrice" + }, + "type": "value" + } + ], + "chartTitle": {}, + "mode": "normal", + "tooltip": { + "show": true + }, + "charts": [ + { + "type": "point_series", + "addTimeMarker": false, + "series": [ + { + "show": true, + "type": "area", + "mode": "normal", + "drawLinesBetweenPoints": true, + "showCircles": true, + "data": { + "id": "1.['1.1']", + "rawId": "col-1-1.['1.1']", + "label": "1.1th percentile of AvgTicketPrice", + "values": [ + { + "x": 1557460800000, + "y": 116.33676605224609, + "extraMetrics": [], + "xRaw": { + "table": { + "columns": [ + { + "id": "col-0-2", + "name": "timestamp per day" + }, + { + "id": "col-1-1.['1.1']", + "name": "1.1th percentile of AvgTicketPrice" + }, + { + "id": "col-2-1.50", + "name": "50th percentile of AvgTicketPrice" + } + ], + "rows": [ + { + "col-0-2": 1557460800000, + "col-1-1.['1.1']": 116, + "col-2-1.50": 658 + }, + { + "col-0-2": 1557547200000, + "col-1-1.['1.1']": 223, + "col-2-1.50": 756 + } + ] + }, + "column": 0, + "row": 0, + "value": 1557460800000 + }, + "yRaw": { + "table": { + "columns": [ + { + "id": "col-0-2", + "name": "timestamp per day" + }, + { + "id": "col-1-1.['1.1']", + "name": "1.1th percentile of AvgTicketPrice" + }, + { + "id": "col-2-1.50", + "name": "50th percentile of AvgTicketPrice" + } + ], + "rows": [ + { + "col-0-2": 1557460800000, + "col-1-1.['1.1']": 116, + "col-2-1.50": 658 + }, + { + "col-0-2": 1557547200000, + "col-1-1.['1.1']": 223, + "col-2-1.50": 756 + } + ] + }, + "column": 1, + "row": 0, + "value": 116.33676605224609 + }, + "parent": null, + "series": "1.1th percentile of AvgTicketPrice", + "seriesId": "col-1-1.['1.1']" + }, + { + "x": 1557547200000, + "y": 223, + "extraMetrics": [], + "xRaw": { + "table": { + "columns": [ + { + "id": "col-0-2", + "name": "timestamp per day" + }, + { + "id": "col-1-1.['1.1']", + "name": "1.1th percentile of AvgTicketPrice" + }, + { + "id": "col-2-1.50", + "name": "50th percentile of AvgTicketPrice" + } + ], + "rows": [ + { + "col-0-2": 1557460800000, + "col-1-1.['1.1']": 116, + "col-2-1.50": 658 + }, + { + "col-0-2": 1557547200000, + "col-1-1.['1.1']": 223, + "col-2-1.50": 756 + } + ] + }, + "column": 0, + "row": 1, + "value": 1557547200000 + }, + "yRaw": { + "table": { + "columns": [ + { + "id": "col-0-2", + "name": "timestamp per day" + }, + { + "id": "col-1-1.['1.1']", + "name": "1.1th percentile of AvgTicketPrice" + }, + { + "id": "col-2-1.50", + "name": "50th percentile of AvgTicketPrice" + } + ], + "rows": [ + { + "col-0-2": 1557460800000, + "col-1-1.['1.1']": 116, + "col-2-1.50": 658 + }, + { + "col-0-2": 1557547200000, + "col-1-1.['1.1']": 223, + "col-2-1.50": 756 + } + ] + }, + "column": 1, + "row": 1, + "value": 223 + }, + "parent": null, + "series": "1.1th percentile of AvgTicketPrice", + "seriesId": "col-1-1.['1.1']" + } + ] + } + }, + { + "data": { + "id": "1.50", + "rawId": "col-2-1.50", + "label": "50th percentile of AvgTicketPrice", + "values": [ + { + "x": 1557460800000, + "y": 658.8453063964844, + "extraMetrics": [], + "xRaw": { + "table": { + "columns": [ + { + "id": "col-0-2", + "name": "timestamp per day" + }, + { + "id": "col-1-1.['1.1']", + "name": "1.1th percentile of AvgTicketPrice" + }, + { + "id": "col-2-1.50", + "name": "50th percentile of AvgTicketPrice" + } + ], + "rows": [ + { + "col-0-2": 1557460800000, + "col-1-1.['1.1']": 116, + "col-2-1.50": 658 + }, + { + "col-0-2": 1557547200000, + "col-1-1.['1.1']": 223, + "col-2-1.50": 756 + } + ] + }, + "column": 0, + "row": 0, + "value": 1557460800000 + }, + "yRaw": { + "table": { + "columns": [ + { + "id": "col-0-2", + "name": "timestamp per day" + }, + { + "id": "col-1-1.['1.1']", + "name": "1.1th percentile of AvgTicketPrice" + }, + { + "id": "col-2-1.50", + "name": "50th percentile of AvgTicketPrice" + } + ], + "rows": [ + { + "col-0-2": 1557460800000, + "col-1-1.['1.1']": 116, + "col-2-1.50": 658 + }, + { + "col-0-2": 1557547200000, + "col-1-1.['1.1']": 223, + "col-2-1.50": 756 + } + ] + }, + "column": 2, + "row": 0, + "value": 658 + }, + "parent": null, + "series": "50th percentile of AvgTicketPrice", + "seriesId": "col-2-1.50" + }, + { + "x": 1557547200000, + "y": 756, + "extraMetrics": [], + "xRaw": { + "table": { + "columns": [ + { + "id": "col-0-2", + "name": "timestamp per day" + }, + { + "id": "col-1-1.['1.1']", + "name": "1.1th percentile of AvgTicketPrice" + }, + { + "id": "col-2-1.50", + "name": "50th percentile of AvgTicketPrice" + } + ], + "rows": [ + { + "col-0-2": 1557460800000, + "col-1-1.['1.1']": 116, + "col-2-1.50": 658 + }, + { + "col-0-2": 1557547200000, + "col-1-1.['1.1']": 223, + "col-2-1.50": 756 + } + ] + }, + "column": 0, + "row": 1, + "value": 1557547200000 + }, + "yRaw": { + "table": { + "columns": [ + { + "id": "col-0-2", + "name": "timestamp per day" + }, + { + "id": "col-1-1.['1.1']", + "name": "1.1th percentile of AvgTicketPrice" + }, + { + "id": "col-2-1.50", + "name": "50th percentile of AvgTicketPrice" + } + ], + "rows": [ + { + "col-0-2": 1557460800000, + "col-1-1.['1.1']": 116, + "col-2-1.50": 658 + }, + { + "col-0-2": 1557547200000, + "col-1-1.['1.1']": 223, + "col-2-1.50": 756 + } + ] + }, + "column": 2, + "row": 1, + "value": 756.2283554077148 + }, + "parent": null, + "series": "50th percentile of AvgTicketPrice", + "seriesId": "col-2-1.50" + } + ] + }, + "drawLinesBetweenPoints": true, + "interpolate": "cardinal", + "mode": "normal", + "show": "true", + "showCircles": true, + "type": "line", + "valueAxis": "ValueAxis-1" + } + ] + } + ], + "enableHover": true +} \ No newline at end of file diff --git a/src/plugins/vis_type_vislib/public/vislib/lib/types/testdata_linechart_percentile_result.json b/src/plugins/vis_type_vislib/public/vislib/lib/types/testdata_linechart_percentile_result.json index d50d20a70608b..02062c987564e 100644 --- a/src/plugins/vis_type_vislib/public/vislib/lib/types/testdata_linechart_percentile_result.json +++ b/src/plugins/vis_type_vislib/public/vislib/lib/types/testdata_linechart_percentile_result.json @@ -122,8 +122,337 @@ { "type": "point_series", "addTimeMarker": false, - "series": [] + "series": [ + { + "data": { + "id": "1.1", + "rawId": "col-1-1.1", + "label": "1st percentile of AvgTicketPrice", + "values": [ + { + "x": 1557460800000, + "y": 116.33676605224609, + "extraMetrics": [], + "xRaw": { + "table": { + "columns": [ + { + "id": "col-0-2", + "name": "timestamp per day" + }, + { + "id": "col-1-1.1", + "name": "1st percentile of AvgTicketPrice" + }, + { + "id": "col-2-1.50", + "name": "50th percentile of AvgTicketPrice" + } + ], + "rows": [ + { + "col-0-2": 1557460800000, + "col-1-1.1": 116, + "col-2-1.50": 658 + }, + { + "col-0-2": 1557547200000, + "col-1-1.1": 223, + "col-2-1.50": 756 + } + ] + }, + "column": 0, + "row": 0, + "value": 1557460800000 + }, + "yRaw": { + "table": { + "columns": [ + { + "id": "col-0-2", + "name": "timestamp per day" + }, + { + "id": "col-1-1.1", + "name": "1st percentile of AvgTicketPrice" + }, + { + "id": "col-2-1.50", + "name": "50th percentile of AvgTicketPrice" + } + ], + "rows": [ + { + "col-0-2": 1557460800000, + "col-1-1.1": 116, + "col-2-1.50": 658 + }, + { + "col-0-2": 1557547200000, + "col-1-1.1": 223, + "col-2-1.50": 756 + } + ] + }, + "column": 1, + "row": 0, + "value": 116.33676605224609 + }, + "parent": null, + "series": "1st percentile of AvgTicketPrice", + "seriesId": "col-1-1.1" + }, + { + "x": 1557547200000, + "y": 223, + "extraMetrics": [], + "xRaw": { + "table": { + "columns": [ + { + "id": "col-0-2", + "name": "timestamp per day" + }, + { + "id": "col-1-1.1", + "name": "1st percentile of AvgTicketPrice" + }, + { + "id": "col-2-1.50", + "name": "50th percentile of AvgTicketPrice" + } + ], + "rows": [ + { + "col-0-2": 1557460800000, + "col-1-1.1": 116, + "col-2-1.50": 658 + }, + { + "col-0-2": 1557547200000, + "col-1-1.1": 223, + "col-2-1.50": 756 + } + ] + }, + "column": 0, + "row": 1, + "value": 1557547200000 + }, + "yRaw": { + "table": { + "columns": [ + { + "id": "col-0-2", + "name": "timestamp per day" + }, + { + "id": "col-1-1.1", + "name": "1st percentile of AvgTicketPrice" + }, + { + "id": "col-2-1.50", + "name": "50th percentile of AvgTicketPrice" + } + ], + "rows": [ + { + "col-0-2": 1557460800000, + "col-1-1.1": 116, + "col-2-1.50": 658 + }, + { + "col-0-2": 1557547200000, + "col-1-1.1": 223, + "col-2-1.50": 756 + } + ] + }, + "column": 1, + "row": 1, + "value": 223 + }, + "parent": null, + "series": "1st percentile of AvgTicketPrice", + "seriesId": "col-1-1.1" + } + ] + }, + "drawLinesBetweenPoints": true, + "interpolate": "cardinal", + "mode": "normal", + "show": "true", + "showCircles": true, + "type": "line", + "valueAxis": "ValueAxis-1" + }, + { + "data": { + "id": "1.50", + "rawId": "col-2-1.50", + "label": "50th percentile of AvgTicketPrice", + "values": [ + { + "x": 1557460800000, + "y": 658.8453063964844, + "extraMetrics": [], + "xRaw": { + "table": { + "columns": [ + { + "id": "col-0-2", + "name": "timestamp per day" + }, + { + "id": "col-1-1.1", + "name": "1st percentile of AvgTicketPrice" + }, + { + "id": "col-2-1.50", + "name": "50th percentile of AvgTicketPrice" + } + ], + "rows": [ + { + "col-0-2": 1557460800000, + "col-1-1.1": 116, + "col-2-1.50": 658 + }, + { + "col-0-2": 1557547200000, + "col-1-1.1": 223, + "col-2-1.50": 756 + } + ] + }, + "column": 0, + "row": 0, + "value": 1557460800000 + }, + "yRaw": { + "table": { + "columns": [ + { + "id": "col-0-2", + "name": "timestamp per day" + }, + { + "id": "col-1-1.1", + "name": "1st percentile of AvgTicketPrice" + }, + { + "id": "col-2-1.50", + "name": "50th percentile of AvgTicketPrice" + } + ], + "rows": [ + { + "col-0-2": 1557460800000, + "col-1-1.1": 116, + "col-2-1.50": 658 + }, + { + "col-0-2": 1557547200000, + "col-1-1.1": 223, + "col-2-1.50": 756 + } + ] + }, + "column": 2, + "row": 0, + "value": 658 + }, + "parent": null, + "series": "50th percentile of AvgTicketPrice", + "seriesId": "col-2-1.50" + }, + { + "x": 1557547200000, + "y": 756, + "extraMetrics": [], + "xRaw": { + "table": { + "columns": [ + { + "id": "col-0-2", + "name": "timestamp per day" + }, + { + "id": "col-1-1.1", + "name": "1st percentile of AvgTicketPrice" + }, + { + "id": "col-2-1.50", + "name": "50th percentile of AvgTicketPrice" + } + ], + "rows": [ + { + "col-0-2": 1557460800000, + "col-1-1.1": 116, + "col-2-1.50": 658 + }, + { + "col-0-2": 1557547200000, + "col-1-1.1": 223, + "col-2-1.50": 756 + } + ] + }, + "column": 0, + "row": 1, + "value": 1557547200000 + }, + "yRaw": { + "table": { + "columns": [ + { + "id": "col-0-2", + "name": "timestamp per day" + }, + { + "id": "col-1-1.1", + "name": "1st percentile of AvgTicketPrice" + }, + { + "id": "col-2-1.50", + "name": "50th percentile of AvgTicketPrice" + } + ], + "rows": [ + { + "col-0-2": 1557460800000, + "col-1-1.1": 116, + "col-2-1.50": 658 + }, + { + "col-0-2": 1557547200000, + "col-1-1.1": 223, + "col-2-1.50": 756 + } + ] + }, + "column": 2, + "row": 1, + "value": 756.2283554077148 + }, + "parent": null, + "series": "50th percentile of AvgTicketPrice", + "seriesId": "col-2-1.50" + } + ] + }, + "drawLinesBetweenPoints": true, + "interpolate": "cardinal", + "mode": "normal", + "show": "true", + "showCircles": true, + "type": "line", + "valueAxis": "ValueAxis-1" + } + ] } ], "enableHover": true -} +} \ No newline at end of file From 7cee2a6b15175928b38bb3d5d5531d7b887f0343 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Mon, 20 Jul 2020 09:25:39 +0100 Subject: [PATCH 14/45] [Observability] Remove app logos (#72259) * removing app logos * fixing TS error --- .../public/components/app/empty_section/index.tsx | 2 -- x-pack/plugins/observability/public/pages/landing/index.tsx | 4 +--- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/x-pack/plugins/observability/public/components/app/empty_section/index.tsx b/x-pack/plugins/observability/public/components/app/empty_section/index.tsx index e19bf1678bc01..4c830b2b2f094 100644 --- a/x-pack/plugins/observability/public/components/app/empty_section/index.tsx +++ b/x-pack/plugins/observability/public/components/app/empty_section/index.tsx @@ -15,8 +15,6 @@ export const EmptySection = ({ section }: Props) => { return ( {section.title}} titleSize="xs" body={{section.description}} diff --git a/x-pack/plugins/observability/public/pages/landing/index.tsx b/x-pack/plugins/observability/public/pages/landing/index.tsx index da46791d9e855..81485953f8713 100644 --- a/x-pack/plugins/observability/public/pages/landing/index.tsx +++ b/x-pack/plugins/observability/public/pages/landing/index.tsx @@ -10,7 +10,6 @@ import { EuiFlexGrid, EuiFlexGroup, EuiFlexItem, - EuiIcon, EuiImage, EuiSpacer, EuiText, @@ -19,10 +18,10 @@ import { import { i18n } from '@kbn/i18n'; import React, { useContext } from 'react'; import styled, { ThemeContext } from 'styled-components'; +import { IngestManagerPanel } from '../../components/app/ingest_manager_panel'; import { WithHeaderLayout } from '../../components/app/layout/with_header'; import { usePluginContext } from '../../hooks/use_plugin_context'; import { appsSection } from '../home/section'; -import { IngestManagerPanel } from '../../components/app/ingest_manager_panel'; const EuiCardWithoutPadding = styled(EuiCard)` padding: 0; @@ -68,7 +67,6 @@ export const LandingPage = () => { } title={

{app.title}

From b39c46eeac36d65eaaf18bdbaa29ba86d9cea26c Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Mon, 20 Jul 2020 09:30:06 +0100 Subject: [PATCH 15/45] [ML] Disabling secondary auth headers when security is disabled (#72371) Co-authored-by: Elastic Machine --- x-pack/plugins/ml/server/lib/request_authorization.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/ml/server/lib/request_authorization.ts b/x-pack/plugins/ml/server/lib/request_authorization.ts index 01df0900b96f4..d40909a3a3a4c 100644 --- a/x-pack/plugins/ml/server/lib/request_authorization.ts +++ b/x-pack/plugins/ml/server/lib/request_authorization.ts @@ -7,7 +7,9 @@ import { KibanaRequest } from 'kibana/server'; export function getAuthorizationHeader(request: KibanaRequest) { - return { - headers: { 'es-secondary-authorization': request.headers.authorization }, - }; + return request.headers.authorization === undefined + ? {} + : { + headers: { 'es-secondary-authorization': request.headers.authorization }, + }; } From ec4f9d50ba311122cf5f6583e790a86db13e462a Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Mon, 20 Jul 2020 10:43:12 +0200 Subject: [PATCH 16/45] Migrate and cleanup legacy scss (#69369) --- src/core/public/index.scss | 3 +- .../public/styles}/_ace_overrides.scss | 5 +- src/core/public/styles/_base.scss | 58 +++ src/core/public/styles/_index.scss | 2 + src/legacy/ui/public/_index.scss | 3 - .../ui/public/styles/_legacy/_base.scss | 153 -------- .../ui/public/styles/_legacy/_index.scss | 7 - .../styles/_legacy/components/_config.scss | 11 - .../_legacy/components/_control_group.scss | 67 ---- .../styles/_legacy/components/_hintbox.scss | 59 --- .../styles/_legacy/components/_index.scss | 13 - .../styles/_legacy/components/_input.scss | 12 - .../styles/_legacy/components/_kui_forms.scss | 29 -- .../styles/_legacy/components/_navbar.scss | 86 ----- .../_legacy/components/_pagination.scss | 56 --- .../styles/_legacy/components/_spinner.scss | 6 - .../styles/_legacy/components/_table.scss | 46 --- .../styles/_legacy/components/_truncate.scss | 3 - .../styles/_legacy/components/_ui_select.scss | 357 ------------------ src/legacy/ui/public/styles/_mixins.scss | 2 - .../public/application/_discover.scss | 12 + .../discover/public/application/_hacks.scss | 4 - .../discover/public/application/_mixins.scss | 27 -- .../angular/doc_table/_doc_table.scss | 77 +++- .../application/angular/doc_table/index.scss | 2 - .../discover/public/application/index.scss | 13 +- .../public/paginate/_paginate.scss | 57 +++ .../kibana_legacy/public/paginate/paginate.js | 1 + src/plugins/timelion/public/_base.scss | 19 + .../timelion/public/directives/_form.scss} | 40 +- .../timelion/public/directives/_index.scss | 3 + .../directives/_saved_object_finder.scss} | 6 +- src/plugins/timelion/public/index.scss | 1 + .../public/agg_table/_agg_table.scss | 4 + .../plugins/ml/public/application/_hacks.scss | 36 -- .../server/lib/layouts/preserve_layout.css | 1 - .../reporting/server/lib/layouts/print.css | 1 - 37 files changed, 251 insertions(+), 1031 deletions(-) rename src/{legacy/ui/public/styles/_legacy/components => core/public/styles}/_ace_overrides.scss (96%) create mode 100644 src/core/public/styles/_base.scss create mode 100644 src/core/public/styles/_index.scss delete mode 100644 src/legacy/ui/public/styles/_legacy/_base.scss delete mode 100644 src/legacy/ui/public/styles/_legacy/_index.scss delete mode 100644 src/legacy/ui/public/styles/_legacy/components/_config.scss delete mode 100644 src/legacy/ui/public/styles/_legacy/components/_control_group.scss delete mode 100644 src/legacy/ui/public/styles/_legacy/components/_hintbox.scss delete mode 100644 src/legacy/ui/public/styles/_legacy/components/_index.scss delete mode 100644 src/legacy/ui/public/styles/_legacy/components/_input.scss delete mode 100644 src/legacy/ui/public/styles/_legacy/components/_kui_forms.scss delete mode 100644 src/legacy/ui/public/styles/_legacy/components/_navbar.scss delete mode 100644 src/legacy/ui/public/styles/_legacy/components/_pagination.scss delete mode 100644 src/legacy/ui/public/styles/_legacy/components/_spinner.scss delete mode 100644 src/legacy/ui/public/styles/_legacy/components/_table.scss delete mode 100644 src/legacy/ui/public/styles/_legacy/components/_truncate.scss delete mode 100644 src/legacy/ui/public/styles/_legacy/components/_ui_select.scss delete mode 100644 src/plugins/discover/public/application/_hacks.scss delete mode 100644 src/plugins/discover/public/application/_mixins.scss create mode 100644 src/plugins/kibana_legacy/public/paginate/_paginate.scss create mode 100644 src/plugins/timelion/public/_base.scss rename src/{legacy/ui/public/styles/_legacy/_mixins.scss => plugins/timelion/public/directives/_form.scss} (58%) rename src/{legacy/ui/public/styles/_legacy/components/_list_group_menu.scss => plugins/timelion/public/directives/_saved_object_finder.scss} (85%) diff --git a/src/core/public/index.scss b/src/core/public/index.scss index 87825350b4e98..c2ad2841d5a77 100644 --- a/src/core/public/index.scss +++ b/src/core/public/index.scss @@ -2,6 +2,5 @@ @import './chrome/index'; @import './overlays/index'; @import './rendering/index'; +@import './styles/index'; -// Global styles need to be migrated -@import '../../legacy/ui/public/styles/_legacy/_index'; diff --git a/src/legacy/ui/public/styles/_legacy/components/_ace_overrides.scss b/src/core/public/styles/_ace_overrides.scss similarity index 96% rename from src/legacy/ui/public/styles/_legacy/components/_ace_overrides.scss rename to src/core/public/styles/_ace_overrides.scss index 2f0bc011f6a5c..30acdbbc80975 100644 --- a/src/legacy/ui/public/styles/_legacy/components/_ace_overrides.scss +++ b/src/core/public/styles/_ace_overrides.scss @@ -1,6 +1,3 @@ -@import '@elastic/eui/src/components/call_out/variables'; -@import '@elastic/eui/src/components/call_out/mixins'; - // SASSTODO: Replace with an EUI editor // Intentionally not using the EuiCodeBlock colors here because they actually change // hue from light to dark theme. So some colors would change while others wouldn't. @@ -181,7 +178,7 @@ } &.ace_multiselect .ace_selection.ace_start { - box-shadow: 0 0 3px 0px $euiColorEmptyShade; + box-shadow: 0 0 3px 0 $euiColorEmptyShade; } .ace_marker-layer .ace_step { diff --git a/src/core/public/styles/_base.scss b/src/core/public/styles/_base.scss new file mode 100644 index 0000000000000..9b06b526fc7dd --- /dev/null +++ b/src/core/public/styles/_base.scss @@ -0,0 +1,58 @@ +@import '@elastic/eui/src/components/collapsible_nav/variables'; +// Application Layout + +// chrome-context +// TODO #64541 +// Delete this block +.chrHeaderWrapper:not(.headerWrapper) .content { + display: flex; + flex-flow: row nowrap; + width: 100%; + height: 100%; + overflow: hidden; +} + +.application, +.app-container { + > * { + position: relative; + } +} + +.application { + position: relative; + z-index: 0; + display: flex; + flex-grow: 1; + flex-shrink: 0; + flex-basis: auto; + flex-direction: column; + + > * { + flex-shrink: 0; + } +} + +// We apply brute force focus states to anything not coming from Eui +// which has focus states designed at the component level. +// You can also use "kbn-resetFocusState" to not apply the default focus +// state. This is useful when you've already hand crafted your own +// focus states in Kibana. +:focus { + &:not([class^='eui']):not(.kbn-resetFocusState) { + @include euiFocusRing; + } +} + +// A necessary hack so that the above focus policy doesn't pollute some EUI +// entrenched inputs. +.euiComboBox { + // :not() specificity needed to override the above + input:not([class^='eui']):focus { + animation: none !important; + } +} + +.euiBody--collapsibleNavIsDocked .euiBottomBar { + margin-left: $euiCollapsibleNavWidth; +} diff --git a/src/core/public/styles/_index.scss b/src/core/public/styles/_index.scss new file mode 100644 index 0000000000000..600414402c278 --- /dev/null +++ b/src/core/public/styles/_index.scss @@ -0,0 +1,2 @@ +@import './base'; +@import './ace_overrides'; diff --git a/src/legacy/ui/public/_index.scss b/src/legacy/ui/public/_index.scss index 323de2ea7d263..a441b773d4a4e 100644 --- a/src/legacy/ui/public/_index.scss +++ b/src/legacy/ui/public/_index.scss @@ -1,6 +1,3 @@ -// Legacy styles to come before all -@import './styles/_legacy/index'; - // Prefix all styles with "kbn" to avoid conflicts. // Examples // kbnChart diff --git a/src/legacy/ui/public/styles/_legacy/_base.scss b/src/legacy/ui/public/styles/_legacy/_base.scss deleted file mode 100644 index 877ae033ae584..0000000000000 --- a/src/legacy/ui/public/styles/_legacy/_base.scss +++ /dev/null @@ -1,153 +0,0 @@ -@import '@elastic/eui/src/components/collapsible_nav/variables'; - -// Forms - -// Angular form states -input.ng-invalid, -textarea.ng-invalid, -select.ng-invalid { - &.ng-dirty, - &.ng-touched { - border-color: $euiColorDanger !important; - } -} - -input[type='radio'], -input[type='checkbox'], -.radio, -.radio-inline, -.checkbox, -.checkbox-inline { - &[disabled], - fieldset[disabled] & { - cursor: default; - opacity: 0.8; - } -} - -.checkbox label { - display: flex; - align-items: center; - padding-left: 0 !important; - - input[type='checkbox'] { - float: none; - margin: 0 $euiSizeXS; - position: static; - } -} - -// Application Layout - -// chrome-context -// TODO #64541 -// Delete this block -.chrHeaderWrapper:not(.headerWrapper) .content { - display: flex; - flex-flow: row nowrap; - width: 100%; - height: 100%; - overflow: hidden; -} - -.application, -.app-container { - > * { - position: relative; - } - - > config { - z-index: 1; - } - - > navbar { - padding-bottom: $euiSizeS; - } - - > nav, - > navbar { - z-index: 2 !important; - } -} - -.application { - position: relative; - z-index: 0; - display: flex; - flex-grow: 1; - flex-shrink: 0; - flex-basis: auto; - flex-direction: column; - - > * { - flex-shrink: 0; - } -} - -[fixed-scroll] { - overflow-x: auto; - padding-bottom: 0; - - + .fixed-scroll-scroller { - position: fixed; - bottom: 0; - overflow-x: auto; - overflow-y: hidden; - } -} - -// Too overused in many places to be moved elsewhere - -.page-row { - padding: 0 $euiSize; - margin: $euiSize 0; -} - -.page-row-text { - color: $euiColorDarkShade; - font-size: $euiFontSizeS; -} - -// We apply brute force focus states to anything not coming from Eui -// which has focus states designed at the component level. -// You can also use "kbn-resetFocusState" to not apply the default focus -// state. This is useful when you've already hand crafted your own -// focus states in Kibana. -:focus { - &:not([class^='eui']):not(.kbn-resetFocusState) { - @include euiFocusRing; - } -} - -// A necessary hack so that the above focus policy doesn't pollute some EUI -// entrenched inputs. -.euiComboBox { - // :not() specificity needed to override the above - input:not([class^='eui']):focus { - animation: none !important; - } -} - -.euiBody--collapsibleNavIsDocked .euiBottomBar { - margin-left: $euiCollapsibleNavWidth; -} - -// Utility classes - -.fullWidth { - width: 100% !important; -} - -.small { - font-size: 0.9em !important; -} -.smaller { - font-size: 0.8em !important; -} -.smallest { - font-size: 0.7em !important; -} - -.text-monospace { - font-family: $euiCodeFontFamily; -} diff --git a/src/legacy/ui/public/styles/_legacy/_index.scss b/src/legacy/ui/public/styles/_legacy/_index.scss deleted file mode 100644 index a0b1a98b09b7d..0000000000000 --- a/src/legacy/ui/public/styles/_legacy/_index.scss +++ /dev/null @@ -1,7 +0,0 @@ -// // -// // KIBANA THEME -@import './base'; -@import './mixins'; - -// // Components -@import './components/index'; diff --git a/src/legacy/ui/public/styles/_legacy/components/_config.scss b/src/legacy/ui/public/styles/_legacy/components/_config.scss deleted file mode 100644 index b56826f1e7088..0000000000000 --- a/src/legacy/ui/public/styles/_legacy/components/_config.scss +++ /dev/null @@ -1,11 +0,0 @@ -// SASSTODO: Selector is so generic it's hard to find if it's actually used -.config { - @extend .navbar !optional; - @extend .navbar-default !optional; - border-bottom: 1px solid transparent; - - .container-fluid { - background-color: $euiPageBackgroundColor; - padding: $euiSizeS; - } -} diff --git a/src/legacy/ui/public/styles/_legacy/components/_control_group.scss b/src/legacy/ui/public/styles/_legacy/components/_control_group.scss deleted file mode 100644 index ce958a9aae77f..0000000000000 --- a/src/legacy/ui/public/styles/_legacy/components/_control_group.scss +++ /dev/null @@ -1,67 +0,0 @@ -.control-group { - display: flex; - flex: 0 0 auto; - flex-direction: row; - flex-wrap: wrap; - align-items: stretch; - padding: $euiSizeXS $euiSize; - - > * { - padding-right: $euiSize; - flex: 0 0 auto; - - &:last-child { - padding-right: 0; - } - } - - // horizontal group of buttons/form elements - .inline-form .input-group { - margin-bottom: 0; - display: flex; - - > * { - border-radius: 0; - } - - > :first-child { - border-bottom-left-radius: $euiBorderRadius; - border-top-left-radius: $euiBorderRadius; - } - - > :last-child { - border-bottom-right-radius: $euiBorderRadius; - border-top-right-radius: $euiBorderRadius; - } - } - - .inline-form { - @include flex-parent(0, 0, auto); - display: flex; - - > .typeahead { - @include flex-parent(); - - > .input-group { - display: flex; - flex: 1 0 auto; - - > * { - float: none; - height: auto; - width: auto; - flex: 0 0 auto; - } - - input[type="text"] { - flex: 1 1 100%; - } - } - } - } - - // the element should take up an even share of available space - > .fill { - flex: 1 1 1%; - } -} diff --git a/src/legacy/ui/public/styles/_legacy/components/_hintbox.scss b/src/legacy/ui/public/styles/_legacy/components/_hintbox.scss deleted file mode 100644 index 0c447031636ac..0000000000000 --- a/src/legacy/ui/public/styles/_legacy/components/_hintbox.scss +++ /dev/null @@ -1,59 +0,0 @@ -.hintbox { - padding: $euiSizeS $euiSizeM; - border-radius: $euiBorderRadius; - margin-bottom: $euiSizeS; - background-color: $euiColorLightShade; - line-height: $euiLineHeight; - - a { - color: $euiLinkColor !important; - - &:hover { - color: darken($euiLinkColor, 10%) !important; - } - } - - pre { - background-color: $euiColorEmptyShade; - } - - ul, ol { - padding-left: $euiSizeL; - } - - // inspired by Bootstrap alerts component - // https://github.com/twbs/bootstrap/blob/063c1b0780ea0240e4adce4c88d57fc23e099475/less/alerts.less#L27-L35 - > * { - margin: 0; - } - - > * + * { - margin-top: $euiSizeS; - } - - // https://github.com/twbs/bootstrap/blob/2aa102bfd40859d15790febed1939e0111a6fb1a/less/tables.less#L88-L106 - .table-bordered { - border: $euiBorderThin; - > thead, - > tbody, - > tfoot { - > tr { - > th, - > td { - border: $euiBorderThin; - } - } - } - > thead > tr { - > th, - > td { - border-bottom-width: 2px; - } - } - } -} - - .hintbox-label, - .hintbox-label[ng-click] { - cursor: help; - } diff --git a/src/legacy/ui/public/styles/_legacy/components/_index.scss b/src/legacy/ui/public/styles/_legacy/components/_index.scss deleted file mode 100644 index cfae0700bb71e..0000000000000 --- a/src/legacy/ui/public/styles/_legacy/components/_index.scss +++ /dev/null @@ -1,13 +0,0 @@ -@import './ace_overrides'; -@import './control_group'; -@import './hintbox'; -@import './input'; -@import './kui_forms'; -@import './list_group_menu'; -@import './navbar'; -@import './config'; -@import './pagination'; -@import './spinner'; -@import './table'; -@import './truncate'; -@import './ui_select'; diff --git a/src/legacy/ui/public/styles/_legacy/components/_input.scss b/src/legacy/ui/public/styles/_legacy/components/_input.scss deleted file mode 100644 index 13efc9646e820..0000000000000 --- a/src/legacy/ui/public/styles/_legacy/components/_input.scss +++ /dev/null @@ -1,12 +0,0 @@ -i.input-error { - position: absolute; - margin-left: -$euiSizeL; - color: $euiColorDanger; - margin-top: $euiSizeS; - z-index: 5; -} - -select { - color: $euiTextColor; - background-color: $euiColorEmptyShade; -} diff --git a/src/legacy/ui/public/styles/_legacy/components/_kui_forms.scss b/src/legacy/ui/public/styles/_legacy/components/_kui_forms.scss deleted file mode 100644 index 2e1b814d647e3..0000000000000 --- a/src/legacy/ui/public/styles/_legacy/components/_kui_forms.scss +++ /dev/null @@ -1,29 +0,0 @@ -.form-control { - @include __legacyInputStyles__bad; -} - -select.form-control { - @include __legacySelectStyles__bad; -} - -.kuiFormSection { - margin-bottom: $euiSize; -} - -.kuiFormLabel { - @include __legacyLabelStyles__bad; - display: block; - margin-bottom: 5px; -} - -.kuiInputNote { - margin: $euiSizeXS 0 $euiSizeS; -} - -.kuiInputNote--danger { - color: $euiColorDanger; -} - -.kuiInputNote--warning { - color: $euiColorWarning; -} diff --git a/src/legacy/ui/public/styles/_legacy/components/_navbar.scss b/src/legacy/ui/public/styles/_legacy/components/_navbar.scss deleted file mode 100644 index b06c655789a50..0000000000000 --- a/src/legacy/ui/public/styles/_legacy/components/_navbar.scss +++ /dev/null @@ -1,86 +0,0 @@ -navbar { - @extend .control-group; - - max-height: 340px; - margin-bottom: 0; - padding: 0 $euiSizeS $euiSizeXS; - color: $euiColorDarkShade; - background-color: $euiColorLightShade; - border: none; - z-index: $euiZLevel1; - - > * { - padding-right: $euiSizeS; - } - - .navbar-text { - margin-top: $euiSizeXS; - margin-bottom: $euiSizeXS; - } - - // the "brand" that is displayed, usually on the left of the navbar - > .name { - @include euiTextTruncate; - - align-self: center; - font-size: $euiFontSizeL; - } - - button { - color: $euiColorDarkShade; - background-color: transparent; - - &:hover, - &:focus { - color: $euiColorDarkShade; - background-color: transparent; - } - - &:active, &.active { - color: $euiColorDarkestShade; - background-color: transparent; - box-shadow: none; - - &:focus { - outline: none; - } - } - - &[disabled] { - color: $euiColorMediumShade; - background-color: transparent; - } - } - - .inline-form .input-group { - button { - color: $euiColorEmptyShade; - background-color: $euiColorDarkShade; - border: none; - } - } - - // responsive modifications - - // desktop - @include euiBreakpoint('l', 'xl') { - > .name { - // 500px is sort of arbitrary, not sure how to deal with lots of buttons - max-width: 500px; - } - } - - // tablets/phones - @include euiBreakpoint('xs', 's', 'm') { - > .fill { - flex: 1 1 map-get($euiBreakpoints, 'l'); - } - } - - // phones - @include euiBreakpoint('xs', 's') { - > .name { - max-width: 100%; - } - } -} diff --git a/src/legacy/ui/public/styles/_legacy/components/_pagination.scss b/src/legacy/ui/public/styles/_legacy/components/_pagination.scss deleted file mode 100644 index cf68f2ac8253f..0000000000000 --- a/src/legacy/ui/public/styles/_legacy/components/_pagination.scss +++ /dev/null @@ -1,56 +0,0 @@ -paginate { - display: block; - - paginate-controls { - display: flex; - align-items: center; - padding: $euiSizeXS $euiSizeXS $euiSizeS; - text-align: center; - - .pagination-other-pages { - flex: 1 0 auto; - display: flex; - justify-content: center; - } - - .pagination-other-pages-list { - flex: 0 0 auto; - display: flex; - justify-content: center; - padding: 0; - margin: 0; - list-style: none; - - > li { - flex: 0 0 auto; - user-select: none; - - a { - text-decoration: none; - background-color: $euiColorLightestShade; - margin-left: $euiSizeXS / 2; - padding: $euiSizeS $euiSizeM; - } - - a:hover { - text-decoration: underline; - } - - &.active a { - text-decoration: none !important; - font-weight: $euiFontWeightBold; - color: $euiColorDarkShade; - cursor: default; - } - } - } - - .pagination-size { - flex: 0 0 auto; - - input[type=number] { - width: 3em; - } - } - } -} diff --git a/src/legacy/ui/public/styles/_legacy/components/_spinner.scss b/src/legacy/ui/public/styles/_legacy/components/_spinner.scss deleted file mode 100644 index 7b3b1bd615ae0..0000000000000 --- a/src/legacy/ui/public/styles/_legacy/components/_spinner.scss +++ /dev/null @@ -1,6 +0,0 @@ -.spinner.ng-hide { - visibility: hidden; - display: block !important; - opacity: 0; - transition-delay: 0.25s; -} diff --git a/src/legacy/ui/public/styles/_legacy/components/_table.scss b/src/legacy/ui/public/styles/_legacy/components/_table.scss deleted file mode 100644 index d0ac9d6f79862..0000000000000 --- a/src/legacy/ui/public/styles/_legacy/components/_table.scss +++ /dev/null @@ -1,46 +0,0 @@ -@import '../../../../../../plugins/discover/public/application/mixins'; - -.table { - // Nesting - .table { - background-color: $euiColorEmptyShade; - } -} - -kbn-table, .kbn-table, tbody[kbn-rows] { - @include dscDocSourceStyle; - // sub tables should not have a leading border - .table .table { - margin-bottom: 0; - - tr:first-child > td { - border-top: none; - } - - td.field-name { - font-weight: $euiFontWeightBold; - } - } -} - -table { - th { - i.fa-sort { - color: $euiColorLightShade; - } - - button.fa-sort-asc, - button.fa-sort-down, - i.fa-sort-asc, - i.fa-sort-down { - color: $euiColorPrimary; - } - - button.fa-sort-desc, - button.fa-sort-up, - i.fa-sort-desc, - i.fa-sort-up { - color: $euiColorPrimary; - } - } -} diff --git a/src/legacy/ui/public/styles/_legacy/components/_truncate.scss b/src/legacy/ui/public/styles/_legacy/components/_truncate.scss deleted file mode 100644 index 30ba5fea2a4ea..0000000000000 --- a/src/legacy/ui/public/styles/_legacy/components/_truncate.scss +++ /dev/null @@ -1,3 +0,0 @@ -.truncate-by-height { - overflow: hidden; -} diff --git a/src/legacy/ui/public/styles/_legacy/components/_ui_select.scss b/src/legacy/ui/public/styles/_legacy/components/_ui_select.scss deleted file mode 100644 index 691ec17b5b967..0000000000000 --- a/src/legacy/ui/public/styles/_legacy/components/_ui_select.scss +++ /dev/null @@ -1,357 +0,0 @@ -/*! - * ui-select - * http://github.com/angular-ui/ui-select - * Version: 0.19.5 - 2016-10-24T23:13:59.551Z - * License: MIT - */ - -/* Style when highlighting a search. */ -.ui-select-highlight { - font-weight: bold; -} - -.ui-select-offscreen { - clip: rect(0 0 0 0) !important; - width: 1px !important; - height: 1px !important; - border: 0 !important; - margin: 0 !important; - padding: 0 !important; - overflow: hidden !important; - position: absolute !important; - outline: 0 !important; - left: 0px !important; - top: 0px !important; -} - -.ui-select-choices-row:hover { - background-color: $euiColorLightestShade; -} - -/* Select2 theme */ - -/* Mark invalid Select2 */ -.ng-dirty.ng-invalid > a.select2-choice { - border-color: $euiColorDanger; -} - -.select2-result-single { - padding-left: 0; -} - -.select2-locked > .select2-search-choice-close { - display: none; -} - -.select-locked > .ui-select-match-close { - display: none; -} - -body > .select2-container.open { - z-index: 9999; /* The z-index Select2 applies to the select2-drop */ -} - -/* Handle up direction Select2 */ -.ui-select-container[theme='select2'].direction-up .ui-select-match, -.ui-select-container.select2.direction-up .ui-select-match { - border-radius: 4px; /* FIXME hardcoded value :-/ */ - border-top-left-radius: 0; - border-top-right-radius: 0; -} -.ui-select-container[theme='select2'].direction-up .ui-select-dropdown, -.ui-select-container.select2.direction-up .ui-select-dropdown { - border-radius: 4px; /* FIXME hardcoded value :-/ */ - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; - - border-top-width: 1px; /* FIXME hardcoded value :-/ */ - border-top-style: solid; - - box-shadow: 0 -4px 8px rgba(0, 0, 0, 0.25); - - margin-top: -4px; /* FIXME hardcoded value :-/ */ -} -.ui-select-container[theme='select2'].direction-up .ui-select-dropdown .select2-search, -.ui-select-container.select2.direction-up .ui-select-dropdown .select2-search { - margin-top: 4px; /* FIXME hardcoded value :-/ */ -} -.ui-select-container[theme='select2'].direction-up.select2-dropdown-open .ui-select-match, -.ui-select-container.select2.direction-up.select2-dropdown-open .ui-select-match { - border-bottom-color: $euiColorPrimary; -} - -.ui-select-container[theme='select2'] .ui-select-dropdown .ui-select-search-hidden, -.ui-select-container[theme='select2'] .ui-select-dropdown .ui-select-search-hidden input { - opacity: 0; - height: 0; - min-height: 0; - padding: 0; - margin: 0; - border: 0; -} - -/* Bootstrap theme */ - -/* Helper class to show styles when focus */ -.btn-default-focus { - color: $euiTextColor; - background-color: $euiColorEmptyShade; - border-color: $euiColorPrimary; - text-decoration: none; - outline: none; - box-shadow: none; -} - -.ui-select-bootstrap .ui-select-toggle { - @include __legacyInputStyles__bad; - @include __legacySelectStyles__bad; -} - -.ui-select-bootstrap .ui-select-toggle > .caret { - display: none; -} - -/* Fix Bootstrap dropdown position when inside a input-group */ -.input-group > .ui-select-bootstrap.dropdown { - /* Instead of relative */ - position: static; -} - -.input-group > .ui-select-bootstrap > input.ui-select-search.form-control { - border-radius: 4px; /* FIXME hardcoded value :-/ */ - border-top-right-radius: 0; - border-bottom-right-radius: 0; -} -.input-group > .ui-select-bootstrap > input.ui-select-search.form-control.direction-up { - border-radius: 4px !important; /* FIXME hardcoded value :-/ */ - border-top-right-radius: 0 !important; - border-bottom-right-radius: 0 !important; -} - -.ui-select-bootstrap .ui-select-search-hidden { - opacity: 0; - height: 0; - min-height: 0; - padding: 0; - margin: 0; - border: 0; -} - -.ui-select-bootstrap > .ui-select-match > .btn { - @include __legacyInputStyles__bad; - @include __legacySelectStyles__bad; - text-align: left !important; // Instead of center because of .btn - - .ui-select-placeholder { - color: $euiColorMediumShade; - } - - &:focus, - &:active { - background-color: $euiColorEmptyShade; - color: $euiTextColor; - outline: none; - } -} - -.ui-select-bootstrap > .ui-select-match > .caret { - display: none; -} - -/* See Scrollable Menu with Bootstrap 3 http://stackoverflow.com/questions/19227496 */ -.ui-select-bootstrap > .ui-select-choices, -.ui-select-bootstrap > .ui-select-no-choice { - width: 100%; - height: auto; - max-height: $euiSize * 14; - overflow-x: hidden; -} - -body > .ui-select-bootstrap.open { - z-index: $euiZContentMenu; -} - -.ui-select-multiple.ui-select-bootstrap { - height: auto; - padding: 3px 5px 2px; - border: $euiBorderThin; - background-color: $euiFormBackgroundColor; - - &.kuiInputError { - border-color: $euiColorDanger; - } -} - -.ui-select-multiple.ui-select-bootstrap input.ui-select-search { - background-color: transparent !important; /* To prevent double background when disabled */ - border: none; - outline: none; - height: 1.666666em; - margin-bottom: 3px; -} - -.ui-select-multiple.ui-select-bootstrap .ui-select-match .close { - font-size: 1.6em; - line-height: 0.75; -} - -.ui-select-multiple.ui-select-bootstrap .ui-select-match-item { - outline: 0; - margin: 0 3px 3px 0; -} - -.ui-select-multiple .ui-select-match-item { - position: relative; -} - -.ui-select-multiple .ui-select-match-item.dropping .ui-select-match-close { - pointer-events: none; -} - -.ui-select-multiple:hover .ui-select-match-item.dropping-before:before { - content: ''; - position: absolute; - top: 0; - right: 100%; - height: 100%; - margin-right: 2px; - border-left: 1px solid $euiColorPrimary; -} - -.ui-select-multiple:hover .ui-select-match-item.dropping-after:after { - content: ''; - position: absolute; - top: 0; - left: 100%; - height: 100%; - margin-left: 2px; - border-right: 1px solid $euiColorPrimary; -} - -.ui-select-bootstrap .ui-select-choices-row > span { - @include euiFontSizeS; - @include euiTextTruncate; - font-weight: inherit; - cursor: pointer; - display: block; - padding: $euiSizeXS $euiSize; - clear: both; - color: $euiTextColor; - white-space: nowrap; - - &:hover, - &:focus { - text-decoration: none; - color: $euiTextColor; - background-color: $euiFocusBackgroundColor; - } -} - -.ui-select-bootstrap .ui-select-choices-row.active > span { - color: $euiTextColor; - text-decoration: none; - outline: 0; - background-color: $euiFocusBackgroundColor; -} - -.ui-select-bootstrap .ui-select-choices-row.disabled > span, -.ui-select-bootstrap .ui-select-choices-row.active.disabled > span { - color: $euiButtonColorDisabled; - cursor: not-allowed; - background-color: transparent; -} - -/* fix hide/show angular animation */ -.ui-select-match.ng-hide-add, -.ui-select-search.ng-hide-add { - display: none !important; -} - -/* Mark invalid Bootstrap */ -.ui-select-bootstrap.ng-dirty.ng-invalid > button.btn.ui-select-match { - border-color: $euiColorDanger; -} - -/* Handle up direction Bootstrap */ -.ui-select-container[theme='bootstrap'].direction-up .ui-select-dropdown { - @include euiBottomShadowMedium; -} - -.ui-select-bootstrap .ui-select-match-text { - width: 100%; - padding-right: 1em; -} -.ui-select-bootstrap .ui-select-match-text span { - display: inline-block; - width: 100%; - overflow: hidden; -} -.ui-select-bootstrap .ui-select-toggle > a.btn { - position: absolute; - height: 10px; - right: 10px; - margin-top: -2px; -} - -/* Spinner */ -.ui-select-refreshing { - position: absolute; - right: 0; - padding: 8px 27px; - top: 1px; - display: inline-block; - font-family: 'Glyphicons Halflings'; - font-style: normal; - font-weight: normal; - line-height: 1; - -webkit-font-smoothing: antialiased; -} - -@-webkit-keyframes ui-select-spin { - 0% { - -webkit-transform: rotate(0deg); - transform: rotate(0deg); - } - 100% { - -webkit-transform: rotate(359deg); - transform: rotate(359deg); - } -} -@keyframes ui-select-spin { - 0% { - -webkit-transform: rotate(0deg); - transform: rotate(0deg); - } - 100% { - -webkit-transform: rotate(359deg); - transform: rotate(359deg); - } -} - -.ui-select-spin { - -webkit-animation: ui-select-spin 2s infinite linear; - animation: ui-select-spin 2s infinite linear; -} - -.ui-select-refreshing.ng-animate { - -webkit-animation: none 0s; -} - -// Other Custom - -/** - * 1. Fix appearance of ui-select in the Filtering UI. - */ -.btn-default .ui-select-placeholder { - color: $euiColorMediumShade; /* 1 */ -} - -.uiSelectMatch--ellipsis { - @include euiTextTruncate; -} - -.ui-select-choices-group-label { - @include euiTitle('xxxs'); - @include euiTextTruncate; - padding: $euiSizeXS; -} diff --git a/src/legacy/ui/public/styles/_mixins.scss b/src/legacy/ui/public/styles/_mixins.scss index c0dc456000dcc..e335ef88a01b5 100644 --- a/src/legacy/ui/public/styles/_mixins.scss +++ b/src/legacy/ui/public/styles/_mixins.scss @@ -105,12 +105,10 @@ @keyframes kibanaFullScreenGraphics_FadeIn { from { opacity: 0; - transform: translateY(200px), scale(.75); } to { opacity: 1; - transform: translateY(0), scale(1); } } } diff --git a/src/plugins/discover/public/application/_discover.scss b/src/plugins/discover/public/application/_discover.scss index 1aaa0a24357ed..69df2a75b8d75 100644 --- a/src/plugins/discover/public/application/_discover.scss +++ b/src/plugins/discover/public/application/_discover.scss @@ -103,3 +103,15 @@ discover-app { right: $euiSizeM; top: $euiSizeXS; } + +[fixed-scroll] { + overflow-x: auto; + padding-bottom: 0; + + + .fixed-scroll-scroller { + position: fixed; + bottom: 0; + overflow-x: auto; + overflow-y: hidden; + } +} diff --git a/src/plugins/discover/public/application/_hacks.scss b/src/plugins/discover/public/application/_hacks.scss deleted file mode 100644 index 9bbe9cd14fd91..0000000000000 --- a/src/plugins/discover/public/application/_hacks.scss +++ /dev/null @@ -1,4 +0,0 @@ -// SASSTODO: the classname is dynamically generated with ng-class -.tab-discover { - overflow: hidden; -} diff --git a/src/plugins/discover/public/application/_mixins.scss b/src/plugins/discover/public/application/_mixins.scss deleted file mode 100644 index 100f81ae92bf0..0000000000000 --- a/src/plugins/discover/public/application/_mixins.scss +++ /dev/null @@ -1,27 +0,0 @@ -/** -* Style ES document _source in table view
key:
value
-* Use alpha so this will stand out against non-white backgrounds, e.g. the highlighted -* row in the Context Log. -*/ -@mixin dscDocSourceStyle { - dl.source { - margin-bottom: 0; - line-height:2em; - word-break: break-word; - - dt, dd { - display: inline; - } - - dt { - background-color: transparentize(shade($euiColorPrimary, 20%), .9); - color: $euiTextColor; - padding: ($euiSizeXS / 2) $euiSizeXS; - margin-right: $euiSizeXS; - word-break: normal; - border-radius: $euiBorderRadius; - } - } -} - - diff --git a/src/plugins/discover/public/application/angular/doc_table/_doc_table.scss b/src/plugins/discover/public/application/angular/doc_table/_doc_table.scss index 3e30214acd2a9..7d05171622e7b 100644 --- a/src/plugins/discover/public/application/angular/doc_table/_doc_table.scss +++ b/src/plugins/discover/public/application/angular/doc_table/_doc_table.scss @@ -27,7 +27,6 @@ doc-table { } .kbnDocTable { - @include dscDocSourceStyle; font-size: $euiFontSizeXS; th { @@ -40,6 +39,35 @@ doc-table { } } +.kbn-table, +.kbnDocTable { + /** + * Style ES document _source in table view
key:
value
+ * Use alpha so this will stand out against non-white backgrounds, e.g. the highlighted + * row in the Context Log. + */ + + dl.source { + margin-bottom: 0; + line-height: 2em; + word-break: break-word; + + dt, + dd { + display: inline; + } + + dt { + background-color: transparentize(shade($euiColorPrimary, 20%), 0.9); + color: $euiTextColor; + padding: ($euiSizeXS / 2) $euiSizeXS; + margin-right: $euiSizeXS; + word-break: normal; + border-radius: $euiBorderRadius; + } + } +} + .kbnDocTable__row { td { position: relative; @@ -80,3 +108,50 @@ doc-table { text-align: center; } +.truncate-by-height { + overflow: hidden; +} + +.table { + // Nesting + .table { + background-color: $euiColorEmptyShade; + } +} + +.kbn-table { + // sub tables should not have a leading border + .table .table { + margin-bottom: 0; + + tr:first-child > td { + border-top: none; + } + + td.field-name { + font-weight: $euiFontWeightBold; + } + } +} + +table { + th { + i.fa-sort { + color: $euiColorLightShade; + } + + button.fa-sort-asc, + button.fa-sort-down, + i.fa-sort-asc, + i.fa-sort-down { + color: $euiColorPrimary; + } + + button.fa-sort-desc, + button.fa-sort-up, + i.fa-sort-desc, + i.fa-sort-up { + color: $euiColorPrimary; + } + } +} diff --git a/src/plugins/discover/public/application/angular/doc_table/index.scss b/src/plugins/discover/public/application/angular/doc_table/index.scss index 4e6cb83c5fe5a..3663d807851c4 100644 --- a/src/plugins/discover/public/application/angular/doc_table/index.scss +++ b/src/plugins/discover/public/application/angular/doc_table/index.scss @@ -1,4 +1,2 @@ -@import '../../mixins'; - @import 'doc_table'; @import 'components/index'; diff --git a/src/plugins/discover/public/application/index.scss b/src/plugins/discover/public/application/index.scss index aaec7ab387e96..5aa353828274c 100644 --- a/src/plugins/discover/public/application/index.scss +++ b/src/plugins/discover/public/application/index.scss @@ -1,13 +1,2 @@ -// Discover plugin styles -@import 'mixins'; -@import 'discover'; -@import 'hacks'; - -// Prefix all styles with "dsc" to avoid conflicts. -// Examples -// dscTable -// dscTable__footer -// monChart__legend--small -// monChart__legend-isLoading - @import 'angular/index'; +@import 'discover'; diff --git a/src/plugins/kibana_legacy/public/paginate/_paginate.scss b/src/plugins/kibana_legacy/public/paginate/_paginate.scss new file mode 100644 index 0000000000000..e9c1acaf9ee0d --- /dev/null +++ b/src/plugins/kibana_legacy/public/paginate/_paginate.scss @@ -0,0 +1,57 @@ +paginate { + display: block; + + paginate-controls { + display: flex; + align-items: center; + padding: $euiSizeXS $euiSizeXS $euiSizeS; + text-align: center; + + .pagination-other-pages { + flex: 1 0 auto; + display: flex; + justify-content: center; + } + + .pagination-other-pages-list { + flex: 0 0 auto; + display: flex; + justify-content: center; + padding: 0; + margin: 0; + list-style: none; + + > li { + flex: 0 0 auto; + user-select: none; + + a { + text-decoration: none; + background-color: $euiColorLightestShade; + margin-left: $euiSizeXS / 2; + padding: $euiSizeS $euiSizeM; + } + + a:hover { + text-decoration: underline; + } + + &.active a { + text-decoration: none !important; + font-weight: $euiFontWeightBold; + color: $euiColorDarkShade; + cursor: default; + } + } + } + + .pagination-size { + flex: 0 0 auto; + + input[type=number] { + width: 3em; + } + } + } +} + diff --git a/src/plugins/kibana_legacy/public/paginate/paginate.js b/src/plugins/kibana_legacy/public/paginate/paginate.js index ea93a969d08c7..f424c33ba7b02 100644 --- a/src/plugins/kibana_legacy/public/paginate/paginate.js +++ b/src/plugins/kibana_legacy/public/paginate/paginate.js @@ -19,6 +19,7 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; +import './_paginate.scss'; import paginateControlsTemplate from './paginate_controls.html'; export function PaginateDirectiveProvider($parse, $compile) { diff --git a/src/plugins/timelion/public/_base.scss b/src/plugins/timelion/public/_base.scss new file mode 100644 index 0000000000000..616ac9b3486e7 --- /dev/null +++ b/src/plugins/timelion/public/_base.scss @@ -0,0 +1,19 @@ +// Angular form states +.ng-invalid { + &.ng-dirty, + &.ng-touched { + border-color: $euiColorDanger; + } +} + +input[type='radio'], +input[type='checkbox'], +.radio, +.checkbox { + &[disabled], + fieldset[disabled] & { + cursor: default; + opacity: .8; + } +} + diff --git a/src/legacy/ui/public/styles/_legacy/_mixins.scss b/src/plugins/timelion/public/directives/_form.scss similarity index 58% rename from src/legacy/ui/public/styles/_legacy/_mixins.scss rename to src/plugins/timelion/public/directives/_form.scss index 2834f60555070..3fcf70700a864 100644 --- a/src/legacy/ui/public/styles/_legacy/_mixins.scss +++ b/src/plugins/timelion/public/directives/_form.scss @@ -1,23 +1,19 @@ -// These mixins are temporary helpers to consolidate styles of elements that -// are not yet converted to use EUI. - -// DO NOT CONTINUE TO USE THESE MIXINS - -@mixin __legacyInputStyles__bad { - &:not([type='range']) { - appearance: none; - } +.form-control { + @include euiFontSizeS; display: block; width: 100%; height: $euiFormControlCompressedHeight; padding: $euiSizeXS $euiSizeM; - @include euiFontSizeS; border: $euiBorderThin; background-color: $euiFormBackgroundColor; color: $euiTextColor; border-radius: $euiBorderRadius; cursor: pointer; + &:not([type='range']) { + appearance: none; + } + &:focus { border-color: $euiColorPrimary; outline: none; @@ -25,30 +21,16 @@ } } -@mixin __legacySelectStyles__bad { +// sass-lint:disable-block no-qualifying-elements +select.form-control { // Makes the select arrow similar to EUI's arrowDown icon - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3E%3Cpath fill='#{hexToRGB($euiTextColor)}' d='M13.0688508,5.15725038 L8.38423975,9.76827428 C8.17054415,9.97861308 7.82999214,9.97914095 7.61576025,9.76827428 L2.93114915,5.15725038 C2.7181359,4.94758321 2.37277319,4.94758321 2.15975994,5.15725038 C1.94674669,5.36691756 1.94674669,5.70685522 2.15975994,5.9165224 L6.84437104,10.5275463 C7.48517424,11.1582836 8.51644979,11.1566851 9.15562896,10.5275463 L13.8402401,5.9165224 C14.0532533,5.70685522 14.0532533,5.36691756 13.8402401,5.15725038 C13.6272268,4.94758321 13.2818641,4.94758321 13.0688508,5.15725038 Z'/%3E%3C/svg%3E"); + background-image: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"%3E%3Cpath fill="#{hexToRGB($euiTextColor)}" d="M13.0688508,5.15725038 L8.38423975,9.76827428 C8.17054415,9.97861308 7.82999214,9.97914095 7.61576025,9.76827428 L2.93114915,5.15725038 C2.7181359,4.94758321 2.37277319,4.94758321 2.15975994,5.15725038 C1.94674669,5.36691756 1.94674669,5.70685522 2.15975994,5.9165224 L6.84437104,10.5275463 C7.48517424,11.1582836 8.51644979,11.1566851 9.15562896,10.5275463 L13.8402401,5.9165224 C14.0532533,5.70685522 14.0532533,5.36691756 13.8402401,5.15725038 C13.6272268,4.94758321 13.2818641,4.94758321 13.0688508,5.15725038 Z"/%3E%3C/svg%3E'); background-size: $euiSize; background-repeat: no-repeat; background-position: calc(100% - #{$euiSizeS}); padding-right: $euiSizeXL; - - &::-ms-expand { - display: none; - } - - &:focus::-ms-value { - color: $euiTextColor; - background: transparent; - } } -@mixin __legacyLabelStyles__bad { - font-size: $euiFontSizeXS; - font-weight: $euiFontWeightSemiBold; - color: $euiTextColor; - - &[for] { - cursor: pointer; - } +.fullWidth { + width: 100%; } diff --git a/src/plugins/timelion/public/directives/_index.scss b/src/plugins/timelion/public/directives/_index.scss index cd46a1a0a369e..a407c1dfabdeb 100644 --- a/src/plugins/timelion/public/directives/_index.scss +++ b/src/plugins/timelion/public/directives/_index.scss @@ -3,3 +3,6 @@ @import './timelion_expression_suggestions/index'; @import './timelion_help/index'; @import './timelion_interval/index'; +@import './saved_object_finder'; +@import './form'; + diff --git a/src/legacy/ui/public/styles/_legacy/components/_list_group_menu.scss b/src/plugins/timelion/public/directives/_saved_object_finder.scss similarity index 85% rename from src/legacy/ui/public/styles/_legacy/components/_list_group_menu.scss rename to src/plugins/timelion/public/directives/_saved_object_finder.scss index 3bac75cb19d9d..b97dace5e9e00 100644 --- a/src/legacy/ui/public/styles/_legacy/components/_list_group_menu.scss +++ b/src/plugins/timelion/public/directives/_saved_object_finder.scss @@ -1,7 +1,7 @@ .list-group-menu { &.select-mode a { - outline: none; - color: tintOrShade($euiColorPrimary, 10%, 10%); + outline: none; + color: tintOrShade($euiColorPrimary, 10%, 10%); } .list-group-menu-item { @@ -12,9 +12,11 @@ font-weight: bold; background-color: $euiColorLightShade; } + &:hover { background-color: tintOrShade($euiColorPrimary, 90%, 90%); } + li { list-style: none; color: tintOrShade($euiColorPrimary, 10%, 10%); diff --git a/src/plugins/timelion/public/index.scss b/src/plugins/timelion/public/index.scss index cf2a7859a505d..6bf7133287c51 100644 --- a/src/plugins/timelion/public/index.scss +++ b/src/plugins/timelion/public/index.scss @@ -8,4 +8,5 @@ // timChart__legend-isLoading @import './app'; +@import './base'; @import './directives/index'; diff --git a/src/plugins/vis_type_table/public/agg_table/_agg_table.scss b/src/plugins/vis_type_table/public/agg_table/_agg_table.scss index 0fffb21eab0fb..4bbc4eb034f8d 100644 --- a/src/plugins/vis_type_table/public/agg_table/_agg_table.scss +++ b/src/plugins/vis_type_table/public/agg_table/_agg_table.scss @@ -36,3 +36,7 @@ kbn-agg-table-group { padding: 0; } } + +.small { + font-size: 0.9em !important; +} diff --git a/x-pack/plugins/ml/public/application/_hacks.scss b/x-pack/plugins/ml/public/application/_hacks.scss index 39740360d8a84..63fec4e74b796 100644 --- a/x-pack/plugins/ml/public/application/_hacks.scss +++ b/x-pack/plugins/ml/public/application/_hacks.scss @@ -18,42 +18,6 @@ cursor: not-allowed; } -// ML bootstrap-select hacks that sit on top of Kibana hacks that often fight with KUI -// Should go away when EUI is fully adopted -.ui-select-match { - .btn-default[disabled], - .btn-default[disabled]:hover, - .btn-default[disabled]:focus { - background-color: $euiColorLightShade; - border-color: $euiColorLightShade; - opacity: 1; - - .ui-select-placeholder { - color: $euiColorDarkShade; - } - } - - .btn { - border: 1px solid $euiColorLightShade; - background-color: $euiColorEmptyShade; - color: $euiColorDarkestShade; - } - } - -.ui-select-container input[type="search"]::placeholder { - color: $euiColorDarkShade; -} - -.ui-select-container input[type="search"]:focus { - box-shadow: none; -} - -.ui-select-multiple.ui-select-bootstrap input.ui-select-search { - font-size: $euiFontSizeS; - padding: 5px 10px; // Matches current padding hacks from other parts of Kibana -} - - // SASSTODO: Remove all the floats .clear, .clearfix { clear: both; diff --git a/x-pack/plugins/reporting/server/lib/layouts/preserve_layout.css b/x-pack/plugins/reporting/server/lib/layouts/preserve_layout.css index 0ea9f3079de82..12ac5b27c7a4a 100644 --- a/x-pack/plugins/reporting/server/lib/layouts/preserve_layout.css +++ b/x-pack/plugins/reporting/server/lib/layouts/preserve_layout.css @@ -37,7 +37,6 @@ filter-bar, discover-app .dscTimechart, discover-app .dscSidebar__container, discover-app .kbnCollapsibleSidebar__collapseButton, -discover-app navbar[name=discover-search], discover-app .discover-table-footer { display: none; } diff --git a/x-pack/plugins/reporting/server/lib/layouts/print.css b/x-pack/plugins/reporting/server/lib/layouts/print.css index 4f1e3f4e5abd0..9b07e3c923138 100644 --- a/x-pack/plugins/reporting/server/lib/layouts/print.css +++ b/x-pack/plugins/reporting/server/lib/layouts/print.css @@ -36,7 +36,6 @@ filter-bar, discover-app .dscTimechart, discover-app .dscSidebar__container, discover-app .kbnCollapsibleSidebar__collapseButton, -discover-app navbar[name="discover-search"], discover-app .discover-table-footer { display: none; } From b29e8ee9c746dd2eff95d950ae4dbca564b11441 Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Mon, 20 Jul 2020 10:55:44 +0200 Subject: [PATCH 17/45] migrate retryCallCluster for new ES client (#71412) * adapt retryCallCluster for new ES client * review comments * retry on 408 ResponseError * use error name instead of instanceof base check * use error name instead of instanceof base check bis * use mockImplementationOnce chaining Co-authored-by: restrry --- src/core/server/elasticsearch/client/index.ts | 1 + .../client/retry_call_cluster.test.ts | 283 ++++++++++++++++++ .../client/retry_call_cluster.ts | 103 +++++++ .../legacy/retry_call_cluster.ts | 4 +- 4 files changed, 389 insertions(+), 2 deletions(-) create mode 100644 src/core/server/elasticsearch/client/retry_call_cluster.test.ts create mode 100644 src/core/server/elasticsearch/client/retry_call_cluster.ts diff --git a/src/core/server/elasticsearch/client/index.ts b/src/core/server/elasticsearch/client/index.ts index 18e84482024ca..b8125de2ee498 100644 --- a/src/core/server/elasticsearch/client/index.ts +++ b/src/core/server/elasticsearch/client/index.ts @@ -22,3 +22,4 @@ export { IScopedClusterClient, ScopedClusterClient } from './scoped_cluster_clie export { ElasticsearchClientConfig } from './client_config'; export { IClusterClient, ICustomClusterClient, ClusterClient } from './cluster_client'; export { configureClient } from './configure_client'; +export { retryCallCluster, migrationRetryCallCluster } from './retry_call_cluster'; diff --git a/src/core/server/elasticsearch/client/retry_call_cluster.test.ts b/src/core/server/elasticsearch/client/retry_call_cluster.test.ts new file mode 100644 index 0000000000000..a7177c0b29047 --- /dev/null +++ b/src/core/server/elasticsearch/client/retry_call_cluster.test.ts @@ -0,0 +1,283 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { errors } from '@elastic/elasticsearch'; +import { elasticsearchClientMock } from './mocks'; +import { loggingSystemMock } from '../../logging/logging_system.mock'; +import { retryCallCluster, migrationRetryCallCluster } from './retry_call_cluster'; + +const dummyBody = { foo: 'bar' }; +const createErrorReturn = (err: any) => elasticsearchClientMock.createClientError(err); + +describe('retryCallCluster', () => { + let client: ReturnType; + + beforeEach(() => { + client = elasticsearchClientMock.createElasticSearchClient(); + }); + + it('returns response from ES API call in case of success', async () => { + const successReturn = elasticsearchClientMock.createClientResponse({ ...dummyBody }); + + client.asyncSearch.get.mockReturnValue(successReturn); + + const result = await retryCallCluster(() => client.asyncSearch.get()); + expect(result.body).toEqual(dummyBody); + }); + + it('retries ES API calls that rejects with `NoLivingConnectionsError`', async () => { + const successReturn = elasticsearchClientMock.createClientResponse({ ...dummyBody }); + + client.asyncSearch.get + .mockImplementationOnce(() => + createErrorReturn(new errors.NoLivingConnectionsError('no living connections', {} as any)) + ) + .mockImplementationOnce(() => successReturn); + + const result = await retryCallCluster(() => client.asyncSearch.get()); + expect(result.body).toEqual(dummyBody); + }); + + it('rejects when ES API calls reject with other errors', async () => { + client.ping + .mockImplementationOnce(() => createErrorReturn(new Error('unknown error'))) + .mockImplementationOnce(() => elasticsearchClientMock.createClientResponse({ ...dummyBody })); + + await expect(retryCallCluster(() => client.ping())).rejects.toMatchInlineSnapshot( + `[Error: unknown error]` + ); + }); + + it('stops retrying when ES API calls reject with other errors', async () => { + client.ping + .mockImplementationOnce(() => + createErrorReturn(new errors.NoLivingConnectionsError('no living connections', {} as any)) + ) + .mockImplementationOnce(() => + createErrorReturn(new errors.NoLivingConnectionsError('no living connections', {} as any)) + ) + .mockImplementationOnce(() => createErrorReturn(new Error('unknown error'))) + .mockImplementationOnce(() => elasticsearchClientMock.createClientResponse({ ...dummyBody })); + + await expect(retryCallCluster(() => client.ping())).rejects.toMatchInlineSnapshot( + `[Error: unknown error]` + ); + }); +}); + +describe('migrationRetryCallCluster', () => { + let client: ReturnType; + let logger: ReturnType; + + beforeEach(() => { + client = elasticsearchClientMock.createElasticSearchClient(); + logger = loggingSystemMock.createLogger(); + }); + + const mockClientPingWithErrorBeforeSuccess = (error: any) => { + client.ping + .mockImplementationOnce(() => createErrorReturn(error)) + .mockImplementationOnce(() => createErrorReturn(error)) + .mockImplementationOnce(() => elasticsearchClientMock.createClientResponse({ ...dummyBody })); + }; + + it('retries ES API calls that rejects with `NoLivingConnectionsError`', async () => { + mockClientPingWithErrorBeforeSuccess( + new errors.NoLivingConnectionsError('no living connections', {} as any) + ); + + const result = await migrationRetryCallCluster(() => client.ping(), logger, 1); + expect(result.body).toEqual(dummyBody); + }); + + it('retries ES API calls that rejects with `ConnectionError`', async () => { + mockClientPingWithErrorBeforeSuccess(new errors.ConnectionError('connection error', {} as any)); + + const result = await migrationRetryCallCluster(() => client.ping(), logger, 1); + expect(result.body).toEqual(dummyBody); + }); + + it('retries ES API calls that rejects with `TimeoutError`', async () => { + mockClientPingWithErrorBeforeSuccess(new errors.TimeoutError('timeout error', {} as any)); + + const result = await migrationRetryCallCluster(() => client.ping(), logger, 1); + expect(result.body).toEqual(dummyBody); + }); + + it('retries ES API calls that rejects with 503 `ResponseError`', async () => { + mockClientPingWithErrorBeforeSuccess( + new errors.ResponseError({ + statusCode: 503, + } as any) + ); + + const result = await migrationRetryCallCluster(() => client.ping(), logger, 1); + expect(result.body).toEqual(dummyBody); + }); + + it('retries ES API calls that rejects 401 `ResponseError`', async () => { + mockClientPingWithErrorBeforeSuccess( + new errors.ResponseError({ + statusCode: 401, + } as any) + ); + + const result = await migrationRetryCallCluster(() => client.ping(), logger, 1); + expect(result.body).toEqual(dummyBody); + }); + + it('retries ES API calls that rejects with 403 `ResponseError`', async () => { + mockClientPingWithErrorBeforeSuccess( + new errors.ResponseError({ + statusCode: 403, + } as any) + ); + + const result = await migrationRetryCallCluster(() => client.ping(), logger, 1); + expect(result.body).toEqual(dummyBody); + }); + + it('retries ES API calls that rejects with 408 `ResponseError`', async () => { + mockClientPingWithErrorBeforeSuccess( + new errors.ResponseError({ + statusCode: 408, + } as any) + ); + + const result = await migrationRetryCallCluster(() => client.ping(), logger, 1); + expect(result.body).toEqual(dummyBody); + }); + + it('retries ES API calls that rejects with 410 `ResponseError`', async () => { + mockClientPingWithErrorBeforeSuccess( + new errors.ResponseError({ + statusCode: 410, + } as any) + ); + + const result = await migrationRetryCallCluster(() => client.ping(), logger, 1); + expect(result.body).toEqual(dummyBody); + }); + + it('retries ES API calls that rejects with `snapshot_in_progress_exception` `ResponseError`', async () => { + mockClientPingWithErrorBeforeSuccess( + new errors.ResponseError({ + statusCode: 500, + body: { + error: { + type: 'snapshot_in_progress_exception', + }, + }, + } as any) + ); + + const result = await migrationRetryCallCluster(() => client.ping(), logger, 1); + expect(result.body).toEqual(dummyBody); + }); + + it('logs only once for each unique error message', async () => { + client.ping + .mockImplementationOnce(() => + createErrorReturn( + new errors.ResponseError({ + statusCode: 503, + } as any) + ) + ) + .mockImplementationOnce(() => + createErrorReturn(new errors.ConnectionError('connection error', {} as any)) + ) + .mockImplementationOnce(() => + createErrorReturn( + new errors.ResponseError({ + statusCode: 503, + } as any) + ) + ) + .mockImplementationOnce(() => + createErrorReturn(new errors.ConnectionError('connection error', {} as any)) + ) + .mockImplementationOnce(() => + createErrorReturn( + new errors.ResponseError({ + statusCode: 500, + body: { + error: { + type: 'snapshot_in_progress_exception', + }, + }, + } as any) + ) + ) + .mockImplementationOnce(() => elasticsearchClientMock.createClientResponse({ ...dummyBody })); + + await migrationRetryCallCluster(() => client.ping(), logger, 1); + + expect(loggingSystemMock.collect(logger).warn).toMatchInlineSnapshot(` + Array [ + Array [ + "Unable to connect to Elasticsearch. Error: Response Error", + ], + Array [ + "Unable to connect to Elasticsearch. Error: connection error", + ], + Array [ + "Unable to connect to Elasticsearch. Error: snapshot_in_progress_exception", + ], + ] + `); + }); + + it('rejects when ES API calls reject with other errors', async () => { + client.ping + .mockImplementationOnce(() => + createErrorReturn( + new errors.ResponseError({ + statusCode: 418, + body: { + error: { + type: `I'm a teapot`, + }, + }, + } as any) + ) + ) + .mockImplementationOnce(() => elasticsearchClientMock.createClientResponse({ ...dummyBody })); + + await expect( + migrationRetryCallCluster(() => client.ping(), logger, 1) + ).rejects.toMatchInlineSnapshot(`[ResponseError: I'm a teapot]`); + }); + + it('stops retrying when ES API calls reject with other errors', async () => { + client.ping + .mockImplementationOnce(() => + createErrorReturn(new errors.TimeoutError('timeout error', {} as any)) + ) + .mockImplementationOnce(() => + createErrorReturn(new errors.TimeoutError('timeout error', {} as any)) + ) + .mockImplementationOnce(() => createErrorReturn(new Error('unknown error'))) + .mockImplementationOnce(() => elasticsearchClientMock.createClientResponse({ ...dummyBody })); + + await expect( + migrationRetryCallCluster(() => client.ping(), logger, 1) + ).rejects.toMatchInlineSnapshot(`[Error: unknown error]`); + }); +}); diff --git a/src/core/server/elasticsearch/client/retry_call_cluster.ts b/src/core/server/elasticsearch/client/retry_call_cluster.ts new file mode 100644 index 0000000000000..1ad039e512215 --- /dev/null +++ b/src/core/server/elasticsearch/client/retry_call_cluster.ts @@ -0,0 +1,103 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { defer, throwError, iif, timer } from 'rxjs'; +import { concatMap, retryWhen } from 'rxjs/operators'; +import { Logger } from '../../logging'; + +const retryResponseStatuses = [ + 503, // ServiceUnavailable + 401, // AuthorizationException + 403, // AuthenticationException + 408, // RequestTimeout + 410, // Gone +]; + +/** + * Retries the provided Elasticsearch API call when a `NoLivingConnectionsError` error is + * encountered. The API call will be retried once a second, indefinitely, until + * a successful response or a different error is received. + * + * @example + * ```ts + * const response = await retryCallCluster(() => client.ping()); + * ``` + * + * @internal + */ +export const retryCallCluster = >(apiCaller: () => T): T => { + return defer(() => apiCaller()) + .pipe( + retryWhen((errors) => + errors.pipe( + concatMap((error) => + iif(() => error.name === 'NoLivingConnectionsError', timer(1000), throwError(error)) + ) + ) + ) + ) + .toPromise() as T; +}; + +/** + * Retries the provided Elasticsearch API call when an error such as + * `AuthenticationException` `NoConnections`, `ConnectionFault`, + * `ServiceUnavailable` or `RequestTimeout` are encountered. The API call will + * be retried once a second, indefinitely, until a successful response or a + * different error is received. + * + * @example + * ```ts + * const response = await migrationRetryCallCluster(() => client.ping(), logger); + * ``` + * + * @internal + */ +export const migrationRetryCallCluster = >( + apiCaller: () => T, + log: Logger, + delay: number = 2500 +): T => { + const previousErrors: string[] = []; + return defer(() => apiCaller()) + .pipe( + retryWhen((errors) => + errors.pipe( + concatMap((error) => { + if (!previousErrors.includes(error.message)) { + log.warn(`Unable to connect to Elasticsearch. Error: ${error.message}`); + previousErrors.push(error.message); + } + return iif( + () => + error.name === 'NoLivingConnectionsError' || + error.name === 'ConnectionError' || + error.name === 'TimeoutError' || + (error.name === 'ResponseError' && + retryResponseStatuses.includes(error.statusCode)) || + error?.body?.error?.type === 'snapshot_in_progress_exception', + timer(delay), + throwError(error) + ); + }) + ) + ) + ) + .toPromise() as T; +}; diff --git a/src/core/server/elasticsearch/legacy/retry_call_cluster.ts b/src/core/server/elasticsearch/legacy/retry_call_cluster.ts index 475a76d406017..1b05cb2bf13cd 100644 --- a/src/core/server/elasticsearch/legacy/retry_call_cluster.ts +++ b/src/core/server/elasticsearch/legacy/retry_call_cluster.ts @@ -53,7 +53,7 @@ export function migrationsRetryCallCluster( .pipe( retryWhen((error$) => error$.pipe( - concatMap((error, i) => { + concatMap((error) => { if (!previousErrors.includes(error.message)) { log.warn(`Unable to connect to Elasticsearch. Error: ${error.message}`); previousErrors.push(error.message); @@ -100,7 +100,7 @@ export function retryCallCluster(apiCaller: LegacyAPICaller) { .pipe( retryWhen((errors) => errors.pipe( - concatMap((error, i) => + concatMap((error) => iif( () => error instanceof legacyElasticsearch.errors.NoConnections, timer(1000), From e2fff84cace96dcb43e518a866d0c80170619a46 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Mon, 20 Jul 2020 09:59:16 +0100 Subject: [PATCH 18/45] added valifation of the alerting privileges at feature level --- .../features/server/feature_registry.test.ts | 162 ++++++++++++++++++ .../plugins/features/server/feature_schema.ts | 37 +++- 2 files changed, 198 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/features/server/feature_registry.test.ts b/x-pack/plugins/features/server/feature_registry.test.ts index 75022922917b3..f123068e41758 100644 --- a/x-pack/plugins/features/server/feature_registry.test.ts +++ b/x-pack/plugins/features/server/feature_registry.test.ts @@ -743,6 +743,168 @@ describe('FeatureRegistry', () => { ); }); + it(`prevents privileges from specifying alerting entries that don't exist at the root level`, () => { + const feature: FeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + alerting: ['bar'], + privileges: { + all: { + alerting: { + all: ['foo', 'bar'], + read: ['baz'], + }, + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], + }, + read: { + alerting: { read: ['foo', 'bar', 'baz'] }, + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], + }, + }, + }; + + const featureRegistry = new FeatureRegistry(); + + expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot( + `"Feature privilege test-feature.all has unknown alerting entries: foo, baz"` + ); + }); + + it(`prevents features from specifying alerting entries that don't exist at the privilege level`, () => { + const feature: FeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + alerting: ['foo', 'bar', 'baz'], + privileges: { + all: { + alerting: { all: ['foo'] }, + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], + }, + read: { + alerting: { all: ['foo'] }, + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], + }, + }, + subFeatures: [ + { + name: 'my sub feature', + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + id: 'cool-sub-feature-privilege', + name: 'cool privilege', + includeIn: 'none', + savedObject: { + all: [], + read: [], + }, + ui: [], + alerting: { all: ['bar'] }, + }, + ], + }, + ], + }, + ], + }; + + const featureRegistry = new FeatureRegistry(); + + expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot( + `"Feature test-feature specifies alerting entries which are not granted to any privileges: baz"` + ); + }); + + it(`prevents reserved privileges from specifying alerting entries that don't exist at the root level`, () => { + const feature: FeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + alerting: ['bar'], + privileges: null, + reserved: { + description: 'something', + privileges: [ + { + id: 'reserved', + privilege: { + alerting: { all: ['foo', 'bar', 'baz'] }, + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], + }, + }, + ], + }, + }; + + const featureRegistry = new FeatureRegistry(); + + expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot( + `"Feature privilege test-feature.reserved has unknown alerting entries: foo, baz"` + ); + }); + + it(`prevents features from specifying alerting entries that don't exist at the reserved privilege level`, () => { + const feature: FeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + alerting: ['foo', 'bar', 'baz'], + privileges: null, + reserved: { + description: 'something', + privileges: [ + { + id: 'reserved', + privilege: { + alerting: { all: ['foo', 'bar'] }, + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], + }, + }, + ], + }, + }; + + const featureRegistry = new FeatureRegistry(); + + expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot( + `"Feature test-feature specifies alerting entries which are not granted to any privileges: baz"` + ); + }); + it(`prevents privileges from specifying management sections that don't exist at the root level`, () => { const feature: FeatureConfig = { id: 'test-feature', diff --git a/x-pack/plugins/features/server/feature_schema.ts b/x-pack/plugins/features/server/feature_schema.ts index 15ddbd9334c8d..95298603d706a 100644 --- a/x-pack/plugins/features/server/feature_schema.ts +++ b/x-pack/plugins/features/server/feature_schema.ts @@ -51,6 +51,10 @@ const subFeaturePrivilegeSchema = Joi.object({ includeIn: Joi.string().allow('all', 'read', 'none').required(), management: managementSchema, catalogue: catalogueSchema, + alerting: Joi.object({ + all: alertingSchema, + read: alertingSchema, + }), api: Joi.array().items(Joi.string()), app: Joi.array().items(Joi.string()), savedObject: Joi.object({ @@ -119,7 +123,7 @@ export function validateFeature(feature: FeatureConfig) { throw validateResult.error; } // the following validation can't be enforced by the Joi schema, since it'd require us looking "up" the object graph for the list of valid value, which they explicitly forbid. - const { app = [], management = {}, catalogue = [] } = feature; + const { app = [], management = {}, catalogue = [], alerting = [] } = feature; const unseenApps = new Set(app); @@ -132,6 +136,8 @@ export function validateFeature(feature: FeatureConfig) { const unseenCatalogue = new Set(catalogue); + const unseenAlertTypes = new Set(alerting); + function validateAppEntry(privilegeId: string, entry: readonly string[] = []) { entry.forEach((privilegeApp) => unseenApps.delete(privilegeApp)); @@ -158,6 +164,23 @@ export function validateFeature(feature: FeatureConfig) { } } + function validateAlertingEntry(privilegeId: string, entry: FeatureKibanaPrivileges['alerting']) { + const all = entry?.all ?? []; + const read = entry?.read ?? []; + + all.forEach((privilegeAlertTypes) => unseenAlertTypes.delete(privilegeAlertTypes)); + read.forEach((privilegeAlertTypes) => unseenAlertTypes.delete(privilegeAlertTypes)); + + const unknownAlertingEntries = difference([...all, ...read], alerting); + if (unknownAlertingEntries.length > 0) { + throw new Error( + `Feature privilege ${ + feature.id + }.${privilegeId} has unknown alerting entries: ${unknownAlertingEntries.join(', ')}` + ); + } + } + function validateManagementEntry( privilegeId: string, managementEntry: Record = {} @@ -218,6 +241,7 @@ export function validateFeature(feature: FeatureConfig) { validateCatalogueEntry(privilegeId, privilegeDefinition.catalogue); validateManagementEntry(privilegeId, privilegeDefinition.management); + validateAlertingEntry(privilegeId, privilegeDefinition.alerting); }); const subFeatureEntries = feature.subFeatures ?? []; @@ -227,6 +251,7 @@ export function validateFeature(feature: FeatureConfig) { validateAppEntry(subFeaturePrivilege.id, subFeaturePrivilege.app); validateCatalogueEntry(subFeaturePrivilege.id, subFeaturePrivilege.catalogue); validateManagementEntry(subFeaturePrivilege.id, subFeaturePrivilege.management); + validateAlertingEntry(subFeaturePrivilege.id, subFeaturePrivilege.alerting); }); }); }); @@ -267,4 +292,14 @@ export function validateFeature(feature: FeatureConfig) { )}` ); } + + if (unseenAlertTypes.size > 0) { + throw new Error( + `Feature ${ + feature.id + } specifies alerting entries which are not granted to any privileges: ${Array.from( + unseenAlertTypes.values() + ).join(',')}` + ); + } } From d7ecd8675ef496b731a00f0953066d0a0426be63 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Mon, 20 Jul 2020 10:26:11 +0100 Subject: [PATCH 19/45] corrected security check for rbac --- .../actions_authorization.test.ts | 18 +++---- .../authorization/actions_authorization.ts | 12 ++--- x-pack/plugins/actions/server/plugin.ts | 1 - .../alerts/server/alerts_client_factory.ts | 1 - .../alerts_authorization.test.ts | 51 +++++++------------ .../authorization/alerts_authorization.ts | 8 +-- 6 files changed, 30 insertions(+), 61 deletions(-) diff --git a/x-pack/plugins/actions/server/authorization/actions_authorization.test.ts b/x-pack/plugins/actions/server/authorization/actions_authorization.test.ts index 4fded7b9e1bce..a48124cdbcb6a 100644 --- a/x-pack/plugins/actions/server/authorization/actions_authorization.test.ts +++ b/x-pack/plugins/actions/server/authorization/actions_authorization.test.ts @@ -19,12 +19,12 @@ const mockAuthorizationAction = (type: string, operation: string) => `${type}/${ function mockSecurity() { const security = securityMock.createSetup(); const authorization = security.authz; - const securityLicense = security.license; // typescript is having trouble inferring jest's automocking (authorization.actions.savedObject.get as jest.MockedFunction< typeof authorization.actions.savedObject.get >).mockImplementation(mockAuthorizationAction); - return { authorization, securityLicense }; + authorization.mode.useRbacForRequest.mockReturnValue(true); + return { authorization }; } beforeEach(() => { @@ -48,12 +48,11 @@ describe('ensureAuthorized', () => { }); test('is a no-op when the security license is disabled', async () => { - const { authorization, securityLicense } = mockSecurity(); - securityLicense.isEnabled.mockReturnValue(false); + const { authorization } = mockSecurity(); + authorization.mode.useRbacForRequest.mockReturnValue(false); const actionsAuthorization = new ActionsAuthorization({ request, authorization, - securityLicense, auditLogger, }); @@ -61,13 +60,12 @@ describe('ensureAuthorized', () => { }); test('ensures the user has privileges to use the operation on the Actions Saved Object type', async () => { - const { authorization, securityLicense } = mockSecurity(); + const { authorization } = mockSecurity(); const checkPrivileges: jest.MockedFunction> = jest.fn(); authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); const actionsAuthorization = new ActionsAuthorization({ - securityLicense, request, authorization, auditLogger, @@ -101,13 +99,12 @@ describe('ensureAuthorized', () => { }); test('ensures the user has privileges to execute an Actions Saved Object type', async () => { - const { authorization, securityLicense } = mockSecurity(); + const { authorization } = mockSecurity(); const checkPrivileges: jest.MockedFunction> = jest.fn(); authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); const actionsAuthorization = new ActionsAuthorization({ - securityLicense, request, authorization, auditLogger, @@ -151,13 +148,12 @@ describe('ensureAuthorized', () => { }); test('throws if user lacks the required privieleges', async () => { - const { authorization, securityLicense } = mockSecurity(); + const { authorization } = mockSecurity(); const checkPrivileges: jest.MockedFunction> = jest.fn(); authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); const actionsAuthorization = new ActionsAuthorization({ - securityLicense, request, authorization, auditLogger, diff --git a/x-pack/plugins/actions/server/authorization/actions_authorization.ts b/x-pack/plugins/actions/server/authorization/actions_authorization.ts index 41396a1243ea9..da5a5a1cdc3eb 100644 --- a/x-pack/plugins/actions/server/authorization/actions_authorization.ts +++ b/x-pack/plugins/actions/server/authorization/actions_authorization.ts @@ -9,13 +9,11 @@ import { KibanaRequest } from 'src/core/server'; import { SecurityPluginSetup } from '../../../security/server'; import { ActionsAuthorizationAuditLogger } from './audit_logger'; import { ACTION_SAVED_OBJECT_TYPE, ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE } from '../saved_objects'; -import { SecurityLicense } from '../../../security/common/licensing'; export interface ConstructorOptions { request: KibanaRequest; auditLogger: ActionsAuthorizationAuditLogger; authorization?: SecurityPluginSetup['authz']; - securityLicense?: SecurityLicense; } const operationAlias: Record< @@ -26,25 +24,23 @@ const operationAlias: Record< authorization.actions.savedObject.get(ACTION_SAVED_OBJECT_TYPE, 'get'), authorization.actions.savedObject.get(ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, 'create'), ], - list: (authorization) => authorization.actions.savedObject.get(ACTION_SAVED_OBJECT_TYPE, 'get'), + list: (authorization) => authorization.actions.savedObject.get(ACTION_SAVED_OBJECT_TYPE, 'find'), }; export class ActionsAuthorization { private readonly request: KibanaRequest; private readonly authorization?: SecurityPluginSetup['authz']; - private readonly securityLicense?: SecurityLicense; private readonly auditLogger: ActionsAuthorizationAuditLogger; - constructor({ request, authorization, securityLicense, auditLogger }: ConstructorOptions) { + constructor({ request, authorization, auditLogger }: ConstructorOptions) { this.request = request; this.authorization = authorization; - this.securityLicense = securityLicense; this.auditLogger = auditLogger; } public async ensureAuthorized(operation: string, actionTypeId?: string) { - const { authorization, securityLicense } = this; - if (authorization && securityLicense?.isEnabled()) { + const { authorization } = this; + if (authorization?.mode?.useRbacForRequest(this.request)) { const checkPrivileges = authorization.checkPrivilegesDynamicallyWithRequest(this.request); const { hasAllRequested, username } = await checkPrivileges( operationAlias[operation] diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index 9a03bee41eeea..62bd1058774de 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -323,7 +323,6 @@ export class ActionsPlugin implements Plugin, Plugi return new ActionsAuthorization({ request, authorization: this.security?.authz, - securityLicense: this.security?.license, auditLogger: new ActionsAuthorizationAuditLogger( this.security?.audit.getLogger(ACTIONS_FEATURE.id) ), diff --git a/x-pack/plugins/alerts/server/alerts_client_factory.ts b/x-pack/plugins/alerts/server/alerts_client_factory.ts index 6d1cde2485407..79b0ccaf1f0bc 100644 --- a/x-pack/plugins/alerts/server/alerts_client_factory.ts +++ b/x-pack/plugins/alerts/server/alerts_client_factory.ts @@ -65,7 +65,6 @@ export class AlertsClientFactory { const spaceId = this.getSpaceId(request); const authorization = new AlertsAuthorization({ authorization: securityPluginSetup?.authz, - securityLicense: securityPluginSetup?.license, request, getSpace: this.getSpace, alertTypeRegistry: this.alertTypeRegistry, diff --git a/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts b/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts index 2a7150c8a4f65..b164d27ded648 100644 --- a/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts +++ b/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts @@ -32,12 +32,12 @@ const mockAuthorizationAction = (type: string, app: string, operation: string) = function mockSecurity() { const security = securityMock.createSetup(); const authorization = security.authz; - const securityLicense = security.license; // typescript is having trouble inferring jest's automocking (authorization.actions.alerting.get as jest.MockedFunction< typeof authorization.actions.alerting.get >).mockImplementation(mockAuthorizationAction); - return { authorization, securityLicense }; + authorization.mode.useRbacForRequest.mockReturnValue(true); + return { authorization }; } function mockFeature(appName: string, typeName?: string) { @@ -221,13 +221,12 @@ describe('AlertsAuthorization', () => { }); test('is a no-op when the security license is disabled', async () => { - const { authorization, securityLicense } = mockSecurity(); - securityLicense.isEnabled.mockReturnValue(false); + const { authorization } = mockSecurity(); + authorization.mode.useRbacForRequest.mockReturnValue(false); const alertAuthorization = new AlertsAuthorization({ request, alertTypeRegistry, authorization, - securityLicense, features, auditLogger, getSpace, @@ -239,7 +238,7 @@ describe('AlertsAuthorization', () => { }); test('ensures the user has privileges to execute the specified type, operation and consumer', async () => { - const { authorization, securityLicense } = mockSecurity(); + const { authorization } = mockSecurity(); const checkPrivileges: jest.MockedFunction> = jest.fn(); @@ -247,7 +246,6 @@ describe('AlertsAuthorization', () => { const alertAuthorization = new AlertsAuthorization({ request, authorization, - securityLicense, alertTypeRegistry, features, auditLogger, @@ -283,7 +281,7 @@ describe('AlertsAuthorization', () => { }); test('ensures the user has privileges to execute the specified type and operation without consumer when consumer is alerts', async () => { - const { authorization, securityLicense } = mockSecurity(); + const { authorization } = mockSecurity(); const checkPrivileges: jest.MockedFunction> = jest.fn(); @@ -291,7 +289,6 @@ describe('AlertsAuthorization', () => { const alertAuthorization = new AlertsAuthorization({ request, authorization, - securityLicense, alertTypeRegistry, features, auditLogger, @@ -327,7 +324,7 @@ describe('AlertsAuthorization', () => { }); test('ensures the user has privileges to execute the specified type, operation and producer when producer is different from consumer', async () => { - const { authorization, securityLicense } = mockSecurity(); + const { authorization } = mockSecurity(); const checkPrivileges: jest.MockedFunction> = jest.fn(); @@ -341,7 +338,6 @@ describe('AlertsAuthorization', () => { const alertAuthorization = new AlertsAuthorization({ request, authorization, - securityLicense, alertTypeRegistry, features, auditLogger, @@ -377,7 +373,7 @@ describe('AlertsAuthorization', () => { }); test('throws if user lacks the required privieleges for the consumer', async () => { - const { authorization, securityLicense } = mockSecurity(); + const { authorization } = mockSecurity(); const checkPrivileges: jest.MockedFunction> = jest.fn(); @@ -385,7 +381,6 @@ describe('AlertsAuthorization', () => { const alertAuthorization = new AlertsAuthorization({ request, authorization, - securityLicense, alertTypeRegistry, features, auditLogger, @@ -427,7 +422,7 @@ describe('AlertsAuthorization', () => { }); test('throws if user lacks the required privieleges for the producer', async () => { - const { authorization, securityLicense } = mockSecurity(); + const { authorization } = mockSecurity(); const checkPrivileges: jest.MockedFunction> = jest.fn(); @@ -435,7 +430,6 @@ describe('AlertsAuthorization', () => { const alertAuthorization = new AlertsAuthorization({ request, authorization, - securityLicense, alertTypeRegistry, features, auditLogger, @@ -477,7 +471,7 @@ describe('AlertsAuthorization', () => { }); test('throws if user lacks the required privieleges for both consumer and producer', async () => { - const { authorization, securityLicense } = mockSecurity(); + const { authorization } = mockSecurity(); const checkPrivileges: jest.MockedFunction> = jest.fn(); @@ -485,7 +479,6 @@ describe('AlertsAuthorization', () => { const alertAuthorization = new AlertsAuthorization({ request, authorization, - securityLicense, alertTypeRegistry, features, auditLogger, @@ -591,7 +584,7 @@ describe('AlertsAuthorization', () => { }); test('creates a filter based on the privileged types', async () => { - const { authorization, securityLicense } = mockSecurity(); + const { authorization } = mockSecurity(); const checkPrivileges: jest.MockedFunction> = jest.fn(); @@ -605,7 +598,6 @@ describe('AlertsAuthorization', () => { const alertAuthorization = new AlertsAuthorization({ request, authorization, - securityLicense, alertTypeRegistry, features, auditLogger, @@ -621,7 +613,7 @@ describe('AlertsAuthorization', () => { }); test('creates an `ensureAlertTypeIsAuthorized` function which throws if type is unauthorized', async () => { - const { authorization, securityLicense } = mockSecurity(); + const { authorization } = mockSecurity(); const checkPrivileges: jest.MockedFunction> = jest.fn(); @@ -652,7 +644,6 @@ describe('AlertsAuthorization', () => { const alertAuthorization = new AlertsAuthorization({ request, authorization, - securityLicense, alertTypeRegistry, features, auditLogger, @@ -681,7 +672,7 @@ describe('AlertsAuthorization', () => { }); test('creates an `ensureAlertTypeIsAuthorized` function which is no-op if type is authorized', async () => { - const { authorization, securityLicense } = mockSecurity(); + const { authorization } = mockSecurity(); const checkPrivileges: jest.MockedFunction> = jest.fn(); @@ -712,7 +703,6 @@ describe('AlertsAuthorization', () => { const alertAuthorization = new AlertsAuthorization({ request, authorization, - securityLicense, alertTypeRegistry, features, auditLogger, @@ -730,7 +720,7 @@ describe('AlertsAuthorization', () => { }); test('creates an `logSuccessfulAuthorization` function which logs every authorized type', async () => { - const { authorization, securityLicense } = mockSecurity(); + const { authorization } = mockSecurity(); const checkPrivileges: jest.MockedFunction> = jest.fn(); @@ -769,7 +759,6 @@ describe('AlertsAuthorization', () => { const alertAuthorization = new AlertsAuthorization({ request, authorization, - securityLicense, alertTypeRegistry, features, auditLogger, @@ -906,7 +895,7 @@ describe('AlertsAuthorization', () => { }); test('augments a list of types with consumers under which the operation is authorized', async () => { - const { authorization, securityLicense } = mockSecurity(); + const { authorization } = mockSecurity(); const checkPrivileges: jest.MockedFunction> = jest.fn(); @@ -937,7 +926,6 @@ describe('AlertsAuthorization', () => { const alertAuthorization = new AlertsAuthorization({ request, authorization, - securityLicense, alertTypeRegistry, features, auditLogger, @@ -993,7 +981,7 @@ describe('AlertsAuthorization', () => { }); test('authorizes user under the Alerts consumer when they are authorized by the producer', async () => { - const { authorization, securityLicense } = mockSecurity(); + const { authorization } = mockSecurity(); const checkPrivileges: jest.MockedFunction> = jest.fn(); @@ -1016,7 +1004,6 @@ describe('AlertsAuthorization', () => { const alertAuthorization = new AlertsAuthorization({ request, authorization, - securityLicense, alertTypeRegistry, features, auditLogger, @@ -1053,7 +1040,7 @@ describe('AlertsAuthorization', () => { }); test('augments a list of types with consumers under which multiple operations are authorized', async () => { - const { authorization, securityLicense } = mockSecurity(); + const { authorization } = mockSecurity(); const checkPrivileges: jest.MockedFunction> = jest.fn(); @@ -1100,7 +1087,6 @@ describe('AlertsAuthorization', () => { const alertAuthorization = new AlertsAuthorization({ request, authorization, - securityLicense, alertTypeRegistry, features, auditLogger, @@ -1164,7 +1150,7 @@ describe('AlertsAuthorization', () => { }); test('omits types which have no consumers under which the operation is authorized', async () => { - const { authorization, securityLicense } = mockSecurity(); + const { authorization } = mockSecurity(); const checkPrivileges: jest.MockedFunction> = jest.fn(); @@ -1195,7 +1181,6 @@ describe('AlertsAuthorization', () => { const alertAuthorization = new AlertsAuthorization({ request, authorization, - securityLicense, alertTypeRegistry, features, auditLogger, diff --git a/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts b/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts index af67c5e40593c..33a9a0bf0396e 100644 --- a/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts +++ b/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts @@ -13,7 +13,6 @@ import { SecurityPluginSetup } from '../../../security/server'; import { RegistryAlertType } from '../alert_type_registry'; import { PluginStartContract as FeaturesPluginStart } from '../../../features/server'; import { AlertsAuthorizationAuditLogger, ScopeType } from './audit_logger'; -import { SecurityLicense } from '../../../security/common/licensing'; import { Space } from '../../../spaces/server'; export enum ReadOperations { @@ -53,14 +52,12 @@ export interface ConstructorOptions { getSpace: (request: KibanaRequest) => Promise; auditLogger: AlertsAuthorizationAuditLogger; authorization?: SecurityPluginSetup['authz']; - securityLicense?: SecurityLicense; } export class AlertsAuthorization { private readonly alertTypeRegistry: AlertTypeRegistry; private readonly request: KibanaRequest; private readonly authorization?: SecurityPluginSetup['authz']; - private readonly securityLicense?: SecurityLicense; private readonly auditLogger: AlertsAuthorizationAuditLogger; private readonly featuresIds: Promise>; private readonly allPossibleConsumers: Promise; @@ -69,14 +66,12 @@ export class AlertsAuthorization { alertTypeRegistry, request, authorization, - securityLicense, features, auditLogger, getSpace, }: ConstructorOptions) { this.request = request; this.authorization = authorization; - this.securityLicense = securityLicense; this.alertTypeRegistry = alertTypeRegistry; this.auditLogger = auditLogger; @@ -114,8 +109,7 @@ export class AlertsAuthorization { } private shouldCheckAuthorization(): boolean { - const { authorization, securityLicense } = this; - return (authorization && securityLicense && securityLicense?.isEnabled()) ?? false; + return this.authorization?.mode?.useRbacForRequest(this.request) ?? false; } public async ensureAuthorized( From ade93f0780c3a7fabb836c00021b7cc32a8a6b4d Mon Sep 17 00:00:00 2001 From: Tim Roes Date: Mon, 20 Jul 2020 11:40:39 +0200 Subject: [PATCH 20/45] Disable indexing of unnecessary Saved Object fields (#70409) * Disable indexing of unnecessary SO fields * Add doc_values * Add no doc_values to discover saved object Co-authored-by: Elastic Machine --- .../server/saved_objects/dashboard.ts | 24 ++++++++++--------- .../discover/server/saved_objects/search.ts | 8 +++---- .../server/saved_objects/visualization.ts | 10 ++++---- x-pack/plugins/lens/server/saved_objects.ts | 1 + 4 files changed, 24 insertions(+), 19 deletions(-) diff --git a/src/plugins/dashboard/server/saved_objects/dashboard.ts b/src/plugins/dashboard/server/saved_objects/dashboard.ts index 14d2c822a421e..850b2470dd475 100644 --- a/src/plugins/dashboard/server/saved_objects/dashboard.ts +++ b/src/plugins/dashboard/server/saved_objects/dashboard.ts @@ -44,21 +44,23 @@ export const dashboardSavedObjectType: SavedObjectsType = { mappings: { properties: { description: { type: 'text' }, - hits: { type: 'integer' }, - kibanaSavedObjectMeta: { properties: { searchSourceJSON: { type: 'text' } } }, - optionsJSON: { type: 'text' }, - panelsJSON: { type: 'text' }, + hits: { type: 'integer', index: false, doc_values: false }, + kibanaSavedObjectMeta: { + properties: { searchSourceJSON: { type: 'text', index: false, doc_values: false } }, + }, + optionsJSON: { type: 'text', index: false, doc_values: false }, + panelsJSON: { type: 'text', index: false, doc_values: false }, refreshInterval: { properties: { - display: { type: 'keyword' }, - pause: { type: 'boolean' }, - section: { type: 'integer' }, - value: { type: 'integer' }, + display: { type: 'keyword', index: false, doc_values: false }, + pause: { type: 'boolean', index: false, doc_values: false }, + section: { type: 'integer', index: false, doc_values: false }, + value: { type: 'integer', index: false, doc_values: false }, }, }, - timeFrom: { type: 'keyword' }, - timeRestore: { type: 'boolean' }, - timeTo: { type: 'keyword' }, + timeFrom: { type: 'keyword', index: false, doc_values: false }, + timeRestore: { type: 'boolean', index: false, doc_values: false }, + timeTo: { type: 'keyword', index: false, doc_values: false }, title: { type: 'text' }, version: { type: 'integer' }, }, diff --git a/src/plugins/discover/server/saved_objects/search.ts b/src/plugins/discover/server/saved_objects/search.ts index 2348d89c4f4dd..c13550e543ab6 100644 --- a/src/plugins/discover/server/saved_objects/search.ts +++ b/src/plugins/discover/server/saved_objects/search.ts @@ -43,15 +43,15 @@ export const searchSavedObjectType: SavedObjectsType = { }, mappings: { properties: { - columns: { type: 'keyword', index: false }, + columns: { type: 'keyword', index: false, doc_values: false }, description: { type: 'text' }, - hits: { type: 'integer', index: false }, + hits: { type: 'integer', index: false, doc_values: false }, kibanaSavedObjectMeta: { properties: { - searchSourceJSON: { type: 'text', index: false }, + searchSourceJSON: { type: 'text', index: false, doc_values: false }, }, }, - sort: { type: 'keyword', index: false }, + sort: { type: 'keyword', index: false, doc_values: false }, title: { type: 'text' }, version: { type: 'integer' }, }, diff --git a/src/plugins/visualizations/server/saved_objects/visualization.ts b/src/plugins/visualizations/server/saved_objects/visualization.ts index c4756de0a8386..ad7618a8640ba 100644 --- a/src/plugins/visualizations/server/saved_objects/visualization.ts +++ b/src/plugins/visualizations/server/saved_objects/visualization.ts @@ -44,12 +44,14 @@ export const visualizationSavedObjectType: SavedObjectsType = { mappings: { properties: { description: { type: 'text' }, - kibanaSavedObjectMeta: { properties: { searchSourceJSON: { type: 'text' } } }, - savedSearchRefName: { type: 'keyword' }, + kibanaSavedObjectMeta: { + properties: { searchSourceJSON: { type: 'text', index: false, doc_values: false } }, + }, + savedSearchRefName: { type: 'keyword', index: false, doc_values: false }, title: { type: 'text' }, - uiStateJSON: { type: 'text' }, + uiStateJSON: { type: 'text', index: false, doc_values: false }, version: { type: 'integer' }, - visState: { type: 'text' }, + visState: { type: 'text', index: false, doc_values: false }, }, }, migrations: visualizationSavedObjectTypeMigrations, diff --git a/x-pack/plugins/lens/server/saved_objects.ts b/x-pack/plugins/lens/server/saved_objects.ts index a16cc3dab7967..82ee490bb22af 100644 --- a/x-pack/plugins/lens/server/saved_objects.ts +++ b/x-pack/plugins/lens/server/saved_objects.ts @@ -40,6 +40,7 @@ export function setupSavedObjects(core: CoreSetup) { }, expression: { index: false, + doc_values: false, type: 'keyword', }, }, From 3442451aacb0d5e469019c470d5cf494276eabae Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 20 Jul 2020 13:04:42 +0300 Subject: [PATCH 21/45] [Security Solution][Case] IBM Resilient content fixes (#72271) --- .../public/common/lib/connectors/resilient/index.tsx | 2 +- .../common/lib/connectors/resilient/translations.ts | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/resilient/index.tsx b/x-pack/plugins/security_solution/public/common/lib/connectors/resilient/index.tsx index d3daf195582a8..ba4879e87a1f6 100644 --- a/x-pack/plugins/security_solution/public/common/lib/connectors/resilient/index.tsx +++ b/x-pack/plugins/security_solution/public/common/lib/connectors/resilient/index.tsx @@ -30,7 +30,7 @@ const validateConnector = (action: ResilientActionConnector): ValidationResult = }; if (!action.config.orgId) { - errors.orgId = [...errors.orgId, i18n.RESILIENT_PROJECT_KEY_LABEL]; + errors.orgId = [...errors.orgId, i18n.RESILIENT_PROJECT_KEY_REQUIRED]; } if (!action.secrets.apiKeyId) { diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/resilient/translations.ts b/x-pack/plugins/security_solution/public/common/lib/connectors/resilient/translations.ts index f8aec2eea3d4b..2ff97ad354095 100644 --- a/x-pack/plugins/security_solution/public/common/lib/connectors/resilient/translations.ts +++ b/x-pack/plugins/security_solution/public/common/lib/connectors/resilient/translations.ts @@ -11,7 +11,7 @@ export * from '../translations'; export const RESILIENT_DESC = i18n.translate( 'xpack.securitySolution.case.connectors.resilient.selectMessageText', { - defaultMessage: 'Push or update SIEM case data to a new issue in resilient', + defaultMessage: 'Push or update Security case data to a new issue in Resilient', } ); @@ -25,28 +25,28 @@ export const RESILIENT_TITLE = i18n.translate( export const RESILIENT_PROJECT_KEY_LABEL = i18n.translate( 'xpack.securitySolution.case.connectors.resilient.orgId', { - defaultMessage: 'Organization Id', + defaultMessage: 'Organization ID', } ); export const RESILIENT_PROJECT_KEY_REQUIRED = i18n.translate( 'xpack.securitySolution.case.connectors.resilient.requiredOrgIdTextField', { - defaultMessage: 'Organization Id', + defaultMessage: 'Organization ID is required', } ); export const RESILIENT_API_KEY_ID_LABEL = i18n.translate( 'xpack.securitySolution.case.connectors.resilient.apiKeyId', { - defaultMessage: 'API key id', + defaultMessage: 'API key ID', } ); export const RESILIENT_API_KEY_ID_REQUIRED = i18n.translate( 'xpack.securitySolution.case.connectors.resilient.requiredApiKeyIdTextField', { - defaultMessage: 'API key id is required', + defaultMessage: 'API key ID is required', } ); From 4acdf278dc75d67041bbdbb8154df06301ba2f04 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Mon, 20 Jul 2020 12:25:13 +0200 Subject: [PATCH 22/45] [ML] fix charts container width init (#72389) --- .../explorer/actions/load_explorer_data.ts | 2 ++ .../explorer_charts_container_service.d.ts | 1 + .../explorer_charts_container_service.js | 10 +++++++--- .../explorer_charts_container_service.test.js | 14 ++++---------- .../ml/public/application/explorer/legacy_utils.ts | 13 ------------- 5 files changed, 14 insertions(+), 26 deletions(-) delete mode 100644 x-pack/plugins/ml/public/application/explorer/legacy_utils.ts diff --git a/x-pack/plugins/ml/public/application/explorer/actions/load_explorer_data.ts b/x-pack/plugins/ml/public/application/explorer/actions/load_explorer_data.ts index 3fcb032bd3ce1..9e53820fc00c5 100644 --- a/x-pack/plugins/ml/public/application/explorer/actions/load_explorer_data.ts +++ b/x-pack/plugins/ml/public/application/explorer/actions/load_explorer_data.ts @@ -200,6 +200,7 @@ const loadExplorerDataProvider = (anomalyTimelineService: AnomalyTimelineService if (selectedCells !== undefined && Array.isArray(anomalyChartRecords)) { memoizedAnomalyDataChange( lastRefresh, + swimlaneContainerWidth, anomalyChartRecords, timerange.earliestMs, timerange.latestMs, @@ -208,6 +209,7 @@ const loadExplorerDataProvider = (anomalyTimelineService: AnomalyTimelineService } else { memoizedAnomalyDataChange( lastRefresh, + swimlaneContainerWidth, [], timerange.earliestMs, timerange.latestMs, diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.d.ts b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.d.ts index 962072b974867..008f47d5b42ac 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.d.ts +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.d.ts @@ -14,6 +14,7 @@ export declare interface ExplorerChartsData { export declare const getDefaultChartsData: () => ExplorerChartsData; export declare const anomalyDataChange: ( + chartsContainerWidth: number, anomalyRecords: any[], earliestMs: number, latestMs: number, diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js index 898e29a303881..1b83a4ed30560 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js @@ -24,7 +24,6 @@ import { } from '../../../../common/util/job_utils'; import { mlResultsService } from '../../services/results_service'; import { mlJobService } from '../../services/job_service'; -import { getChartContainerWidth } from '../legacy_utils'; import { explorerService } from '../explorer_dashboard_service'; import { CHART_TYPE } from '../explorer_constants'; @@ -48,7 +47,13 @@ const MAX_CHARTS_PER_ROW = 4; // callback(getDefaultChartsData()); -export const anomalyDataChange = function (anomalyRecords, earliestMs, latestMs, severity = 0) { +export const anomalyDataChange = function ( + chartsContainerWidth, + anomalyRecords, + earliestMs, + latestMs, + severity = 0 +) { const data = getDefaultChartsData(); const filteredRecords = anomalyRecords.filter((record) => { @@ -56,7 +61,6 @@ export const anomalyDataChange = function (anomalyRecords, earliestMs, latestMs, }); const allSeriesRecords = processRecordsForDisplay(filteredRecords); // Calculate the number of charts per row, depending on the width available, to a max of 4. - const chartsContainerWidth = getChartContainerWidth(); let chartsPerRow = Math.min( Math.max(Math.floor(chartsContainerWidth / 550), 1), MAX_CHARTS_PER_ROW diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.test.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.test.js index 6a9fd19180a4e..433aa65cc5dd4 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.test.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.test.js @@ -88,12 +88,6 @@ jest.mock('../../util/string_utils', () => ({ }, })); -jest.mock('../legacy_utils', () => ({ - getChartContainerWidth() { - return 1140; - }, -})); - jest.mock('../explorer_dashboard_service', () => ({ explorerService: { setCharts: jest.fn(), @@ -109,7 +103,7 @@ describe('explorerChartsContainerService', () => { }); test('call anomalyChangeListener with empty series config', (done) => { - anomalyDataChange([], 1486656000000, 1486670399999); + anomalyDataChange(1140, [], 1486656000000, 1486670399999); setImmediate(() => { expect(explorerService.setCharts.mock.calls.length).toBe(1); @@ -122,7 +116,7 @@ describe('explorerChartsContainerService', () => { }); test('call anomalyChangeListener with actual series config', (done) => { - anomalyDataChange(mockAnomalyChartRecords, 1486656000000, 1486670399999); + anomalyDataChange(1140, mockAnomalyChartRecords, 1486656000000, 1486670399999); setImmediate(() => { expect(explorerService.setCharts.mock.calls.length).toBe(2); @@ -138,7 +132,7 @@ describe('explorerChartsContainerService', () => { return d; }); - anomalyDataChange(mockAnomalyChartRecordsClone, 1486656000000, 1486670399999); + anomalyDataChange(1140, mockAnomalyChartRecordsClone, 1486656000000, 1486670399999); setImmediate(() => { expect(explorerService.setCharts.mock.calls.length).toBe(2); @@ -161,7 +155,7 @@ describe('explorerChartsContainerService', () => { mockAnomalyChartRecordsClone[1].partition_field_value = 'AAL.'; expect(() => { - anomalyDataChange(mockAnomalyChartRecordsClone, 1486656000000, 1486670399999); + anomalyDataChange(1140, mockAnomalyChartRecordsClone, 1486656000000, 1486670399999); }).not.toThrow(); setImmediate(() => { diff --git a/x-pack/plugins/ml/public/application/explorer/legacy_utils.ts b/x-pack/plugins/ml/public/application/explorer/legacy_utils.ts deleted file mode 100644 index b85b0401c45ca..0000000000000 --- a/x-pack/plugins/ml/public/application/explorer/legacy_utils.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -// This file includes utils which should eventuelly become obsolete once Anomaly Explorer -// is fully migrated to React. Their purpose is to retain functionality while we migrate step by step. - -export function getChartContainerWidth() { - const chartContainer = document.querySelector('.explorer-charts'); - return Math.floor((chartContainer && chartContainer.clientWidth) || 0); -} From 7976e2bda4c395adc5c9587d95e498d482a92eb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Mon, 20 Jul 2020 11:39:26 +0100 Subject: [PATCH 23/45] [APM] Testing error rate API and restructuring folders (#72257) * adding error rate and restructuring tests * removing console log * removing console log * adding error rate and restructuring tests * fixing TS error * removing unnecessary files * removing trial tests --- .../apm_api_integration/basic/tests/index.ts | 36 +++++++++----- .../tests/{ => service_maps}/service_maps.ts | 2 +- .../services/{transactions => }/agent_name.ts | 2 +- .../basic/tests/{ => services}/annotations.ts | 2 +- .../{transactions => }/transaction_types.ts | 2 +- .../{ => settings}/agent_configuration.ts | 6 +-- .../basic/tests/{ => settings}/custom_link.ts | 4 +- .../top_traces.expectation.json | 0 .../basic/tests/traces/top_traces.ts | 2 +- .../tests/transaction_groups/error_rate.ts | 47 +++++++++++++++++++ .../expectation/error_rate.json | 42 +++++++++++++++++ .../expectation}/top_transaction_groups.json | 0 .../expectation}/transaction_charts.json | 0 .../top_transaction_groups.ts | 4 +- .../transaction_charts.ts | 4 +- .../apm_api_integration/trial/tests/index.ts | 12 +++-- .../tests/{ => service_maps}/service_maps.ts | 2 +- .../trial/tests/{ => services}/annotations.ts | 2 +- .../tests/{ => services}/rum_services.ts | 2 +- 19 files changed, 138 insertions(+), 33 deletions(-) rename x-pack/test/apm_api_integration/basic/tests/{ => service_maps}/service_maps.ts (93%) rename x-pack/test/apm_api_integration/basic/tests/services/{transactions => }/agent_name.ts (94%) rename x-pack/test/apm_api_integration/basic/tests/{ => services}/annotations.ts (95%) rename x-pack/test/apm_api_integration/basic/tests/services/{transactions => }/transaction_types.ts (94%) rename x-pack/test/apm_api_integration/basic/tests/{ => settings}/agent_configuration.ts (98%) rename x-pack/test/apm_api_integration/basic/tests/{ => settings}/custom_link.ts (96%) rename x-pack/test/apm_api_integration/basic/tests/traces/{ => expectation}/top_traces.expectation.json (100%) create mode 100644 x-pack/test/apm_api_integration/basic/tests/transaction_groups/error_rate.ts create mode 100644 x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/error_rate.json rename x-pack/test/apm_api_integration/basic/tests/{services/transactions/expectations => transaction_groups/expectation}/top_transaction_groups.json (100%) rename x-pack/test/apm_api_integration/basic/tests/{services/transactions/expectations => transaction_groups/expectation}/transaction_charts.json (100%) rename x-pack/test/apm_api_integration/basic/tests/{services/transactions => transaction_groups}/top_transaction_groups.ts (93%) rename x-pack/test/apm_api_integration/basic/tests/{services/transactions => transaction_groups}/transaction_charts.ts (91%) rename x-pack/test/apm_api_integration/trial/tests/{ => service_maps}/service_maps.ts (99%) rename x-pack/test/apm_api_integration/trial/tests/{ => services}/annotations.ts (99%) rename x-pack/test/apm_api_integration/trial/tests/{ => services}/rum_services.ts (95%) diff --git a/x-pack/test/apm_api_integration/basic/tests/index.ts b/x-pack/test/apm_api_integration/basic/tests/index.ts index 3658208e38d00..873aa478ad080 100644 --- a/x-pack/test/apm_api_integration/basic/tests/index.ts +++ b/x-pack/test/apm_api_integration/basic/tests/index.ts @@ -9,22 +9,32 @@ export default function apmApiIntegrationTests({ loadTestFile }: FtrProviderCont describe('APM specs (basic)', function () { this.tags('ciGroup1'); - loadTestFile(require.resolve('./annotations')); loadTestFile(require.resolve('./feature_controls')); - loadTestFile(require.resolve('./agent_configuration')); - loadTestFile(require.resolve('./custom_link')); - loadTestFile(require.resolve('./service_maps')); - // traces - loadTestFile(require.resolve('./traces/top_traces')); + describe('Service Maps', function () { + loadTestFile(require.resolve('./service_maps/service_maps')); + }); - // services - loadTestFile(require.resolve('./services/top_services')); + describe('Services', function () { + loadTestFile(require.resolve('./services/annotations')); + loadTestFile(require.resolve('./services/top_services')); + loadTestFile(require.resolve('./services/agent_name')); + loadTestFile(require.resolve('./services/transaction_types')); + }); - // services/transaction - loadTestFile(require.resolve('./services/transactions/top_transaction_groups')); - loadTestFile(require.resolve('./services/transactions/transaction_charts')); - loadTestFile(require.resolve('./services/transactions/agent_name')); - loadTestFile(require.resolve('./services/transactions/transaction_types')); + describe('Settings', function () { + loadTestFile(require.resolve('./settings/custom_link')); + loadTestFile(require.resolve('./settings/agent_configuration')); + }); + + describe('Traces', function () { + loadTestFile(require.resolve('./traces/top_traces')); + }); + + describe('Transaction Group', function () { + loadTestFile(require.resolve('./transaction_groups/top_transaction_groups')); + loadTestFile(require.resolve('./transaction_groups/transaction_charts')); + loadTestFile(require.resolve('./transaction_groups/error_rate')); + }); }); } diff --git a/x-pack/test/apm_api_integration/basic/tests/service_maps.ts b/x-pack/test/apm_api_integration/basic/tests/service_maps/service_maps.ts similarity index 93% rename from x-pack/test/apm_api_integration/basic/tests/service_maps.ts rename to x-pack/test/apm_api_integration/basic/tests/service_maps/service_maps.ts index 45972aaf529af..b0e503eb7d1eb 100644 --- a/x-pack/test/apm_api_integration/basic/tests/service_maps.ts +++ b/x-pack/test/apm_api_integration/basic/tests/service_maps/service_maps.ts @@ -5,7 +5,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; export default function serviceMapsApiTests({ getService }: FtrProviderContext) { const supertest = getService('supertest'); diff --git a/x-pack/test/apm_api_integration/basic/tests/services/transactions/agent_name.ts b/x-pack/test/apm_api_integration/basic/tests/services/agent_name.ts similarity index 94% rename from x-pack/test/apm_api_integration/basic/tests/services/transactions/agent_name.ts rename to x-pack/test/apm_api_integration/basic/tests/services/agent_name.ts index a9aeaf813aa5c..5be5e43b359f5 100644 --- a/x-pack/test/apm_api_integration/basic/tests/services/transactions/agent_name.ts +++ b/x-pack/test/apm_api_integration/basic/tests/services/agent_name.ts @@ -5,7 +5,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); diff --git a/x-pack/test/apm_api_integration/basic/tests/annotations.ts b/x-pack/test/apm_api_integration/basic/tests/services/annotations.ts similarity index 95% rename from x-pack/test/apm_api_integration/basic/tests/annotations.ts rename to x-pack/test/apm_api_integration/basic/tests/services/annotations.ts index e0659fe195f93..3136dcef2e985 100644 --- a/x-pack/test/apm_api_integration/basic/tests/annotations.ts +++ b/x-pack/test/apm_api_integration/basic/tests/services/annotations.ts @@ -6,7 +6,7 @@ import expect from '@kbn/expect'; import { JsonObject } from 'src/plugins/kibana_utils/common'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; export default function annotationApiTests({ getService }: FtrProviderContext) { const supertestWrite = getService('supertestAsApmAnnotationsWriteUser'); diff --git a/x-pack/test/apm_api_integration/basic/tests/services/transactions/transaction_types.ts b/x-pack/test/apm_api_integration/basic/tests/services/transaction_types.ts similarity index 94% rename from x-pack/test/apm_api_integration/basic/tests/services/transactions/transaction_types.ts rename to x-pack/test/apm_api_integration/basic/tests/services/transaction_types.ts index 56844dfcedda1..3e8f320ad6b24 100644 --- a/x-pack/test/apm_api_integration/basic/tests/services/transactions/transaction_types.ts +++ b/x-pack/test/apm_api_integration/basic/tests/services/transaction_types.ts @@ -5,7 +5,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); diff --git a/x-pack/test/apm_api_integration/basic/tests/agent_configuration.ts b/x-pack/test/apm_api_integration/basic/tests/settings/agent_configuration.ts similarity index 98% rename from x-pack/test/apm_api_integration/basic/tests/agent_configuration.ts rename to x-pack/test/apm_api_integration/basic/tests/settings/agent_configuration.ts index 1c8097760bc04..283540201b9b5 100644 --- a/x-pack/test/apm_api_integration/basic/tests/agent_configuration.ts +++ b/x-pack/test/apm_api_integration/basic/tests/settings/agent_configuration.ts @@ -6,9 +6,9 @@ import expect from '@kbn/expect'; import { omit, orderBy } from 'lodash'; -import { AgentConfigurationIntake } from '../../../../plugins/apm/common/agent_configuration/configuration_types'; -import { AgentConfigSearchParams } from '../../../../plugins/apm/server/routes/settings/agent_configuration'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { AgentConfigurationIntake } from '../../../../../plugins/apm/common/agent_configuration/configuration_types'; +import { AgentConfigSearchParams } from '../../../../../plugins/apm/server/routes/settings/agent_configuration'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; export default function agentConfigurationTests({ getService }: FtrProviderContext) { const supertestRead = getService('supertestAsApmReadUser'); diff --git a/x-pack/test/apm_api_integration/basic/tests/custom_link.ts b/x-pack/test/apm_api_integration/basic/tests/settings/custom_link.ts similarity index 96% rename from x-pack/test/apm_api_integration/basic/tests/custom_link.ts rename to x-pack/test/apm_api_integration/basic/tests/settings/custom_link.ts index ec93d2b3a3b41..9465708db2fba 100644 --- a/x-pack/test/apm_api_integration/basic/tests/custom_link.ts +++ b/x-pack/test/apm_api_integration/basic/tests/settings/custom_link.ts @@ -5,8 +5,8 @@ */ import URL from 'url'; import expect from '@kbn/expect'; -import { CustomLink } from '../../../../plugins/apm/common/custom_link/custom_link_types'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { CustomLink } from '../../../../../plugins/apm/common/custom_link/custom_link_types'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; export default function customLinksTests({ getService }: FtrProviderContext) { const supertestRead = getService('supertestAsApmReadUser'); diff --git a/x-pack/test/apm_api_integration/basic/tests/traces/top_traces.expectation.json b/x-pack/test/apm_api_integration/basic/tests/traces/expectation/top_traces.expectation.json similarity index 100% rename from x-pack/test/apm_api_integration/basic/tests/traces/top_traces.expectation.json rename to x-pack/test/apm_api_integration/basic/tests/traces/expectation/top_traces.expectation.json diff --git a/x-pack/test/apm_api_integration/basic/tests/traces/top_traces.ts b/x-pack/test/apm_api_integration/basic/tests/traces/top_traces.ts index aef208b6fc06b..e96cb20a68fda 100644 --- a/x-pack/test/apm_api_integration/basic/tests/traces/top_traces.ts +++ b/x-pack/test/apm_api_integration/basic/tests/traces/top_traces.ts @@ -5,7 +5,7 @@ */ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import expectTopTraces from './top_traces.expectation.json'; +import expectTopTraces from './expectation/top_traces.expectation.json'; export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/error_rate.ts b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/error_rate.ts new file mode 100644 index 0000000000000..2c22cbbcce780 --- /dev/null +++ b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/error_rate.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; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import expectedErrorRate from './expectation/error_rate.json'; + +export default function ApiTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + // url parameters + const start = encodeURIComponent('2020-06-29T06:45:00.000Z'); + const end = encodeURIComponent('2020-06-29T06:49:00.000Z'); + const uiFilters = encodeURIComponent(JSON.stringify({})); + + describe('Error rate', () => { + describe('when data is not loaded', () => { + it('handles the empty state', async () => { + const response = await supertest.get( + `/api/apm/services/opbeans-node/transaction_groups/error_rate?start=${start}&end=${end}&uiFilters=${uiFilters}` + ); + expect(response.status).to.be(200); + expect(response.body).to.eql({ + noHits: true, + erroneousTransactionsRate: [], + average: null, + }); + }); + }); + describe('when data is loaded', () => { + before(() => esArchiver.load('8.0.0')); + after(() => esArchiver.unload('8.0.0')); + + it('returns the transaction error rate', async () => { + const response = await supertest.get( + `/api/apm/services/opbeans-node/transaction_groups/error_rate?start=${start}&end=${end}&uiFilters=${uiFilters}` + ); + + expect(response.status).to.be(200); + expect(response.body).to.eql(expectedErrorRate); + }); + }); + }); +} diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/error_rate.json b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/error_rate.json new file mode 100644 index 0000000000000..9ff45ebdbb21b --- /dev/null +++ b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/error_rate.json @@ -0,0 +1,42 @@ +{ + "noHits":false, + "erroneousTransactionsRate":[ + { + "x":1593413100000, + "y":null + }, + { + "x":1593413130000, + "y":null + }, + { + "x":1593413160000, + "y":null + }, + { + "x":1593413190000, + "y":null + }, + { + "x":1593413220000, + "y":null + }, + { + "x":1593413250000, + "y":0 + }, + { + "x":1593413280000, + "y":0.14102564102564102 + }, + { + "x":1593413310000, + "y":0.14634146341463414 + }, + { + "x":1593413340000, + "y":null + } + ], + "average":0.09578903481342504 +} \ No newline at end of file diff --git a/x-pack/test/apm_api_integration/basic/tests/services/transactions/expectations/top_transaction_groups.json b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/top_transaction_groups.json similarity index 100% rename from x-pack/test/apm_api_integration/basic/tests/services/transactions/expectations/top_transaction_groups.json rename to x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/top_transaction_groups.json diff --git a/x-pack/test/apm_api_integration/basic/tests/services/transactions/expectations/transaction_charts.json b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/transaction_charts.json similarity index 100% rename from x-pack/test/apm_api_integration/basic/tests/services/transactions/expectations/transaction_charts.json rename to x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/transaction_charts.json diff --git a/x-pack/test/apm_api_integration/basic/tests/services/transactions/top_transaction_groups.ts b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/top_transaction_groups.ts similarity index 93% rename from x-pack/test/apm_api_integration/basic/tests/services/transactions/top_transaction_groups.ts rename to x-pack/test/apm_api_integration/basic/tests/transaction_groups/top_transaction_groups.ts index bf8d3f6a56e6a..43b2ad5414c7a 100644 --- a/x-pack/test/apm_api_integration/basic/tests/services/transactions/top_transaction_groups.ts +++ b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/top_transaction_groups.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; -import expectedTransactionGroups from './expectations/top_transaction_groups.json'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import expectedTransactionGroups from './expectation/top_transaction_groups.json'; export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); diff --git a/x-pack/test/apm_api_integration/basic/tests/services/transactions/transaction_charts.ts b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/transaction_charts.ts similarity index 91% rename from x-pack/test/apm_api_integration/basic/tests/services/transactions/transaction_charts.ts rename to x-pack/test/apm_api_integration/basic/tests/transaction_groups/transaction_charts.ts index 7178498863787..68a7499a2389c 100644 --- a/x-pack/test/apm_api_integration/basic/tests/services/transactions/transaction_charts.ts +++ b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/transaction_charts.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; -import expectedTransactionCharts from './expectations/transaction_charts.json'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import expectedTransactionCharts from './expectation/transaction_charts.json'; export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); diff --git a/x-pack/test/apm_api_integration/trial/tests/index.ts b/x-pack/test/apm_api_integration/trial/tests/index.ts index 37328badcb794..fb942f755a063 100644 --- a/x-pack/test/apm_api_integration/trial/tests/index.ts +++ b/x-pack/test/apm_api_integration/trial/tests/index.ts @@ -9,8 +9,14 @@ import { FtrProviderContext } from '../../../api_integration/ftr_provider_contex export default function observabilityApiIntegrationTests({ loadTestFile }: FtrProviderContext) { describe('APM specs (trial)', function () { this.tags('ciGroup1'); - loadTestFile(require.resolve('./annotations')); - loadTestFile(require.resolve('./service_maps')); - loadTestFile(require.resolve('./rum_services')); + + describe('Services', function () { + loadTestFile(require.resolve('./services/annotations')); + loadTestFile(require.resolve('./services/rum_services.ts')); + }); + + describe('Service Maps', function () { + loadTestFile(require.resolve('./service_maps/service_maps')); + }); }); } diff --git a/x-pack/test/apm_api_integration/trial/tests/service_maps.ts b/x-pack/test/apm_api_integration/trial/tests/service_maps/service_maps.ts similarity index 99% rename from x-pack/test/apm_api_integration/trial/tests/service_maps.ts rename to x-pack/test/apm_api_integration/trial/tests/service_maps/service_maps.ts index 0b370f6a30a8b..4002e8cff5bad 100644 --- a/x-pack/test/apm_api_integration/trial/tests/service_maps.ts +++ b/x-pack/test/apm_api_integration/trial/tests/service_maps/service_maps.ts @@ -6,7 +6,7 @@ import querystring from 'querystring'; import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; export default function serviceMapsApiTests({ getService }: FtrProviderContext) { const supertest = getService('supertest'); diff --git a/x-pack/test/apm_api_integration/trial/tests/annotations.ts b/x-pack/test/apm_api_integration/trial/tests/services/annotations.ts similarity index 99% rename from x-pack/test/apm_api_integration/trial/tests/annotations.ts rename to x-pack/test/apm_api_integration/trial/tests/services/annotations.ts index 662879c495230..c2ddc10c5f1d2 100644 --- a/x-pack/test/apm_api_integration/trial/tests/annotations.ts +++ b/x-pack/test/apm_api_integration/trial/tests/services/annotations.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; import { merge, cloneDeep, isPlainObject } from 'lodash'; import { JsonObject } from 'src/plugins/kibana_utils/common'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; const DEFAULT_INDEX_NAME = 'observability-annotations'; diff --git a/x-pack/test/apm_api_integration/trial/tests/rum_services.ts b/x-pack/test/apm_api_integration/trial/tests/services/rum_services.ts similarity index 95% rename from x-pack/test/apm_api_integration/trial/tests/rum_services.ts rename to x-pack/test/apm_api_integration/trial/tests/services/rum_services.ts index 5505387de54a7..78171a65a11fd 100644 --- a/x-pack/test/apm_api_integration/trial/tests/rum_services.ts +++ b/x-pack/test/apm_api_integration/trial/tests/services/rum_services.ts @@ -5,7 +5,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; export default function rumServicesApiTests({ getService }: FtrProviderContext) { const supertest = getService('supertest'); From b1edce8050cc209053d094f1788b7de657123ae2 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Mon, 20 Jul 2020 12:50:09 +0200 Subject: [PATCH 24/45] [ML] improve annotation flyout performance (#72299) --- .../annotations/annotation_flyout/index.tsx | 270 ++++++++++-------- 1 file changed, 148 insertions(+), 122 deletions(-) diff --git a/x-pack/plugins/ml/public/application/components/annotations/annotation_flyout/index.tsx b/x-pack/plugins/ml/public/application/components/annotations/annotation_flyout/index.tsx index 2196f3d6cc6d2..84abe3ed8a821 100644 --- a/x-pack/plugins/ml/public/application/components/annotations/annotation_flyout/index.tsx +++ b/x-pack/plugins/ml/public/application/components/annotations/annotation_flyout/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Component, Fragment, FC, ReactNode } from 'react'; +import React, { Component, FC, ReactNode, useCallback } from 'react'; import useObservable from 'react-use/lib/useObservable'; import * as Rx from 'rxjs'; import { cloneDeep } from 'lodash'; @@ -28,6 +28,7 @@ import { import { CommonProps } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { distinctUntilChanged } from 'rxjs/operators'; import { ANNOTATION_MAX_LENGTH_CHARS, ANNOTATION_EVENT_USER, @@ -60,7 +61,6 @@ interface Entity { } interface Props { - annotation: AnnotationState; chartDetails: { entityData: { entities: Entity[] }; functionLabel: string; @@ -70,25 +70,39 @@ interface Props { } interface State { + annotationState: AnnotationState | null; isDeleteModalVisible: boolean; applyAnnotationToSeries: boolean; } -class AnnotationFlyoutUI extends Component { +export class AnnotationFlyoutUI extends Component { public state: State = { isDeleteModalVisible: false, applyAnnotationToSeries: true, + annotationState: null, }; public annotationSub: Rx.Subscription | null = null; + componentDidMount() { + this.annotationSub = annotation$.subscribe((v) => { + this.setState({ + annotationState: v, + }); + }); + } + + componentWillUnmount() { + this.annotationSub!.unsubscribe(); + } + public annotationTextChangeHandler = (e: React.ChangeEvent) => { - if (this.props.annotation === null) { + if (this.state.annotationState === null) { return; } annotation$.next({ - ...this.props.annotation, + ...this.state.annotationState, annotation: e.target.value, }); }; @@ -102,21 +116,21 @@ class AnnotationFlyoutUI extends Component { }; public deleteHandler = async () => { - const { annotation } = this.props; + const { annotationState } = this.state; const toastNotifications = getToastNotifications(); - if (annotation === null || annotation._id === undefined) { + if (annotationState === null || annotationState._id === undefined) { return; } try { - await ml.annotations.deleteAnnotation(annotation._id); + await ml.annotations.deleteAnnotation(annotationState._id); toastNotifications.addSuccess( i18n.translate( 'xpack.ml.timeSeriesExplorer.timeSeriesChart.deletedAnnotationNotificationMessage', { defaultMessage: 'Deleted annotation for job with ID {jobId}.', - values: { jobId: annotation.job_id }, + values: { jobId: annotationState.job_id }, } ) ); @@ -127,7 +141,7 @@ class AnnotationFlyoutUI extends Component { { defaultMessage: 'An error occurred deleting the annotation for job with ID {jobId}: {error}', - values: { jobId: annotation.job_id, error: JSON.stringify(err) }, + values: { jobId: annotationState.job_id, error: JSON.stringify(err) }, } ) ); @@ -145,13 +159,13 @@ class AnnotationFlyoutUI extends Component { public validateAnnotationText = () => { // Validates the entered text, returning an array of error messages // for display in the form. An empty array is returned if the text is valid. - const { annotation } = this.props; + const { annotationState } = this.state; const errors: string[] = []; - if (annotation === null) { + if (annotationState === null) { return errors; } - if (annotation.annotation.trim().length === 0) { + if (annotationState.annotation.trim().length === 0) { errors.push( i18n.translate('xpack.ml.timeSeriesExplorer.annotationFlyout.noAnnotationTextError', { defaultMessage: 'Enter annotation text', @@ -159,7 +173,7 @@ class AnnotationFlyoutUI extends Component { ); } - const textLength = annotation.annotation.length; + const textLength = annotationState.annotation.length; if (textLength > ANNOTATION_MAX_LENGTH_CHARS) { const charsOver = textLength - ANNOTATION_MAX_LENGTH_CHARS; errors.push( @@ -178,7 +192,8 @@ class AnnotationFlyoutUI extends Component { }; public saveOrUpdateAnnotation = () => { - const { annotation: originalAnnotation, chartDetails, detectorIndex } = this.props; + const { annotationState: originalAnnotation } = this.state; + const { chartDetails, detectorIndex } = this.props; if (originalAnnotation === null) { return; } @@ -262,14 +277,12 @@ class AnnotationFlyoutUI extends Component { }; public render(): ReactNode { - const { annotation, detectors, detectorIndex } = this.props; - const { isDeleteModalVisible } = this.state; + const { detectors, detectorIndex } = this.props; + const { annotationState, isDeleteModalVisible } = this.state; - if (annotation === null) { - return null; - } + if (!annotationState) return null; - const isExistingAnnotation = typeof annotation._id !== 'undefined'; + const isExistingAnnotation = typeof annotationState._id !== 'undefined'; // Check the length of the text is within the max length limit, // and warn if the length is approaching the limit. @@ -279,14 +292,16 @@ class AnnotationFlyoutUI extends Component { let helpText = null; if ( isInvalid === false && - annotation.annotation.length > ANNOTATION_MAX_LENGTH_CHARS * lengthRatioToShowWarning + annotationState.annotation.length > ANNOTATION_MAX_LENGTH_CHARS * lengthRatioToShowWarning ) { helpText = i18n.translate( 'xpack.ml.timeSeriesExplorer.annotationFlyout.approachingMaxLengthWarning', { defaultMessage: '{charsRemaining, number} {charsRemaining, plural, one {character} other {characters}} remaining', - values: { charsRemaining: ANNOTATION_MAX_LENGTH_CHARS - annotation.annotation.length }, + values: { + charsRemaining: ANNOTATION_MAX_LENGTH_CHARS - annotationState.annotation.length, + }, } ); } @@ -295,127 +310,138 @@ class AnnotationFlyoutUI extends Component { detector && 'detector_description' in detector ? detector.detector_description : ''; return ( - - - - -

- {isExistingAnnotation ? ( - - ) : ( - - )} -

-
-
- - + + + + + } + fullWidth + helpText={helpText} + isInvalid={isInvalid} + error={validationErrors} + > + - - + + } - fullWidth - helpText={helpText} - isInvalid={isInvalid} - error={validationErrors} - > - - - - + this.setState({ + applyAnnotationToSeries: !this.state.applyAnnotationToSeries, + }) + } + /> + + + + + + + + + + + {isExistingAnnotation && ( + - } - checked={this.state.applyAnnotationToSeries} - onChange={() => - this.setState({ - applyAnnotationToSeries: !this.state.applyAnnotationToSeries, - }) - } - /> - - - - - - + + )} + + + + {isExistingAnnotation ? ( + ) : ( + - - - - {isExistingAnnotation && ( - - - )} - - - - {isExistingAnnotation ? ( - - ) : ( - - )} - - - - -
+ + + + -
+ ); } } export const AnnotationFlyout: FC = (props) => { - const annotationProp = useObservable(annotation$); + const annotationProp = useObservable( + annotation$.pipe( + distinctUntilChanged((prev, curr) => { + // prevent re-rendering + return prev !== null && curr !== null; + }) + ) + ); - if (annotationProp === undefined) { + const cancelEditingHandler = useCallback(() => { + annotation$.next(null); + }, []); + + if (annotationProp === undefined || annotationProp === null) { return null; } - return ; + const isExistingAnnotation = typeof annotationProp._id !== 'undefined'; + + return ( + + + +

+ {isExistingAnnotation ? ( + + ) : ( + + )} +

+
+
+ +
+ ); }; From 46f46c7315c38045c009a4d10d4c9db7192cb043 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Mon, 20 Jul 2020 12:16:18 +0100 Subject: [PATCH 25/45] fixed unit in alerts client factory --- x-pack/plugins/alerts/server/alerts_client_factory.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/plugins/alerts/server/alerts_client_factory.test.ts b/x-pack/plugins/alerts/server/alerts_client_factory.test.ts index 4e6a93499f4c9..16b5af499bb90 100644 --- a/x-pack/plugins/alerts/server/alerts_client_factory.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client_factory.test.ts @@ -95,7 +95,6 @@ test('creates an alerts client with proper constructor arguments when security i expect(AlertsAuthorization).toHaveBeenCalledWith({ request, authorization: securityPluginSetup.authz, - securityLicense: securityPluginSetup.license, alertTypeRegistry: alertsClientFactoryParams.alertTypeRegistry, features: alertsClientFactoryParams.features, auditLogger: expect.any(AlertsAuthorizationAuditLogger), From 9504c9453b83e85ccc2d0e5d8cd8009faee57463 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Mon, 20 Jul 2020 13:47:18 +0100 Subject: [PATCH 26/45] [ML] Adding missing index pattern name to new job wizards (#72400) --- .../public/application/jobs/new_job/pages/new_job/page.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/new_job/page.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/new_job/page.tsx index 48b044e5371de..8e223b69b00e8 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/new_job/page.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/new_job/page.tsx @@ -210,6 +210,12 @@ export const Page: FC = ({ existingJobsAndGroups, jobType }) => { : {jobCreatorTitle} + + From bf89b3cdd21cff32675b35db945a89f8a4d7247d Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Mon, 20 Jul 2020 08:53:09 -0400 Subject: [PATCH 27/45] [Ingest Manager] Do not bumb config revision during config creation (#72270) --- .../server/routes/agent_config/handlers.ts | 1 + .../ingest_manager/server/services/agent_config.ts | 10 ++++++---- .../ingest_manager/server/services/package_config.ts | 6 ++++-- x-pack/plugins/ingest_manager/server/services/setup.ts | 4 +++- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/ingest_manager/server/routes/agent_config/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/agent_config/handlers.ts index 718aca89ea4fd..4e4653ec023ce 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent_config/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent_config/handlers.ts @@ -135,6 +135,7 @@ export const createAgentConfigHandler: RequestHandler< newSysPackageConfig.namespace = agentConfig.namespace; await packageConfigService.create(soClient, callCluster, newSysPackageConfig, { user, + bumpConfigRevision: false, }); } diff --git a/x-pack/plugins/ingest_manager/server/services/agent_config.ts b/x-pack/plugins/ingest_manager/server/services/agent_config.ts index c068b594318c1..0a9adc1f1c593 100644 --- a/x-pack/plugins/ingest_manager/server/services/agent_config.ts +++ b/x-pack/plugins/ingest_manager/server/services/agent_config.ts @@ -41,7 +41,8 @@ class AgentConfigService { soClient: SavedObjectsClientContract, id: string, agentConfig: Partial, - user?: AuthenticatedUser + user?: AuthenticatedUser, + options: { bumpRevision: boolean } = { bumpRevision: true } ): Promise { const oldAgentConfig = await this.get(soClient, id, false); @@ -60,7 +61,7 @@ class AgentConfigService { await soClient.update(SAVED_OBJECT_TYPE, id, { ...agentConfig, - revision: oldAgentConfig.revision + 1, + ...(options.bumpRevision ? { revision: oldAgentConfig.revision + 1 } : {}), updated_at: new Date().toISOString(), updated_by: user ? user.username : 'system', }); @@ -265,7 +266,7 @@ class AgentConfigService { soClient: SavedObjectsClientContract, id: string, packageConfigIds: string[], - options?: { user?: AuthenticatedUser } + options: { user?: AuthenticatedUser; bumpRevision: boolean } = { bumpRevision: true } ): Promise { const oldAgentConfig = await this.get(soClient, id, false); @@ -281,7 +282,8 @@ class AgentConfigService { [...((oldAgentConfig.package_configs || []) as string[])].concat(packageConfigIds) ), }, - options?.user + options?.user, + { bumpRevision: options.bumpRevision } ); } diff --git a/x-pack/plugins/ingest_manager/server/services/package_config.ts b/x-pack/plugins/ingest_manager/server/services/package_config.ts index e8ca09a83c2b6..c2d465cf7c73f 100644 --- a/x-pack/plugins/ingest_manager/server/services/package_config.ts +++ b/x-pack/plugins/ingest_manager/server/services/package_config.ts @@ -42,7 +42,7 @@ class PackageConfigService { soClient: SavedObjectsClientContract, callCluster: CallESAsCurrentUser, packageConfig: NewPackageConfig, - options?: { id?: string; user?: AuthenticatedUser } + options?: { id?: string; user?: AuthenticatedUser; bumpConfigRevision?: boolean } ): Promise { // Check that its agent config does not have a package config with the same name const parentAgentConfig = await agentConfigService.get(soClient, packageConfig.config_id); @@ -104,6 +104,7 @@ class PackageConfigService { // Assign it to the given agent config await agentConfigService.assignPackageConfigs(soClient, packageConfig.config_id, [newSo.id], { user: options?.user, + bumpRevision: options?.bumpConfigRevision ?? true, }); return { @@ -117,7 +118,7 @@ class PackageConfigService { soClient: SavedObjectsClientContract, packageConfigs: NewPackageConfig[], configId: string, - options?: { user?: AuthenticatedUser } + options?: { user?: AuthenticatedUser; bumpConfigRevision?: boolean } ): Promise { const isoDate = new Date().toISOString(); const { saved_objects: newSos } = await soClient.bulkCreate( @@ -142,6 +143,7 @@ class PackageConfigService { newSos.map((newSo) => newSo.id), { user: options?.user, + bumpRevision: options?.bumpConfigRevision ?? true, } ); diff --git a/x-pack/plugins/ingest_manager/server/services/setup.ts b/x-pack/plugins/ingest_manager/server/services/setup.ts index 627abc158143d..c91cae98e17d2 100644 --- a/x-pack/plugins/ingest_manager/server/services/setup.ts +++ b/x-pack/plugins/ingest_manager/server/services/setup.ts @@ -218,5 +218,7 @@ async function addPackageToConfig( config.namespace ); - await packageConfigService.create(soClient, callCluster, newPackageConfig); + await packageConfigService.create(soClient, callCluster, newPackageConfig, { + bumpConfigRevision: false, + }); } From f331cc8b641dc6bc4ce2ae82e404f88f0451d768 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Mon, 20 Jul 2020 13:54:36 +0100 Subject: [PATCH 28/45] [Ingest Manager] Set `_meta` in the index.mappings (#72026) Co-authored-by: Elastic Machine --- .../__snapshots__/template.test.ts.snap | 21 +++++++++++++++++++ .../epm/elasticsearch/template/template.ts | 18 +++++++++------- 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap index 219c2de675359..0333fb024a717 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap @@ -87,6 +87,13 @@ exports[`tests loading base.yml: base.yml 1`] = ` "validarray": { "type": "integer" } + }, + "_meta": { + "package": { + "name": "nginx" + }, + "managed_by": "ingest-manager", + "managed": true } }, "aliases": {} @@ -190,6 +197,13 @@ exports[`tests loading coredns.logs.yml: coredns.logs.yml 1`] = ` } } } + }, + "_meta": { + "package": { + "name": "coredns" + }, + "managed_by": "ingest-manager", + "managed": true } }, "aliases": {} @@ -1677,6 +1691,13 @@ exports[`tests loading system.yml: system.yml 1`] = ` } } } + }, + "_meta": { + "package": { + "name": "system" + }, + "managed_by": "ingest-manager", + "managed": true } }, "aliases": {} diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts index 876573f2270ea..a739806d5868b 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts @@ -249,6 +249,15 @@ function getBaseTemplate( packageName: string, composedOfTemplates: string[] ): IndexTemplate { + // Meta information to identify Ingest Manager's managed templates and indices + const _meta = { + package: { + name: packageName, + }, + managed_by: 'ingest-manager', + managed: true, + }; + return { // This takes precedence over all index templates installed by ES by default (logs-*-* and metrics-*-*) // if this number is lower than the ES value (which is 100) this template will never be applied when a data stream @@ -304,19 +313,14 @@ function getBaseTemplate( date_detection: false, // All the properties we know from the fields.yml file properties: mappings.properties, + _meta, }, // To be filled with the aliases that we need aliases: {}, }, data_stream: {}, composed_of: composedOfTemplates, - _meta: { - package: { - name: packageName, - }, - managed_by: 'ingest-manager', - managed: true, - }, + _meta, }; } From 6cf796a4fb2a00165b8bf1a33047dd3688fc3e43 Mon Sep 17 00:00:00 2001 From: Robert Austin Date: Mon, 20 Jul 2020 09:38:30 -0400 Subject: [PATCH 29/45] [Resolver] Selector performance (#72380) * Memoize various selectors * Improve performance of the selectors that calculate the `aria-flowto` attribute. * more tests. --- .../common/endpoint/types.ts | 1 - .../models/indexed_process_tree/index.ts | 53 ++--- .../isometric_taxi_layout.ts | 11 +- .../public/resolver/store/camera/selectors.ts | 65 +++--- .../resolver/store/data/selectors.test.ts | 90 ++++++++ .../public/resolver/store/data/selectors.ts | 208 +++++++++++------- .../resolver/store/mocks/endpoint_event.ts | 37 ++++ .../resolver/store/mocks/resolver_tree.ts | 87 ++++++++ .../public/resolver/store/selectors.test.ts | 117 +--------- .../public/resolver/store/selectors.ts | 35 +-- .../panels/panel_content_related_detail.tsx | 3 +- 11 files changed, 427 insertions(+), 280 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/resolver/store/mocks/endpoint_event.ts create mode 100644 x-pack/plugins/security_solution/public/resolver/store/mocks/resolver_tree.ts diff --git a/x-pack/plugins/security_solution/common/endpoint/types.ts b/x-pack/plugins/security_solution/common/endpoint/types.ts index b477207b1c5a3..5e7e4d22f8c3c 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types.ts @@ -482,7 +482,6 @@ export interface LegacyEndpointEvent { type: string; version: string; }; - process?: object; rule?: object; user?: object; event?: { diff --git a/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/index.ts b/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/index.ts index 35a32d91d8a02..628d0267754f2 100644 --- a/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/index.ts +++ b/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/index.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +/* eslint-disable no-shadow */ + import { uniquePidForProcess, uniqueParentPidForProcess, orderByTime } from '../process_event'; import { IndexedProcessTree } from '../../types'; import { ResolverEvent } from '../../../../common/endpoint/types'; @@ -24,16 +26,15 @@ export function factory( const uniqueProcessPid = uniquePidForProcess(process); idToValue.set(uniqueProcessPid, process); - const uniqueParentPid = uniqueParentPidForProcess(process); - // if its defined and not '' - if (uniqueParentPid) { - let siblings = idToChildren.get(uniqueParentPid); - if (!siblings) { - siblings = []; - idToChildren.set(uniqueParentPid, siblings); - } - siblings.push(process); + // NB: If the value was null or undefined, use `undefined` + const uniqueParentPid: string | undefined = uniqueParentPidForProcess(process) ?? undefined; + + let childrenWithTheSameParent = idToChildren.get(uniqueParentPid); + if (!childrenWithTheSameParent) { + childrenWithTheSameParent = []; + idToChildren.set(uniqueParentPid, childrenWithTheSameParent); } + childrenWithTheSameParent.push(process); } // sort the children of each node @@ -50,9 +51,8 @@ export function factory( /** * Returns an array with any children `ProcessEvent`s of the passed in `process` */ -export function children(tree: IndexedProcessTree, process: ResolverEvent): ResolverEvent[] { - const id = uniquePidForProcess(process); - const currentProcessSiblings = tree.idToChildren.get(id); +export function children(tree: IndexedProcessTree, parentID: string | undefined): ResolverEvent[] { + const currentProcessSiblings = tree.idToChildren.get(parentID); return currentProcessSiblings === undefined ? [] : currentProcessSiblings; } @@ -78,31 +78,6 @@ export function parent( } } -/** - * Returns the following sibling - */ -export function nextSibling( - tree: IndexedProcessTree, - sibling: ResolverEvent -): ResolverEvent | undefined { - const parentNode = parent(tree, sibling); - if (parentNode) { - // The siblings of `sibling` are the children of its parent. - const siblings = children(tree, parentNode); - - // Find the sibling - const index = siblings.indexOf(sibling); - - // if the sibling wasn't found, or if it was the last element in the array, return undefined - if (index === -1 || index === siblings.length - 1) { - return undefined; - } - - // return the next sibling - return siblings[index + 1]; - } -} - /** * Number of processes in the tree */ @@ -133,6 +108,8 @@ export function root(tree: IndexedProcessTree) { export function* levelOrder(tree: IndexedProcessTree) { const rootNode = root(tree); if (rootNode !== null) { - yield* baseLevelOrder(rootNode, children.bind(null, tree)); + yield* baseLevelOrder(rootNode, (parentNode: ResolverEvent): ResolverEvent[] => + children(tree, uniquePidForProcess(parentNode)) + ); } } diff --git a/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/isometric_taxi_layout.ts b/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/isometric_taxi_layout.ts index 6058a40037ad2..11c888d1462f8 100644 --- a/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/isometric_taxi_layout.ts +++ b/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/isometric_taxi_layout.ts @@ -19,6 +19,7 @@ import * as event from '../../../../common/endpoint/models/event'; import { ResolverEvent } from '../../../../common/endpoint/types'; import * as model from './index'; import { getFriendlyElapsedTime as elapsedTime } from '../../lib/date'; +import { uniquePidForProcess } from '../process_event'; /** * Graph the process tree @@ -146,10 +147,12 @@ function widthsOfProcessSubtrees(indexedProcessTree: IndexedProcessTree): Proces return widths; } - const processesInReverseLevelOrder = [...model.levelOrder(indexedProcessTree)].reverse(); + const processesInReverseLevelOrder: ResolverEvent[] = [ + ...model.levelOrder(indexedProcessTree), + ].reverse(); for (const process of processesInReverseLevelOrder) { - const children = model.children(indexedProcessTree, process); + const children = model.children(indexedProcessTree, uniquePidForProcess(process)); const sumOfWidthOfChildren = function sumOfWidthOfChildren() { return children.reduce(function sum(currentValue, child) { @@ -226,7 +229,7 @@ function processEdgeLineSegments( metadata: edgeLineMetadata, }; - const siblings = model.children(indexedProcessTree, parent); + const siblings = model.children(indexedProcessTree, uniquePidForProcess(parent)); const isFirstChild = process === siblings[0]; if (metadata.isOnlyChild) { @@ -420,7 +423,7 @@ function* levelOrderWithWidths( parentWidth, }; - const siblings = model.children(tree, parent); + const siblings = model.children(tree, uniquePidForProcess(parent)); if (siblings.length === 1) { metadata.isOnlyChild = true; metadata.lastChildWidth = width; diff --git a/x-pack/plugins/security_solution/public/resolver/store/camera/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/camera/selectors.ts index 86d934bd95663..baa049ba42f92 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/camera/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/camera/selectors.ts @@ -164,7 +164,8 @@ export const scale: (state: CameraState) => (time: number) => Vector2 = createSe scalingConstants.maximum ); - return (time) => { + // memoizing this so the vector returned will be the same object reference if called with the same `time`. + return defaultMemoize((time) => { /** * If the animation has completed, return the `scaleNotCountingAnimation`, as * the animation always completes with the scale set back at starting value. @@ -247,12 +248,13 @@ export const scale: (state: CameraState) => (time: number) => Vector2 = createSe */ return [lerpedScale, lerpedScale]; } - }; + }); } else { /** * The scale should be the same in both axes. + * Memoizing this so the vector returned will be the same object reference every time. */ - return () => [scaleNotCountingAnimation, scaleNotCountingAnimation]; + return defaultMemoize(() => [scaleNotCountingAnimation, scaleNotCountingAnimation]); } /** @@ -277,22 +279,26 @@ export const clippingPlanes: ( ) => (time: number) => ClippingPlanes = createSelector( (state) => state.rasterSize, scale, - (rasterSize, scaleAtTime) => (time: number) => { - const [scaleX, scaleY] = scaleAtTime(time); - const renderWidth = rasterSize[0]; - const renderHeight = rasterSize[1]; - const clippingPlaneRight = renderWidth / 2 / scaleX; - const clippingPlaneTop = renderHeight / 2 / scaleY; - - return { - renderWidth, - renderHeight, - clippingPlaneRight, - clippingPlaneTop, - clippingPlaneLeft: -clippingPlaneRight, - clippingPlaneBottom: -clippingPlaneTop, - }; - } + (rasterSize, scaleAtTime) => + /** + * memoizing this for object reference equality. + */ + defaultMemoize((time: number) => { + const [scaleX, scaleY] = scaleAtTime(time); + const renderWidth = rasterSize[0]; + const renderHeight = rasterSize[1]; + const clippingPlaneRight = renderWidth / 2 / scaleX; + const clippingPlaneTop = renderHeight / 2 / scaleY; + + return { + renderWidth, + renderHeight, + clippingPlaneRight, + clippingPlaneTop, + clippingPlaneLeft: -clippingPlaneRight, + clippingPlaneBottom: -clippingPlaneTop, + }; + }) ); /** @@ -323,7 +329,10 @@ export const translation: (state: CameraState) => (time: number) => Vector2 = cr scale, (state) => state.animation, (panning, translationNotCountingCurrentPanning, scaleAtTime, animation) => { - return (time: number) => { + /** + * Memoizing this for object reference equality. + */ + return defaultMemoize((time: number) => { const [scaleX, scaleY] = scaleAtTime(time); if (animation !== undefined && animationIsActive(animation, time)) { return vector2.lerp( @@ -343,7 +352,7 @@ export const translation: (state: CameraState) => (time: number) => Vector2 = cr } else { return translationNotCountingCurrentPanning; } - }; + }); } ); @@ -357,7 +366,10 @@ export const inverseProjectionMatrix: ( clippingPlanes, translation, (clippingPlanesAtTime, translationAtTime) => { - return (time: number) => { + /** + * Memoizing this for object reference equality (and reduced memory churn.) + */ + return defaultMemoize((time: number) => { const { renderWidth, renderHeight, @@ -404,7 +416,7 @@ export const inverseProjectionMatrix: ( translateForCamera, multiply(scaleToClippingPlaneDimensions, multiply(invertY, screenToNDC)) ); - }; + }); } ); @@ -415,7 +427,8 @@ export const viewableBoundingBox: (state: CameraState) => (time: number) => AABB clippingPlanes, inverseProjectionMatrix, (clippingPlanesAtTime, matrixAtTime) => { - return (time: number) => { + // memoizing this so the AABB returned will be the same object reference if called with the same `time`. + return defaultMemoize((time: number) => { const { renderWidth, renderHeight } = clippingPlanesAtTime(time); const matrix = matrixAtTime(time); const bottomLeftCorner: Vector2 = [0, renderHeight]; @@ -424,7 +437,7 @@ export const viewableBoundingBox: (state: CameraState) => (time: number) => AABB minimum: vector2.applyMatrix3(bottomLeftCorner, matrix), maximum: vector2.applyMatrix3(topRightCorner, matrix), }; - }; + }); } ); @@ -436,6 +449,8 @@ export const projectionMatrix: (state: CameraState) => (time: number) => Matrix3 clippingPlanes, translation, (clippingPlanesAtTime, translationAtTime) => { + // memoizing this so the matrix returned will be the same object reference if called with the same `time`. + // this should also save on some memory allocation return defaultMemoize((time: number) => { const { renderWidth, diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.test.ts b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.test.ts index cf23596db6134..683f8f1a5f84a 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.test.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.test.ts @@ -9,6 +9,13 @@ import { DataState } from '../../types'; import { dataReducer } from './reducer'; import { DataAction } from './action'; import { createStore } from 'redux'; +import { + mockTreeWithNoAncestorsAnd2Children, + mockTreeWith2AncestorsAndNoChildren, +} from '../mocks/resolver_tree'; +import { uniquePidForProcess } from '../../models/process_event'; +import { EndpointEvent } from '../../../../common/endpoint/types'; + describe('data state', () => { let actions: DataAction[] = []; @@ -263,4 +270,87 @@ describe('data state', () => { }); }); }); + describe('with a tree with no descendants and 2 ancestors', () => { + const originID = 'c'; + const firstAncestorID = 'b'; + const secondAncestorID = 'a'; + beforeEach(() => { + actions.push({ + type: 'serverReturnedResolverData', + payload: { + result: mockTreeWith2AncestorsAndNoChildren({ + originID, + firstAncestorID, + secondAncestorID, + }), + // this value doesn't matter + databaseDocumentID: '', + }, + }); + }); + it('should have no flowto candidate for the origin', () => { + expect(selectors.ariaFlowtoCandidate(state())(originID)).toBe(null); + }); + it('should have no flowto candidate for the first ancestor', () => { + expect(selectors.ariaFlowtoCandidate(state())(firstAncestorID)).toBe(null); + }); + it('should have no flowto candidate for the second ancestor ancestor', () => { + expect(selectors.ariaFlowtoCandidate(state())(secondAncestorID)).toBe(null); + }); + }); + describe('with a tree with 2 children and no ancestors', () => { + const originID = 'c'; + const firstChildID = 'd'; + const secondChildID = 'e'; + beforeEach(() => { + actions.push({ + type: 'serverReturnedResolverData', + payload: { + result: mockTreeWithNoAncestorsAnd2Children({ originID, firstChildID, secondChildID }), + // this value doesn't matter + databaseDocumentID: '', + }, + }); + }); + it('should have no flowto candidate for the origin', () => { + expect(selectors.ariaFlowtoCandidate(state())(originID)).toBe(null); + }); + it('should use the second child as the flowto candidate for the first child', () => { + expect(selectors.ariaFlowtoCandidate(state())(firstChildID)).toBe(secondChildID); + }); + it('should have no flowto candidate for the second child', () => { + expect(selectors.ariaFlowtoCandidate(state())(secondChildID)).toBe(null); + }); + }); + describe('with a tree where the root process has no parent info at all', () => { + const originID = 'c'; + const firstChildID = 'd'; + const secondChildID = 'e'; + beforeEach(() => { + const tree = mockTreeWithNoAncestorsAnd2Children({ originID, firstChildID, secondChildID }); + for (const event of tree.lifecycle) { + // delete the process.parent key, if present + // cast as `EndpointEvent` because `ResolverEvent` can also be `LegacyEndpointEvent` which has no `process` field + delete (event as EndpointEvent).process?.parent; + } + + actions.push({ + type: 'serverReturnedResolverData', + payload: { + result: tree, + // this value doesn't matter + databaseDocumentID: '', + }, + }); + }); + it('should be able to calculate the aria flowto candidates for all processes nodes', () => { + const graphables = selectors.graphableProcesses(state()); + expect(graphables.length).toBe(3); + for (const event of graphables) { + expect(() => { + selectors.ariaFlowtoCandidate(state())(uniquePidForProcess(event)); + }).not.toThrow(); + } + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts index dc17fc70ef8af..109b3abddcc77 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts @@ -19,9 +19,9 @@ import { isGraphableProcess, isTerminatedProcess, uniquePidForProcess, + uniqueParentPidForProcess, } from '../../models/process_event'; import * as indexedProcessTreeModel from '../../models/indexed_process_tree'; -import { isEqual } from '../../models/aabb'; import { ResolverEvent, @@ -62,7 +62,7 @@ export function hasError(state: DataState): boolean { * The last ResolverTree we received, if any. It may be stale (it might not be for the same databaseDocumentID that * we're currently interested in. */ -const resolverTree = (state: DataState): ResolverTree | undefined => { +const resolverTreeResponse = (state: DataState): ResolverTree | undefined => { if (state.lastResponse && state.lastResponse.successful) { return state.lastResponse.result; } else { @@ -73,7 +73,9 @@ const resolverTree = (state: DataState): ResolverTree | undefined => { /** * Process events that will be displayed as terminated. */ -export const terminatedProcesses = createSelector(resolverTree, function (tree?: ResolverTree) { +export const terminatedProcesses = createSelector(resolverTreeResponse, function ( + tree?: ResolverTree +) { if (!tree) { return new Set(); } @@ -90,7 +92,7 @@ export const terminatedProcesses = createSelector(resolverTree, function (tree?: /** * Process events that will be graphed. */ -export const graphableProcesses = createSelector(resolverTree, function (tree?) { +export const graphableProcesses = createSelector(resolverTreeResponse, function (tree?) { if (tree) { return resolverTreeModel.lifecycleEvents(tree).filter(isGraphableProcess); } else { @@ -101,7 +103,7 @@ export const graphableProcesses = createSelector(resolverTree, function (tree?) /** * The 'indexed process tree' contains the tree data, indexed in helpful ways. Used for O(1) access to stuff during graph layout. */ -export const indexedProcessTree = createSelector(graphableProcesses, function indexedTree( +export const tree = createSelector(graphableProcesses, function indexedTree( /* eslint-disable no-shadow */ graphableProcesses /* eslint-enable no-shadow */ @@ -114,13 +116,16 @@ export const indexedProcessTree = createSelector(graphableProcesses, function in */ export const relatedEventsStats: ( state: DataState -) => Map | null = createSelector(resolverTree, (tree?: ResolverTree) => { - if (tree) { - return resolverTreeModel.relatedEventsStats(tree); - } else { - return null; +) => Map | null = createSelector( + resolverTreeResponse, + (resolverTree?: ResolverTree) => { + if (resolverTree) { + return resolverTreeModel.relatedEventsStats(resolverTree); + } else { + return null; + } } -}); +); /** * returns a map of entity_ids to related event data. @@ -133,7 +138,9 @@ export function relatedEventsByEntityId(data: DataState): Map (entityID: string) => (ecsCategory: string) => ResolverEvent[] = createSelector( relatedEventsByEntityId, function provideGettersByCategory( /* eslint-disable no-shadow */ @@ -173,16 +180,16 @@ export function relatedEventsReady(data: DataState): Map { * `true` if there were more children than we got in the last request. */ export function hasMoreChildren(state: DataState): boolean { - const tree = resolverTree(state); - return tree ? resolverTreeModel.hasMoreChildren(tree) : false; + const resolverTree = resolverTreeResponse(state); + return resolverTree ? resolverTreeModel.hasMoreChildren(resolverTree) : false; } /** * `true` if there were more ancestors than we got in the last request. */ export function hasMoreAncestors(state: DataState): boolean { - const tree = resolverTree(state); - return tree ? resolverTreeModel.hasMoreAncestors(tree) : false; + const resolverTree = resolverTreeResponse(state); + return resolverTree ? resolverTreeModel.hasMoreAncestors(resolverTree) : false; } interface RelatedInfoFunctions { @@ -248,7 +255,7 @@ export const relatedEventInfoByEntityId: ( }); }; - const matchingEventsForCategory = defaultMemoize(unmemoizedMatchingEventsForCategory); + const matchingEventsForCategory = unmemoizedMatchingEventsForCategory; /** * The number of events that occurred before the API limit was reached. @@ -313,16 +320,13 @@ export function databaseDocumentIDToFetch(state: DataState): string | null { } } -export const layout = createSelector( - indexedProcessTree, - function processNodePositionsAndEdgeLineSegments( - /* eslint-disable no-shadow */ - indexedProcessTree - /* eslint-enable no-shadow */ - ) { - return isometricTaxiLayout(indexedProcessTree); - } -); +export const layout = createSelector(tree, function processNodePositionsAndEdgeLineSegments( + /* eslint-disable no-shadow */ + indexedProcessTree + /* eslint-enable no-shadow */ +) { + return isometricTaxiLayout(indexedProcessTree); +}); /** * Given a nodeID (aka entity_id) get the indexed process event. @@ -332,8 +336,9 @@ export const layout = createSelector( export const processEventForID: ( state: DataState ) => (nodeID: string) => ResolverEvent | null = createSelector( - indexedProcessTree, - (tree) => (nodeID: string) => indexedProcessTreeModel.processEvent(tree, nodeID) + tree, + (indexedProcessTree) => (nodeID: string) => + indexedProcessTreeModel.processEvent(indexedProcessTree, nodeID) ); /** @@ -349,30 +354,66 @@ export const ariaLevel: (state: DataState) => (nodeID: string) => number | null ); /** - * Returns the following sibling if there is one, or `null`. + * Returns the following sibling if there is one, or `null` if there isn't. + * For root nodes, other root nodes are treated as siblings. + * This is used to calculate the `aria-flowto` attribute. */ -export const followingSibling: ( +export const ariaFlowtoCandidate: ( state: DataState ) => (nodeID: string) => string | null = createSelector( - indexedProcessTree, + tree, processEventForID, - (tree, eventGetter) => { - return (nodeID: string) => { - const event = eventGetter(nodeID); + (indexedProcessTree, eventGetter) => { + // A map of preceding sibling IDs to following sibling IDs or `null`, if there is no following sibling + const memo: Map = new Map(); - // event not found - if (event === null) { - return null; + return function memoizedGetter(/** the unique ID of a node. **/ nodeID: string): string | null { + // Previous calculations are memoized. Check for a value in the memo. + const existingValue = memo.get(nodeID); + + /** + * `undefined` means the key wasn't in the map. + * Note: the value may be null, meaning that we checked and there is no following sibling. + * If there is a value in the map, return it. + */ + if (existingValue !== undefined) { + return existingValue; } - const nextSibling = indexedProcessTreeModel.nextSibling(tree, event); - // next sibling not found - if (nextSibling === undefined) { - return null; + /** + * Getting the following sibling of a node has an `O(n)` time complexity where `n` is the number of children the parent of the node has. + * For this reason, we calculate the following siblings of the node and all of its siblings at once and cache them. + */ + const nodeEvent: ResolverEvent | null = eventGetter(nodeID); + + if (!nodeEvent) { + // this should never happen. + throw new Error('could not find child event in process tree.'); + } + + // nodes with the same parent ID + const children = indexedProcessTreeModel.children( + indexedProcessTree, + uniqueParentPidForProcess(nodeEvent) + ); + + let previousChild: ResolverEvent | null = null; + // Loop over all nodes that have the same parent ID (even if the parent ID is undefined or points to a node that isn't in the tree.) + for (const child of children) { + if (previousChild !== null) { + // Set the `child` as the following sibling of `previousChild`. + memo.set(uniquePidForProcess(previousChild), uniquePidForProcess(child)); + } + // Set the child as the previous child. + previousChild = child; + } + + if (previousChild) { + // if there is a previous child, it has no following sibling. + memo.set(uniquePidForProcess(previousChild), null); } - // return the node ID - return uniquePidForProcess(nextSibling); + return memoizedGetter(nodeID); }; } ); @@ -385,7 +426,7 @@ const spatiallyIndexedLayout: (state: DataState) => rbush = creat edgeLineSegments, /* eslint-enable no-shadow */ }) { - const tree: rbush = new rbush(); + const spatialIndex: rbush = new rbush(); const processesToIndex: IndexedProcessNode[] = []; const edgeLineSegmentsToIndex: IndexedEdgeLineSegment[] = []; @@ -421,50 +462,49 @@ const spatiallyIndexedLayout: (state: DataState) => rbush = creat }; edgeLineSegmentsToIndex.push(indexedLineSegment); } - tree.load([...processesToIndex, ...edgeLineSegmentsToIndex]); - return tree; + spatialIndex.load([...processesToIndex, ...edgeLineSegmentsToIndex]); + return spatialIndex; } ); +/** + * Returns nodes and edge lines that could be visible in the `query`. + */ export const nodesAndEdgelines: ( state: DataState -) => (query: AABB) => VisibleEntites = createSelector(spatiallyIndexedLayout, function (tree) { - // memoize the results of this call to avoid unnecessarily rerunning - let lastBoundingBox: AABB | null = null; - let currentlyVisible: VisibleEntites = { - processNodePositions: new Map(), - connectingEdgeLineSegments: [], - }; - return (boundingBox: AABB) => { - if (lastBoundingBox !== null && isEqual(lastBoundingBox, boundingBox)) { - return currentlyVisible; - } else { - const { - minimum: [minX, minY], - maximum: [maxX, maxY], - } = boundingBox; - const entities = tree.search({ - minX, - minY, - maxX, - maxY, - }); - const visibleProcessNodePositions = new Map( - entities - .filter((entity): entity is IndexedProcessNode => entity.type === 'processNode') - .map((node) => [node.entity, node.position]) - ); - const connectingEdgeLineSegments = entities - .filter((entity): entity is IndexedEdgeLineSegment => entity.type === 'edgeLine') - .map((node) => node.entity); - currentlyVisible = { - processNodePositions: visibleProcessNodePositions, - connectingEdgeLineSegments, - }; - lastBoundingBox = boundingBox; - return currentlyVisible; - } - }; +) => ( + /** + * An axis aligned bounding box (in world corrdinates) to search in. Any entities that might collide with this box will be returned. + */ + query: AABB +) => VisibleEntites = createSelector(spatiallyIndexedLayout, function (spatialIndex) { + /** + * Memoized for performance and object reference equality. + */ + return defaultMemoize((boundingBox: AABB) => { + const { + minimum: [minX, minY], + maximum: [maxX, maxY], + } = boundingBox; + const entities = spatialIndex.search({ + minX, + minY, + maxX, + maxY, + }); + const visibleProcessNodePositions = new Map( + entities + .filter((entity): entity is IndexedProcessNode => entity.type === 'processNode') + .map((node) => [node.entity, node.position]) + ); + const connectingEdgeLineSegments = entities + .filter((entity): entity is IndexedEdgeLineSegment => entity.type === 'edgeLine') + .map((node) => node.entity); + return { + processNodePositions: visibleProcessNodePositions, + connectingEdgeLineSegments, + }; + }); }); /** diff --git a/x-pack/plugins/security_solution/public/resolver/store/mocks/endpoint_event.ts b/x-pack/plugins/security_solution/public/resolver/store/mocks/endpoint_event.ts new file mode 100644 index 0000000000000..b58ea73e1fdc7 --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/store/mocks/endpoint_event.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EndpointEvent } from '../../../../common/endpoint/types'; + +/** + * Simple mock endpoint event that works for tree layouts. + */ +export function mockEndpointEvent({ + entityID, + name, + parentEntityId, + timestamp, +}: { + entityID: string; + name: string; + parentEntityId: string | undefined; + timestamp: number; +}): EndpointEvent { + return { + '@timestamp': timestamp, + event: { + type: 'start', + category: 'process', + }, + process: { + entity_id: entityID, + name, + parent: { + entity_id: parentEntityId, + }, + }, + } as EndpointEvent; +} diff --git a/x-pack/plugins/security_solution/public/resolver/store/mocks/resolver_tree.ts b/x-pack/plugins/security_solution/public/resolver/store/mocks/resolver_tree.ts new file mode 100644 index 0000000000000..862cf47f73947 --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/store/mocks/resolver_tree.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { mockEndpointEvent } from './endpoint_event'; +import { ResolverTree, ResolverEvent } from '../../../../common/endpoint/types'; + +export function mockTreeWith2AncestorsAndNoChildren({ + originID, + firstAncestorID, + secondAncestorID, +}: { + secondAncestorID: string; + firstAncestorID: string; + originID: string; +}): ResolverTree { + const secondAncestor: ResolverEvent = mockEndpointEvent({ + entityID: secondAncestorID, + name: 'a', + parentEntityId: 'none', + timestamp: 0, + }); + const firstAncestor: ResolverEvent = mockEndpointEvent({ + entityID: firstAncestorID, + name: 'b', + parentEntityId: secondAncestorID, + timestamp: 1, + }); + const originEvent: ResolverEvent = mockEndpointEvent({ + entityID: originID, + name: 'c', + parentEntityId: firstAncestorID, + timestamp: 2, + }); + return ({ + entityID: originID, + children: { + childNodes: [], + }, + ancestry: { + ancestors: [{ lifecycle: [secondAncestor] }, { lifecycle: [firstAncestor] }], + }, + lifecycle: [originEvent], + } as unknown) as ResolverTree; +} + +export function mockTreeWithNoAncestorsAnd2Children({ + originID, + firstChildID, + secondChildID, +}: { + originID: string; + firstChildID: string; + secondChildID: string; +}): ResolverTree { + const origin: ResolverEvent = mockEndpointEvent({ + entityID: originID, + name: 'c', + parentEntityId: 'none', + timestamp: 0, + }); + const firstChild: ResolverEvent = mockEndpointEvent({ + entityID: firstChildID, + name: 'd', + parentEntityId: originID, + timestamp: 1, + }); + const secondChild: ResolverEvent = mockEndpointEvent({ + entityID: secondChildID, + name: 'e', + parentEntityId: originID, + timestamp: 2, + }); + + return ({ + entityID: originID, + children: { + childNodes: [{ lifecycle: [firstChild] }, { lifecycle: [secondChild] }], + }, + ancestry: { + ancestors: [], + }, + lifecycle: [origin], + } as unknown) as ResolverTree; +} diff --git a/x-pack/plugins/security_solution/public/resolver/store/selectors.test.ts b/x-pack/plugins/security_solution/public/resolver/store/selectors.test.ts index ba4a5a169c549..df365a078b27f 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/selectors.test.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/selectors.test.ts @@ -9,7 +9,10 @@ import { createStore } from 'redux'; import { ResolverAction } from './actions'; import { resolverReducer } from './reducer'; import * as selectors from './selectors'; -import { EndpointEvent, ResolverEvent, ResolverTree } from '../../../common/endpoint/types'; +import { + mockTreeWith2AncestorsAndNoChildren, + mockTreeWithNoAncestorsAnd2Children, +} from './mocks/resolver_tree'; describe('resolver selectors', () => { const actions: ResolverAction[] = []; @@ -33,7 +36,7 @@ describe('resolver selectors', () => { actions.push({ type: 'serverReturnedResolverData', payload: { - result: treeWith2AncestorsAndNoChildren({ + result: mockTreeWith2AncestorsAndNoChildren({ originID, firstAncestorID, secondAncestorID, @@ -71,7 +74,7 @@ describe('resolver selectors', () => { actions.push({ type: 'serverReturnedResolverData', payload: { - result: treeWithNoAncestorsAnd2Children({ originID, firstChildID, secondChildID }), + result: mockTreeWithNoAncestorsAnd2Children({ originID, firstChildID, secondChildID }), // this value doesn't matter databaseDocumentID: '', }, @@ -149,111 +152,3 @@ describe('resolver selectors', () => { }); }); }); -/** - * Simple mock endpoint event that works for tree layouts. - */ -function mockEndpointEvent({ - entityID, - name, - parentEntityId, - timestamp, -}: { - entityID: string; - name: string; - parentEntityId: string; - timestamp: number; -}): EndpointEvent { - return { - '@timestamp': timestamp, - event: { - type: 'start', - category: 'process', - }, - process: { - entity_id: entityID, - name, - parent: { - entity_id: parentEntityId, - }, - }, - } as EndpointEvent; -} - -function treeWith2AncestorsAndNoChildren({ - originID, - firstAncestorID, - secondAncestorID, -}: { - secondAncestorID: string; - firstAncestorID: string; - originID: string; -}): ResolverTree { - const secondAncestor: ResolverEvent = mockEndpointEvent({ - entityID: secondAncestorID, - name: 'a', - parentEntityId: 'none', - timestamp: 0, - }); - const firstAncestor: ResolverEvent = mockEndpointEvent({ - entityID: firstAncestorID, - name: 'b', - parentEntityId: secondAncestorID, - timestamp: 1, - }); - const originEvent: ResolverEvent = mockEndpointEvent({ - entityID: originID, - name: 'c', - parentEntityId: firstAncestorID, - timestamp: 2, - }); - return ({ - entityID: originID, - children: { - childNodes: [], - }, - ancestry: { - ancestors: [{ lifecycle: [secondAncestor] }, { lifecycle: [firstAncestor] }], - }, - lifecycle: [originEvent], - } as unknown) as ResolverTree; -} - -function treeWithNoAncestorsAnd2Children({ - originID, - firstChildID, - secondChildID, -}: { - originID: string; - firstChildID: string; - secondChildID: string; -}): ResolverTree { - const origin: ResolverEvent = mockEndpointEvent({ - entityID: originID, - name: 'c', - parentEntityId: 'none', - timestamp: 0, - }); - const firstChild: ResolverEvent = mockEndpointEvent({ - entityID: firstChildID, - name: 'd', - parentEntityId: originID, - timestamp: 1, - }); - const secondChild: ResolverEvent = mockEndpointEvent({ - entityID: secondChildID, - name: 'e', - parentEntityId: originID, - timestamp: 2, - }); - - return ({ - entityID: originID, - children: { - childNodes: [{ lifecycle: [firstChild] }, { lifecycle: [secondChild] }], - }, - ancestry: { - ancestors: [], - }, - lifecycle: [origin], - } as unknown) as ResolverTree; -} diff --git a/x-pack/plugins/security_solution/public/resolver/store/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/selectors.ts index ff2179dc3a2ae..040e2920ce554 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/selectors.ts @@ -212,17 +212,6 @@ export const graphableProcesses = composeSelectors( dataSelectors.graphableProcesses ); -/** - * Calls the `secondSelector` with the result of the `selector`. Use this when re-exporting a - * concern-specific selector. `selector` should return the concern-specific state. - */ -function composeSelectors( - selector: (state: OuterState) => InnerState, - secondSelector: (state: InnerState) => ReturnValue -): (state: OuterState) => ReturnValue { - return (state) => secondSelector(selector(state)); -} - const boundingBox = composeSelectors(cameraStateSelector, cameraSelectors.viewableBoundingBox); const nodesAndEdgelines = composeSelectors(dataStateSelector, dataSelectors.nodesAndEdgelines); @@ -246,6 +235,7 @@ export const visibleNodesAndEdgeLines = createSelector(nodesAndEdgelines, boundi boundingBox /* eslint-enable no-shadow */ ) { + // `boundingBox` and `nodesAndEdgelines` are each memoized. return (time: number) => nodesAndEdgelines(boundingBox(time)); }); @@ -261,14 +251,14 @@ export const ariaLevel: ( /** * Takes a nodeID (aka entity_id) and returns the node ID of the node that aria should 'flowto' or null - * If the node has a following sibling that is currently visible, that will be returned, otherwise null. + * If the node has a flowto candidate that is currently visible, that will be returned, otherwise null. */ export const ariaFlowtoNodeID: ( state: ResolverState ) => (time: number) => (nodeID: string) => string | null = createSelector( visibleNodesAndEdgeLines, - composeSelectors(dataStateSelector, dataSelectors.followingSibling), - (visibleNodesAndEdgeLinesAtTime, followingSibling) => { + composeSelectors(dataStateSelector, dataSelectors.ariaFlowtoCandidate), + (visibleNodesAndEdgeLinesAtTime, ariaFlowtoCandidate) => { return defaultMemoize((time: number) => { // get the visible nodes at `time` const { processNodePositions } = visibleNodesAndEdgeLinesAtTime(time); @@ -280,10 +270,23 @@ export const ariaFlowtoNodeID: ( // return the ID of `nodeID`'s following sibling, if it is visible return (nodeID: string): string | null => { - const sibling: string | null = followingSibling(nodeID); + const flowtoNode: string | null = ariaFlowtoCandidate(nodeID); - return sibling === null || nodesVisibleAtTime.has(sibling) === false ? null : sibling; + return flowtoNode === null || nodesVisibleAtTime.has(flowtoNode) === false + ? null + : flowtoNode; }; }); } ); + +/** + * Calls the `secondSelector` with the result of the `selector`. Use this when re-exporting a + * concern-specific selector. `selector` should return the concern-specific state. + */ +function composeSelectors( + selector: (state: OuterState) => InnerState, + secondSelector: (state: InnerState) => ReturnValue +): (state: OuterState) => ReturnValue { + return (state) => secondSelector(selector(state)); +} diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_related_detail.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_related_detail.tsx index 4544381d94955..10e57a09b5da4 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_related_detail.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_related_detail.tsx @@ -169,7 +169,9 @@ export const RelatedEventDetail = memo(function RelatedEventDetail({ process, ...relevantData } = relatedEventToShowDetailsFor as ResolverEvent & { + // Type this with various unknown keys so that ts will let us delete those keys ecs: unknown; + process: unknown; }; let displayDate = ''; const sectionData: Array<{ @@ -371,4 +373,3 @@ export const RelatedEventDetail = memo(function RelatedEventDetail({ ); }); -RelatedEventDetail.displayName = 'RelatedEventDetail'; From a894e5ad7d5420690ee569ed55d0fe8f8299870b Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Mon, 20 Jul 2020 15:06:54 +0100 Subject: [PATCH 30/45] allow user to disable alert even if they dont have privileges to the underlying action --- x-pack/plugins/alerts/server/alerts_client.ts | 4 ---- .../security_and_spaces/tests/alerting/disable.ts | 9 --------- 2 files changed, 13 deletions(-) diff --git a/x-pack/plugins/alerts/server/alerts_client.ts b/x-pack/plugins/alerts/server/alerts_client.ts index 1f286b42c1449..eec60f924bf38 100644 --- a/x-pack/plugins/alerts/server/alerts_client.ts +++ b/x-pack/plugins/alerts/server/alerts_client.ts @@ -613,10 +613,6 @@ export class AlertsClient { WriteOperations.Disable ); - if (attributes.actions.length) { - await this.actionsAuthorization.ensureAuthorized('execute'); - } - if (attributes.enabled === true) { await this.unsecuredSavedObjectsClient.update( 'alert', 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 3a732424853a0..4e4f9053bd24f 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 @@ -90,15 +90,6 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte await getScheduledTask(createdAlert.scheduledTaskId); break; case 'space_1_all_alerts_none_actions at space1': - expect(response.statusCode).to.eql(403); - expect(response.body).to.eql({ - error: 'Forbidden', - message: `Unauthorized to execute actions`, - statusCode: 403, - }); - // Ensure task still exists - await getScheduledTask(createdAlert.scheduledTaskId); - break; case 'superuser at space1': case 'space_1_all at space1': case 'space_1_all_with_restricted_fixture at space1': From 7ac5fc4e1f0b184cd60fc6d12f0dc058a83fe2db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Kopyci=C5=84ski?= Date: Mon, 20 Jul 2020 16:14:13 +0200 Subject: [PATCH 31/45] =?UTF-8?q?[Security=20Solution][Timeline]=20Fix=20t?= =?UTF-8?q?imeline=20styling=20and=20createFrom=20beh=E2=80=A6=20(#72152)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../security_solution/common/constants.ts | 5 - .../components/alerts_table/helpers.ts | 2 +- .../security_solution/public/graphql/types.ts | 8 + .../components/open_timeline/helpers.test.ts | 197 ++++++++++++++++++ .../components/open_timeline/helpers.ts | 58 ++++-- .../open_timeline/open_timeline.tsx | 3 +- .../open_timeline_modal/index.tsx | 2 +- .../open_timeline_modal_body.tsx | 66 +++--- .../open_timeline/use_timeline_status.tsx | 4 +- .../data_providers/provider_badge.tsx | 2 +- .../timelines/components/timeline/styles.tsx | 3 +- .../containers/one/index.gql_query.ts | 1 + .../timelines/containers/persist.gql_query.ts | 3 + .../public/timelines/store/timeline/epic.ts | 3 + .../server/lib/timeline/saved_object.ts | 8 +- .../saved_objects/timeline.ts | 1 + 16 files changed, 297 insertions(+), 69 deletions(-) diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index b39a038c4cc3c..f934d90c740a5 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -167,8 +167,3 @@ export const showAllOthersBucket: string[] = [ 'destination.ip', 'user.name', ]; - -/* - * This should be set to true after https://github.com/elastic/kibana/pull/67496 is merged - */ -export const enableElasticFilter = false; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/helpers.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_table/helpers.ts index 5025d782e2aa2..084e4bff7e0ac 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/helpers.ts +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/helpers.ts @@ -148,7 +148,7 @@ export const reformatDataProviderWithNewValue = { // Support for legacy "template-like" timeline behavior that is using hardcoded list of templateFields - if (timelineType === TimelineType.default) { + if (timelineType !== TimelineType.template) { if (templateFields.includes(dataProvider.queryMatch.field)) { const newValue = getStringArray(dataProvider.queryMatch.field, ecsData); if (newValue.length) { diff --git a/x-pack/plugins/security_solution/public/graphql/types.ts b/x-pack/plugins/security_solution/public/graphql/types.ts index 5f8595df23f9b..1808d32547fbe 100644 --- a/x-pack/plugins/security_solution/public/graphql/types.ts +++ b/x-pack/plugins/security_solution/public/graphql/types.ts @@ -5654,6 +5654,8 @@ export namespace GetOneTimeline { kqlQuery: Maybe; + type: Maybe; + queryMatch: Maybe<_QueryMatch>; }; @@ -5870,6 +5872,8 @@ export namespace PersistTimelineMutation { eventType: Maybe; + excludedRowRendererIds: Maybe; + favorite: Maybe; filters: Maybe; @@ -5932,6 +5936,8 @@ export namespace PersistTimelineMutation { kqlQuery: Maybe; + type: Maybe; + queryMatch: Maybe; and: Maybe; @@ -5964,6 +5970,8 @@ export namespace PersistTimelineMutation { kqlQuery: Maybe; + type: Maybe; + queryMatch: Maybe<_QueryMatch>; }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts index 5759d96b95f9e..f4bd17005fed7 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts @@ -306,6 +306,203 @@ describe('helpers', () => { width: 1100, }); }); + + test('if duplicates and timeline.timelineType is not matching with outcome timelineType it should return draft with empty title', () => { + const timeline = { + savedObjectId: 'savedObject-1', + title: 'Awesome Timeline', + version: '1', + status: TimelineStatus.active, + timelineType: TimelineType.default, + }; + + const newTimeline = defaultTimelineToTimelineModel(timeline, false, TimelineType.template); + expect(newTimeline).toEqual({ + columns: [ + { + columnHeaderType: 'not-filtered', + id: '@timestamp', + width: 190, + }, + { + columnHeaderType: 'not-filtered', + id: 'message', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'event.category', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'event.action', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'host.name', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'source.ip', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'destination.ip', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'user.name', + width: 180, + }, + ], + dataProviders: [], + dateRange: { start: '2020-07-07T08:20:18.966Z', end: '2020-07-08T08:20:18.966Z' }, + description: '', + deletedEventIds: [], + eventIdToNoteIds: {}, + eventType: 'all', + excludedRowRendererIds: [], + filters: [], + highlightedDropAndProviderId: '', + historyIds: [], + id: 'savedObject-1', + isFavorite: false, + isLive: false, + isSelectAllChecked: false, + isLoading: false, + isSaving: false, + itemsPerPage: 25, + itemsPerPageOptions: [10, 25, 50, 100], + kqlMode: 'filter', + kqlQuery: { + filterQuery: null, + filterQueryDraft: null, + }, + loadingEventIds: [], + noteIds: [], + pinnedEventIds: {}, + pinnedEventsSaveObject: {}, + savedObjectId: 'savedObject-1', + selectedEventIds: {}, + show: false, + showCheckboxes: false, + sort: { + columnId: '@timestamp', + sortDirection: 'desc', + }, + status: TimelineStatus.draft, + title: '', + timelineType: TimelineType.template, + templateTimelineId: null, + templateTimelineVersion: null, + version: '1', + width: 1100, + }); + }); + + test('if duplicates and timeline.timelineType is not matching with outcome timelineType it should return draft with empty title template', () => { + const timeline = { + savedObjectId: 'savedObject-1', + title: 'Awesome Template', + version: '1', + status: TimelineStatus.active, + timelineType: TimelineType.template, + }; + + const newTimeline = defaultTimelineToTimelineModel(timeline, false, TimelineType.default); + expect(newTimeline).toEqual({ + columns: [ + { + columnHeaderType: 'not-filtered', + id: '@timestamp', + width: 190, + }, + { + columnHeaderType: 'not-filtered', + id: 'message', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'event.category', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'event.action', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'host.name', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'source.ip', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'destination.ip', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'user.name', + width: 180, + }, + ], + dataProviders: [], + dateRange: { start: '2020-07-07T08:20:18.966Z', end: '2020-07-08T08:20:18.966Z' }, + description: '', + deletedEventIds: [], + eventIdToNoteIds: {}, + eventType: 'all', + excludedRowRendererIds: [], + filters: [], + highlightedDropAndProviderId: '', + historyIds: [], + id: 'savedObject-1', + isFavorite: false, + isLive: false, + isSelectAllChecked: false, + isLoading: false, + isSaving: false, + itemsPerPage: 25, + itemsPerPageOptions: [10, 25, 50, 100], + kqlMode: 'filter', + kqlQuery: { + filterQuery: null, + filterQueryDraft: null, + }, + loadingEventIds: [], + noteIds: [], + pinnedEventIds: {}, + pinnedEventsSaveObject: {}, + savedObjectId: 'savedObject-1', + selectedEventIds: {}, + show: false, + showCheckboxes: false, + sort: { + columnId: '@timestamp', + sortDirection: 'desc', + }, + status: TimelineStatus.draft, + title: '', + timelineType: TimelineType.default, + templateTimelineId: null, + templateTimelineVersion: null, + version: '1', + width: 1100, + }); + }); + test('if columns are null, we should get the default columns', () => { const timeline = { savedObjectId: 'savedObject-1', diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts index 9899b38f445f9..af289f94c9a0d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts @@ -173,10 +173,6 @@ const getTemplateTimelineId = ( duplicate: boolean, targetTimelineType?: TimelineType ) => { - if (!duplicate) { - return timeline.templateTimelineId; - } - if ( targetTimelineType === TimelineType.default && timeline.timelineType === TimelineType.template @@ -184,18 +180,26 @@ const getTemplateTimelineId = ( return timeline.templateTimelineId; } - // TODO: MOVE TO BACKEND - return uuid.v4(); + return duplicate && timeline.timelineType === TimelineType.template + ? // TODO: MOVE TO THE BACKEND + uuid.v4() + : timeline.templateTimelineId; }; -const convertToDefaultField = ({ and, ...dataProvider }: DataProviderResult) => - deepMerge(dataProvider, { - type: DataProviderType.default, - queryMatch: { - value: - dataProvider.queryMatch!.operator === IS_OPERATOR ? '' : dataProvider.queryMatch!.value, - }, - }); +const convertToDefaultField = ({ and, ...dataProvider }: DataProviderResult) => { + if (dataProvider.type === DataProviderType.template) { + return deepMerge(dataProvider, { + type: DataProviderType.default, + enabled: dataProvider.queryMatch!.operator !== IS_OPERATOR, + queryMatch: { + value: + dataProvider.queryMatch!.operator === IS_OPERATOR ? '' : dataProvider.queryMatch!.value, + }, + }); + } + + return dataProvider; +}; const getDataProviders = ( duplicate: boolean, @@ -212,6 +216,28 @@ const getDataProviders = ( return dataProviders; }; +export const getTimelineTitle = ( + timeline: TimelineResult, + duplicate: boolean, + timelineType?: TimelineType +) => { + const isCreateTimelineFromAction = timelineType && timeline.timelineType !== timelineType; + if (isCreateTimelineFromAction) return ''; + + return duplicate ? `${timeline.title} - Duplicate` : timeline.title || ''; +}; + +export const getTimelineStatus = ( + timeline: TimelineResult, + duplicate: boolean, + timelineType?: TimelineType +) => { + const isCreateTimelineFromAction = timelineType && timeline.timelineType !== timelineType; + if (isCreateTimelineFromAction) return TimelineStatus.draft; + + return duplicate ? TimelineStatus.active : timeline.status; +}; + // eslint-disable-next-line complexity export const defaultTimelineToTimelineModel = ( timeline: TimelineResult, @@ -234,11 +260,11 @@ export const defaultTimelineToTimelineModel = ( pinnedEventIds: setPinnedEventIds(duplicate, timeline.pinnedEventIds), pinnedEventsSaveObject: setPinnedEventsSaveObject(duplicate, timeline.pinnedEventsSaveObject), id: duplicate ? '' : timeline.savedObjectId, - status: duplicate ? TimelineStatus.active : timeline.status, + status: getTimelineStatus(timeline, duplicate, timelineType), savedObjectId: duplicate ? null : timeline.savedObjectId, version: duplicate ? null : timeline.version, timelineType: timelineType ?? timeline.timelineType, - title: duplicate ? `${timeline.title} - Duplicate` : timeline.title || '', + title: getTimelineTitle(timeline, duplicate, timelineType), templateTimelineId: getTemplateTimelineId(timeline, duplicate, timelineType), templateTimelineVersion: duplicate && isTemplate ? 1 : timeline.templateTimelineVersion, }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx index 13786c55e2a8d..d839a1deddf21 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiPanel, EuiBasicTable, EuiSpacer } from '@elastic/eui'; +import { EuiPanel, EuiBasicTable } from '@elastic/eui'; import React, { useCallback, useMemo, useRef } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -183,7 +183,6 @@ export const OpenTimeline = React.memo( /> - {!!timelineFilter && timelineFilter} ( ({ hideActions = [], modalTitle, onClose, onOpen }) => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_body.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_body.tsx index b3ae39bf8b346..af2bd53df77db 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_body.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_body.tsx @@ -79,43 +79,43 @@ export const OpenTimelineModalBody = memo( selectedTimelinesCount={selectedItems.length} title={title} /> - <> - - {SearchRowContent} - - - + <> + + {SearchRowContent} + + + ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_status.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_status.tsx index a95f163349f05..8c4c686698c88 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_status.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_status.tsx @@ -33,9 +33,7 @@ export const useTimelineStatus = ({ templateTimelineFilter: JSX.Element[] | null; installPrepackagedTimelines: () => void; } => { - const [selectedTab, setSelectedTab] = useState( - TemplateTimelineType.elastic - ); + const [selectedTab, setSelectedTab] = useState(null); const isTemplateFilterEnabled = useMemo(() => timelineType === TimelineType.template, [ timelineType, ]); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/provider_badge.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/provider_badge.tsx index af63957d35075..bf2094e7659ee 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/provider_badge.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/provider_badge.tsx @@ -92,7 +92,7 @@ const ConvertFieldBadge = styled(ProviderFieldBadge)` `; const TemplateFieldBadge: React.FC = ({ type, toggleType }) => { - if (type === DataProviderType.default) { + if (type !== DataProviderType.template) { return ( {i18n.CONVERT_TO_TEMPLATE_FIELD} ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx index eb103d8e7e861..5c992fd640a97 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx @@ -258,8 +258,7 @@ export const EventsTdContent = styled.div.attrs(({ className }) => ({ ? `${width}px` : '100%'}; /* Using width: 100% instead of flex: 1 and max-width: 100% for IE11 */ - > button.euiButtonIcon, - > .euiToolTipAnchor > button.euiButtonIcon { + button.euiButtonIcon { margin-left: ${({ theme }) => `-${theme.eui.paddingSizes.xs}`}; } `; diff --git a/x-pack/plugins/security_solution/public/timelines/containers/one/index.gql_query.ts b/x-pack/plugins/security_solution/public/timelines/containers/one/index.gql_query.ts index 0aaeb22d72afc..5e50a7fb3313e 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/one/index.gql_query.ts +++ b/x-pack/plugins/security_solution/public/timelines/containers/one/index.gql_query.ts @@ -42,6 +42,7 @@ export const oneTimelineQuery = gql` enabled excluded kqlQuery + type queryMatch { field displayField diff --git a/x-pack/plugins/security_solution/public/timelines/containers/persist.gql_query.ts b/x-pack/plugins/security_solution/public/timelines/containers/persist.gql_query.ts index 6a0609f9158f3..c38aa67ccebb2 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/persist.gql_query.ts +++ b/x-pack/plugins/security_solution/public/timelines/containers/persist.gql_query.ts @@ -32,6 +32,7 @@ export const persistTimelineMutation = gql` enabled excluded kqlQuery + type queryMatch { field displayField @@ -45,6 +46,7 @@ export const persistTimelineMutation = gql` enabled excluded kqlQuery + type queryMatch { field displayField @@ -56,6 +58,7 @@ export const persistTimelineMutation = gql` } description eventType + excludedRowRendererIds favorite { fullName userName diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts index 2f9331ec9db8e..7757794c6dc9a 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts @@ -68,6 +68,7 @@ import { updateTimeline, updateTitle, updateAutoSaveMsg, + setExcludedRowRendererIds, setFilters, setSavedQueryId, startTimelineSaving, @@ -88,9 +89,11 @@ import { ActionTimeline, TimelineEpicDependencies } from './types'; const timelineActionsType = [ applyKqlFilterQuery.type, addProvider.type, + addTimeline.type, dataProviderEdited.type, removeColumn.type, removeProvider.type, + setExcludedRowRendererIds.type, setFilters.type, setSavedQueryId.type, updateColumns.type, diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object.ts index 82a2a866a71ff..b50195219f993 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/saved_object.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object.ts @@ -7,7 +7,7 @@ import { getOr } from 'lodash/fp'; import { SavedObjectsFindOptions } from '../../../../../../src/core/server'; -import { UNAUTHENTICATED_USER, enableElasticFilter } from '../../../common/constants'; +import { UNAUTHENTICATED_USER } from '../../../common/constants'; import { NoteSavedObject } from '../../../common/types/timeline/note'; import { PinnedEventSavedObject } from '../../../common/types/timeline/pinned_event'; import { @@ -153,12 +153,10 @@ const getTimelineTypeFilter = ( templateTimelineType == null ? null : templateTimelineType === TemplateTimelineType.elastic - ? `siem-ui-timeline.attributes.createdBy: "Elsatic"` + ? `siem-ui-timeline.attributes.createdBy: "Elastic"` : `not siem-ui-timeline.attributes.createdBy: "Elastic"`; - const filters = enableElasticFilter - ? [typeFilter, draftFilter, immutableFilter, templateTimelineTypeFilter] - : [typeFilter, draftFilter, immutableFilter]; + const filters = [typeFilter, draftFilter, immutableFilter, templateTimelineTypeFilter]; return filters.filter((f) => f != null).join(' and '); }; diff --git a/x-pack/test/api_integration/apis/security_solution/saved_objects/timeline.ts b/x-pack/test/api_integration/apis/security_solution/saved_objects/timeline.ts index 10ba9621c0430..a399c07e31065 100644 --- a/x-pack/test/api_integration/apis/security_solution/saved_objects/timeline.ts +++ b/x-pack/test/api_integration/apis/security_solution/saved_objects/timeline.ts @@ -114,6 +114,7 @@ export default function ({ getService }: FtrProviderContext) { enabled: true, excluded: false, kqlQuery: '', + type: 'default', queryMatch: { field: 'host.name', displayField: null, From 6c3b900d1183e62a391891c7fc6795f837e74ccb Mon Sep 17 00:00:00 2001 From: Liza Katz Date: Mon, 20 Jul 2020 17:29:28 +0300 Subject: [PATCH 32/45] [Plugin Generator] Generate tsconfig and useDefaultBehaviors (#72040) * improve test stability * add a tsconfig file and useDefaultBehaviors Co-authored-by: Elastic Machine --- .../kbn-plugin-generator/sao_template/sao.js | 6 ++++++ .../sao_template/sao.test.js | 3 +++ .../template/public/components/app.tsx | 2 +- .../sao_template/template/tsconfig.json | 16 ++++++++++++++++ 4 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 packages/kbn-plugin-generator/sao_template/template/tsconfig.json diff --git a/packages/kbn-plugin-generator/sao_template/sao.js b/packages/kbn-plugin-generator/sao_template/sao.js index dc4d8a2fc10fb..e5f81a984ee93 100755 --- a/packages/kbn-plugin-generator/sao_template/sao.js +++ b/packages/kbn-plugin-generator/sao_template/sao.js @@ -120,6 +120,11 @@ module.exports = function ({ name, targetPath }) { return !customPath; }, }, + generateTsconfig: { + type: 'confirm', + message: 'Would you like to use a custom tsconfig file?', + default: true, + }, }, filters: { 'public/**/index.scss': 'generateScss', @@ -128,6 +133,7 @@ module.exports = function ({ name, targetPath }) { 'translations/**/*': 'generateTranslations', 'i18nrc.json': 'generateTranslations', 'eslintrc.js': 'generateEslint', + 'tsconfig.json': 'generateTsconfig', }, move: { 'eslintrc.js': '.eslintrc.js', diff --git a/packages/kbn-plugin-generator/sao_template/sao.test.js b/packages/kbn-plugin-generator/sao_template/sao.test.js index 03d95e12d58da..af243326cff33 100755 --- a/packages/kbn-plugin-generator/sao_template/sao.test.js +++ b/packages/kbn-plugin-generator/sao_template/sao.test.js @@ -80,12 +80,14 @@ describe('plugin generator sao integration', () => { generateApi: true, generateScss: false, generateEslint: false, + generateTsconfig: false, }); // check output files expect(res.fileList).toContain('public/plugin.ts'); expect(res.fileList).not.toContain('public/index.scss'); expect(res.fileList).not.toContain('.eslintrc.js'); + expect(res.fileList).not.toContain('tsconfig.json'); }); it('plugin package has correct title', async () => { @@ -136,6 +138,7 @@ describe('plugin generator sao integration', () => { it('includes dotfiles', async () => { const res = await sao.mockPrompt(template); + expect(res.files['tsconfig.json']).toBeTruthy(); expect(res.files['.eslintrc.js']).toBeTruthy(); expect(res.files['.i18nrc.json']).toBeTruthy(); }); diff --git a/packages/kbn-plugin-generator/sao_template/template/public/components/app.tsx b/packages/kbn-plugin-generator/sao_template/template/public/components/app.tsx index 7b259a9c5b99d..d75bd2f01ef23 100644 --- a/packages/kbn-plugin-generator/sao_template/template/public/components/app.tsx +++ b/packages/kbn-plugin-generator/sao_template/template/public/components/app.tsx @@ -73,7 +73,7 @@ export const <%= upperCamelCaseName %>App = ({ basename, notifications, http, na <> - + diff --git a/packages/kbn-plugin-generator/sao_template/template/tsconfig.json b/packages/kbn-plugin-generator/sao_template/template/tsconfig.json new file mode 100644 index 0000000000000..8a3ced743d0fa --- /dev/null +++ b/packages/kbn-plugin-generator/sao_template/template/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./target", + "skipLibCheck": true + }, + "include": [ + "index.ts", + "common/**/*.ts", + "public/**/*.ts", + "public/**/*.tsx", + "server/**/*.ts", + "../../typings/**/*", + ], + "exclude": [] +} From 11182c8ef79cd821ac2b2284b4a3d6295e6ab75a Mon Sep 17 00:00:00 2001 From: Kerry Gallagher Date: Mon, 20 Jul 2020 15:51:22 +0100 Subject: [PATCH 33/45] Fix match phrase and not match phrase comparators (#71850) Co-authored-by: Elastic Machine --- .../alerting/log_threshold/register_log_threshold_alert_type.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/register_log_threshold_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/register_log_threshold_alert_type.ts index fbbb38da53929..ab55601f4c475 100644 --- a/x-pack/plugins/infra/server/lib/alerting/log_threshold/register_log_threshold_alert_type.ts +++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/register_log_threshold_alert_type.ts @@ -56,6 +56,8 @@ const criteriaSchema = schema.object({ schema.literal(Comparator.NOT_EQ), schema.literal(Comparator.MATCH), schema.literal(Comparator.NOT_MATCH), + schema.literal(Comparator.MATCH_PHRASE), + schema.literal(Comparator.NOT_MATCH_PHRASE), ]), value: schema.oneOf([schema.number(), schema.string()]), }); From e5c7e9a4741c4472294c650dcb555a4705079abe Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Mon, 20 Jul 2020 17:02:54 +0200 Subject: [PATCH 34/45] [Ingest Pipelines] Processor Editor Move Tooltip (#72239) * first implementation of tooltip * Add processor tooltip component files * remove init position from code for now * colocate on change handler and make code a bit cleaner * removed document.body.appendChild logic because EuiPortal does that for us * use correct toggle button api * added test to check button disabled while editing * remove cursor not allowed * simplify logic * assert if against positive * remove unused variable * Remove unused actions const Co-authored-by: Elastic Machine --- .../pipeline_processors_editor.test.tsx | 8 +++ .../components/_shared.scss | 1 + .../components/index.ts | 2 + .../pipeline_processors_editor_item.tsx | 20 ++++-- .../index.ts | 7 +++ ...ipeline_processors_editor_item_toolip.scss | 7 +++ ...ipeline_processors_editor_item_tooltip.tsx | 61 +++++++++++++++++++ .../processor_information.tsx | 32 ++++++++++ .../components/drop_zone_button.tsx | 1 - .../processors_tree/processors_tree.scss | 4 -- .../pipeline_processors_editor/constants.ts | 5 -- .../pipeline_processors_editor/context.tsx | 22 +++++-- .../pipeline_processors_editor/types.ts | 2 +- 13 files changed, 151 insertions(+), 21 deletions(-) create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item_tooltip/index.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item_tooltip/pipeline_processors_editor_item_toolip.scss create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item_tooltip/pipeline_processors_editor_item_tooltip.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item_tooltip/processor_information.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.test.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.test.tsx index acfa012990b21..df4832f9a45e0 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.test.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.test.tsx @@ -178,5 +178,13 @@ describe('Pipeline Editor', () => { expect(data2.processors).toEqual(testProcessors.processors); expect(data2.on_failure).toEqual([{ test: { if: '1 == 5' } }]); }); + + it('prevents moving a processor while in edit mode', () => { + const { find, exists } = testBed; + find('processors>0.editItemButton').simulate('click'); + expect(exists('processorSettingsForm')).toBe(true); + expect(find('processors>0.moveItemButton').props().disabled).toBe(true); + expect(find('processors>1.moveItemButton').props().disabled).toBe(true); + }); }); }); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/_shared.scss b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/_shared.scss index c7c49c00bb5cf..fe9a54671a00e 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/_shared.scss +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/_shared.scss @@ -1,2 +1,3 @@ $dropZoneZIndex: $euiZLevel1; /* Prevent the next item down from obscuring the button */ $cancelButtonZIndex: $euiZLevel2; +$processorItemMouseTooltipZIndex: $euiZLevel3; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/index.ts index de0621b187230..b532b2d953e65 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/index.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/index.ts @@ -19,3 +19,5 @@ export { PipelineProcessorsEditorItem } from './pipeline_processors_editor_item' export { ProcessorRemoveModal } from './processor_remove_modal'; export { OnDoneLoadJsonHandler, LoadFromJsonButton } from './load_from_json'; + +export { PipelineProcessorsItemTooltip, Position } from './pipeline_processors_editor_item_tooltip'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.tsx index 97b57a971ff7d..3fbef4c1b7898 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.tsx @@ -51,7 +51,7 @@ export const PipelineProcessorsEditorItem: FunctionComponent = memo( editor, processorsDispatch, }) { - const isDisabled = editor.mode.id !== 'idle'; + const isEditorNotInIdleMode = editor.mode.id !== 'idle'; const isInMoveMode = Boolean(movingProcessor); const isMovingThisProcessor = processor.id === movingProcessor?.id; const isEditingThisProcessor = @@ -83,6 +83,7 @@ export const PipelineProcessorsEditorItem: FunctionComponent = memo( 'pipelineProcessorsEditor__item__moveButton--cancel': isMovingThisProcessor, }); const icon = isMovingThisProcessor ? 'cross' : 'sortable'; + const disabled = isEditorNotInIdleMode && !isMovingThisProcessor; const moveButton = ( = memo( iconType={icon} data-test-subj={dataTestSubj} size="s" - disabled={isDisabled && !isMovingThisProcessor} + isDisabled={disabled} label={label} aria-label={label} - onChange={() => (!isMovingThisProcessor ? onMove() : onCancelMove())} + onChange={() => { + if (isMovingThisProcessor) { + onCancelMove(); + } else { + onMove(); + } + }} /> ); // Remove the tooltip from the DOM to prevent it from lingering if the mouse leave event @@ -132,7 +139,7 @@ export const PipelineProcessorsEditorItem: FunctionComponent = memo( { let nextOptions: Record; if (!nextDescription) { @@ -164,7 +171,8 @@ export const PipelineProcessorsEditorItem: FunctionComponent = memo( {!isInMoveMode && ( = memo( @@ -461,7 +461,7 @@ exports[`isNewKibanaInstance 1`] = ` - Centralize security events for interactive investigation in ready-to-go visualizations. + Protect hosts, analyze security information and events, hunt threats, automate detections, and create cases. } footer={ @@ -478,7 +478,7 @@ exports[`isNewKibanaInstance 1`] = ` } textAlign="left" - title="Security" + title="SIEM + Endpoint Security" titleSize="xs" /> @@ -758,7 +758,7 @@ exports[`mlEnabled 1`] = ` - Centralize security events for interactive investigation in ready-to-go visualizations. + Protect hosts, analyze security information and events, hunt threats, automate detections, and create cases. } footer={ @@ -775,7 +775,7 @@ exports[`mlEnabled 1`] = ` } textAlign="left" - title="Security" + title="SIEM + Endpoint Security" titleSize="xs" /> @@ -1060,7 +1060,7 @@ exports[`render 1`] = ` - Centralize security events for interactive investigation in ready-to-go visualizations. + Protect hosts, analyze security information and events, hunt threats, automate detections, and create cases. } footer={ @@ -1077,7 +1077,7 @@ exports[`render 1`] = ` } textAlign="left" - title="Security" + title="SIEM + Endpoint Security" titleSize="xs" /> diff --git a/src/plugins/home/public/application/components/add_data.js b/src/plugins/home/public/application/components/add_data.js index fa1327b3fcd08..c35b7b04932fb 100644 --- a/src/plugins/home/public/application/components/add_data.js +++ b/src/plugins/home/public/application/components/add_data.js @@ -81,12 +81,12 @@ const AddDataUi = ({ apmUiEnabled, isNewKibanaInstance, intl, mlEnabled }) => { const siemData = { title: intl.formatMessage({ id: 'home.addData.securitySolution.nameTitle', - defaultMessage: 'Security', + defaultMessage: 'SIEM + Endpoint Security', }), description: intl.formatMessage({ id: 'home.addData.securitySolution.nameDescription', defaultMessage: - 'Centralize security events for interactive investigation in ready-to-go visualizations.', + 'Protect hosts, analyze security information and events, hunt threats, automate detections, and create cases.', }), ariaDescribedby: 'aria-describedby.addSiemButtonLabel', }; diff --git a/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx b/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx index 3a8f2f0c16b96..a1e7293ce974b 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx @@ -63,26 +63,19 @@ export const HeaderGlobal = React.memo(({ hideDetectionEngine - + - {indicesExist ? ( - key !== SecurityPageName.detections, navTabs) - : navTabs - } - /> - ) : ( - key === SecurityPageName.overview, navTabs)} - /> - )} + key !== SecurityPageName.detections, navTabs) + : navTabs + } + /> diff --git a/x-pack/plugins/security_solution/public/common/components/header_global/translations.ts b/x-pack/plugins/security_solution/public/common/components/header_global/translations.ts index f67f665434a96..d3205be9bd2fc 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_global/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/header_global/translations.ts @@ -6,9 +6,12 @@ import { i18n } from '@kbn/i18n'; -export const SIEM = i18n.translate('xpack.securitySolution.headerGlobal.siem', { - defaultMessage: 'SIEM', -}); +export const SECURITY_SOLUTION = i18n.translate( + 'xpack.securitySolution.headerGlobal.securitySolution', + { + defaultMessage: 'Security solution', + } +); export const BUTTON_ADD_DATA = i18n.translate('xpack.securitySolution.headerGlobal.buttonAddData', { defaultMessage: 'Add data', diff --git a/x-pack/plugins/security_solution/public/common/containers/source/index.test.tsx b/x-pack/plugins/security_solution/public/common/containers/source/index.test.tsx index 03ad6ad3396f8..8ba7f7da7b8e3 100644 --- a/x-pack/plugins/security_solution/public/common/containers/source/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/source/index.test.tsx @@ -7,6 +7,7 @@ import { act, renderHook } from '@testing-library/react-hooks'; import { useWithSource, indicesExistOrDataTemporarilyUnavailable } from '.'; +import { NO_ALERT_INDEX } from '../../../../common/constants'; import { mockBrowserFields, mockIndexFields, mocksSource } from './mock'; jest.mock('../../lib/kibana'); @@ -79,6 +80,17 @@ describe('Index Fields & Browser Fields', () => { }); }); + test('Make sure we are not querying for NO_ALERT_INDEX and it is not includes in the index pattern', async () => { + const { result, waitForNextUpdate } = renderHook(() => + useWithSource('default', [NO_ALERT_INDEX]) + ); + + await waitForNextUpdate(); + return expect(result.current.indexPattern.title).toEqual( + 'apm-*-transaction*,auditbeat-*,endgame-*,filebeat-*,logs-*,packetbeat-*,winlogbeat-*' + ); + }); + describe('indicesExistOrDataTemporarilyUnavailable', () => { test('it returns true when undefined', () => { let undefVar; diff --git a/x-pack/plugins/security_solution/public/common/containers/source/index.tsx b/x-pack/plugins/security_solution/public/common/containers/source/index.tsx index cc43dd6f42772..bbd00900105e8 100644 --- a/x-pack/plugins/security_solution/public/common/containers/source/index.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/source/index.tsx @@ -11,7 +11,7 @@ import { useEffect, useMemo, useState } from 'react'; import memoizeOne from 'memoize-one'; import { IIndexPattern } from 'src/plugins/data/public'; -import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; +import { DEFAULT_INDEX_KEY, NO_ALERT_INDEX } from '../../../../common/constants'; import { useUiSetting$ } from '../../lib/kibana'; import { IndexField, SourceQuery } from '../../../graphql/types'; @@ -126,8 +126,9 @@ export const useWithSource = ( ) => { const [configIndex] = useUiSetting$(DEFAULT_INDEX_KEY); const defaultIndex = useMemo(() => { - if (indexToAdd != null && !isEmpty(indexToAdd)) { - return onlyCheckIndexToAdd ? indexToAdd : [...configIndex, ...indexToAdd]; + const filterIndexAdd = (indexToAdd ?? []).filter((item) => item !== NO_ALERT_INDEX); + if (!isEmpty(filterIndexAdd)) { + return onlyCheckIndexToAdd ? filterIndexAdd : [...configIndex, ...filterIndexAdd]; } return configIndex; }, [configIndex, indexToAdd, onlyCheckIndexToAdd]); @@ -138,7 +139,7 @@ export const useWithSource = ( errorMessage: null, indexPattern: getIndexFields(defaultIndex.join(), []), indicesExist: indicesExistOrDataTemporarilyUnavailable(undefined), - loading: false, + loading: true, }); const apolloClient = useApolloClient(); @@ -155,7 +156,7 @@ export const useWithSource = ( try { const result = await apolloClient.query({ query: sourceQuery, - fetchPolicy: 'cache-first', + fetchPolicy: 'network-only', variables: { sourceId, defaultIndex, diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.tsx index f93f380469622..99968cd4d9fe8 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.tsx @@ -48,7 +48,6 @@ const PrePackagedRulesPromptComponent: React.FC = ( return ( {i18n.PRE_BUILT_TITLE}} body={

{i18n.PRE_BUILT_MSG}

} actions={ diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.tsx index 7bf151adde5cc..2b842515d0b71 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.tsx @@ -33,7 +33,6 @@ import { useKibana } from '../../../../common/lib/kibana'; import { getSchema } from './schema'; import * as I18n from './translations'; import { APP_ID } from '../../../../../common/constants'; -import { SecurityPageName } from '../../../../app/types'; interface StepRuleActionsProps extends RuleStepProps { defaultValues?: ActionsStepRule | null; @@ -86,16 +85,13 @@ const StepRuleActionsComponent: FC = ({ }); const { submit } = form; - // TO DO need to make sure that logic is still valid - const kibanaAbsoluteUrl = useMemo(() => { - const url = application.getUrlForApp(`${APP_ID}:${SecurityPageName.detections}`, { - absolute: true, - }); - if (url != null && url.includes('app/security/alerts')) { - return url.replace('app/security/alerts', 'app/security'); - } - return url; - }, [application]); + const kibanaAbsoluteUrl = useMemo( + () => + application.getUrlForApp(`${APP_ID}`, { + absolute: true, + }), + [application] + ); const onSubmit = useCallback( async (enabled: boolean) => { diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/fetch_index_patterns.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/fetch_index_patterns.tsx index 9a2f43bb475b1..6257a9980e00c 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/fetch_index_patterns.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/fetch_index_patterns.tsx @@ -70,7 +70,7 @@ export const useFetchIndexPatterns = (defaultIndices: string[] = []): Return => apolloClient .query({ query: sourceQuery, - fetchPolicy: 'cache-first', + fetchPolicy: 'network-only', variables: { sourceId: 'default', defaultIndex: indices, diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx index 7b843b4f69447..f4e39ff8227c7 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx @@ -138,10 +138,9 @@ export const StatefulFieldsBrowserComponent: React.FC = ({ setShow(false); }, []); // only merge in the default category if the field browser is visible - const browserFieldsWithDefaultCategory = useMemo( - () => (show ? mergeBrowserFieldsWithDefaultCategory(browserFields) : {}), - [show, browserFields] - ); + const browserFieldsWithDefaultCategory = useMemo(() => { + return show ? mergeBrowserFieldsWithDefaultCategory(browserFields) : {}; + }, [show, browserFields]); return ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/index.tsx index 5f35bc5212d37..7ee7e12c0ef62 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/index.tsx @@ -78,8 +78,7 @@ const StatefulSearchOrFilterComponent = React.memo( serializedQuery: convertKueryToElasticSearchQuery(expression, indexPattern), }, }), - // eslint-disable-next-line react-hooks/exhaustive-deps - [indexPattern, timelineId] + [applyKqlFilterQuery, indexPattern, timelineId] ); const setFilterQueryDraftFromKueryExpression = useCallback( @@ -91,8 +90,7 @@ const StatefulSearchOrFilterComponent = React.memo( expression, }, }), - // eslint-disable-next-line react-hooks/exhaustive-deps - [timelineId] + [timelineId, setKqlFilterQueryDraft] ); const setFiltersInTimeline = useCallback( @@ -101,8 +99,7 @@ const StatefulSearchOrFilterComponent = React.memo( id: timelineId, filters: newFilters, }), - // eslint-disable-next-line react-hooks/exhaustive-deps - [timelineId] + [timelineId, setFilters] ); const setSavedQueryInTimeline = useCallback( @@ -111,8 +108,7 @@ const StatefulSearchOrFilterComponent = React.memo( id: timelineId, savedQueryId: newSavedQueryId, }), - // eslint-disable-next-line react-hooks/exhaustive-deps - [timelineId] + [timelineId, setSavedQueryId] ); const handleUpdateEventType = useCallback( @@ -121,8 +117,7 @@ const StatefulSearchOrFilterComponent = React.memo( id: timelineId, eventType: newEventType, }), - // eslint-disable-next-line react-hooks/exhaustive-deps - [timelineId] + [timelineId, updateEventType] ); return ( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/utils.test.ts index 0d363e1f6f3c2..95e6071e4defc 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/utils.test.ts @@ -9,13 +9,13 @@ import { getNotificationResultsLink } from './utils'; describe('utils', () => { it('getNotificationResultsLink', () => { const resultLink = getNotificationResultsLink({ - kibanaSiemAppUrl: 'http://localhost:5601/app/siem', + kibanaSiemAppUrl: 'http://localhost:5601/app/security', id: 'notification-id', from: '00000', to: '1111', }); expect(resultLink).toEqual( - `http://localhost:5601/app/siem#/detections/rules/id/notification-id?timerange=(global:(linkTo:!(timeline),timerange:(from:00000,kind:absolute,to:1111)),timeline:(linkTo:!(global),timerange:(from:00000,kind:absolute,to:1111)))` + `http://localhost:5601/app/security/detections/rules/id/notification-id?timerange=(global:(linkTo:!(timeline),timerange:(from:00000,kind:absolute,to:1111)),timeline:(linkTo:!(global),timerange:(from:00000,kind:absolute,to:1111)))` ); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/utils.ts index c91c4490e8eba..983ee86598fa1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/utils.ts @@ -4,8 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import { APP_PATH } from '../../../../common/constants'; + export const getNotificationResultsLink = ({ - kibanaSiemAppUrl = '/app/siem', + kibanaSiemAppUrl = APP_PATH, id, from, to, @@ -17,5 +19,5 @@ export const getNotificationResultsLink = ({ }) => { if (from == null || to == null) return ''; - return `${kibanaSiemAppUrl}#/detections/rules/id/${id}?timerange=(global:(linkTo:!(timeline),timerange:(from:${from},kind:absolute,to:${to})),timeline:(linkTo:!(global),timerange:(from:${from},kind:absolute,to:${to})))`; + return `${kibanaSiemAppUrl}/detections/rules/id/${id}?timerange=(global:(linkTo:!(timeline),timerange:(from:${from},kind:absolute,to:${to})),timeline:(linkTo:!(global),timerange:(from:${from},kind:absolute,to:${to})))`; }; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index b87957ae45289..c1f0dc4c0c60c 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -14067,7 +14067,6 @@ "xpack.securitySolution.header.editableTitle.editButtonAria": "クリックすると {title} を編集できます", "xpack.securitySolution.header.editableTitle.save": "保存", "xpack.securitySolution.headerGlobal.buttonAddData": "データの追加", - "xpack.securitySolution.headerGlobal.siem": "Security", "xpack.securitySolution.headerPage.pageSubtitle": "前回のイベント: {beat}", "xpack.securitySolution.hooks.useAddToTimeline.addedFieldMessage": "{fieldOrValue}をタイムラインに追加しました", "xpack.securitySolution.host.details.architectureLabel": "アーキテクチャー", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 01ffa4833a3bb..0f2a51c8ff889 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -14073,7 +14073,6 @@ "xpack.securitySolution.header.editableTitle.editButtonAria": "通过单击,可以编辑 {title}", "xpack.securitySolution.header.editableTitle.save": "保存", "xpack.securitySolution.headerGlobal.buttonAddData": "添加数据", - "xpack.securitySolution.headerGlobal.siem": "Security", "xpack.securitySolution.headerPage.pageSubtitle": "最后事件:{beat}", "xpack.securitySolution.hooks.useAddToTimeline.addedFieldMessage": "已将 {fieldOrValue} 添加到时间线", "xpack.securitySolution.host.details.architectureLabel": "架构", From 54c3644757d5622f9e516aa3c99cf2ec87d6591e Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 20 Jul 2020 18:35:42 +0300 Subject: [PATCH 37/45] [Alerting][Connectors] Increase the size of the logos (#72419) --- .../sections/action_connector_form/action_type_menu.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 bc36c41664e57..7ecb833fdfc9e 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 @@ -81,7 +81,7 @@ export const ActionTypeMenu = ({ } + icon={} title={item.name} description={item.selectMessage} isDisabled={!checkEnabledResult.isEnabled} From d744d18b1921655c12b82bb6760f9bc8348e2a3c Mon Sep 17 00:00:00 2001 From: Bohdan Tsymbala Date: Mon, 20 Jul 2020 17:44:17 +0200 Subject: [PATCH 38/45] [ENDPOINT] Added unerolling status for host. (#72303) * Added unerolling status for host. * Added unenrolling status to frontend tests. --- .../common/endpoint/types.ts | 5 ++++ .../endpoint_hosts/view/host_constants.ts | 1 + .../pages/endpoint_hosts/view/index.test.tsx | 25 ++++++++++++------- .../pages/endpoint_hosts/view/index.tsx | 2 +- .../server/endpoint/routes/metadata/index.ts | 1 + .../endpoint/routes/metadata/metadata.test.ts | 4 +-- 6 files changed, 26 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/security_solution/common/endpoint/types.ts b/x-pack/plugins/security_solution/common/endpoint/types.ts index 5e7e4d22f8c3c..a982f9ffe8f21 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types.ts @@ -419,6 +419,11 @@ export enum HostStatus { * Host is offline as indicated by its checkin status during the last checkin window */ OFFLINE = 'offline', + + /** + * Host is unenrolling as indicated by its checkin status during the last checkin window + */ + UNENROLLING = 'unenrolling', } export type HostInfo = Immutable<{ diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/host_constants.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/host_constants.ts index 790bbd3cb98da..4204d4f79f19c 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/host_constants.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/host_constants.ts @@ -15,6 +15,7 @@ export const HOST_STATUS_TO_HEALTH_COLOR = Object.freeze< [HostStatus.ERROR]: 'danger', [HostStatus.ONLINE]: 'success', [HostStatus.OFFLINE]: 'subdued', + [HostStatus.UNENROLLING]: 'warning', }); export const POLICY_STATUS_TO_HEALTH_COLOR = Object.freeze< diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx index a61088e2edd29..47227244b7066 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx @@ -112,14 +112,16 @@ describe('when on the hosts page', () => { let firstPolicyID: string; beforeEach(() => { reactTestingLibrary.act(() => { - const hostListData = mockHostResultList({ total: 3 }); + const hostListData = mockHostResultList({ total: 4 }); firstPolicyID = hostListData.hosts[0].metadata.Endpoint.policy.applied.id; - [HostStatus.ERROR, HostStatus.ONLINE, HostStatus.OFFLINE].forEach((status, index) => { - hostListData.hosts[index] = { - metadata: hostListData.hosts[index].metadata, - host_status: status, - }; - }); + [HostStatus.ERROR, HostStatus.ONLINE, HostStatus.OFFLINE, HostStatus.UNENROLLING].forEach( + (status, index) => { + hostListData.hosts[index] = { + metadata: hostListData.hosts[index].metadata, + host_status: status, + }; + } + ); hostListData.hosts.forEach((item, index) => { generatedPolicyStatuses[index] = item.metadata.Endpoint.policy.applied.status; }); @@ -134,12 +136,12 @@ describe('when on the hosts page', () => { it('should display rows in the table', async () => { const renderResult = render(); const rows = await renderResult.findAllByRole('row'); - expect(rows).toHaveLength(4); + expect(rows).toHaveLength(5); }); it('should show total', async () => { const renderResult = render(); const total = await renderResult.findByTestId('hostListTableTotal'); - expect(total.textContent).toEqual('3 Hosts'); + expect(total.textContent).toEqual('4 Hosts'); }); it('should display correct status', async () => { const renderResult = render(); @@ -157,6 +159,11 @@ describe('when on the hosts page', () => { expect( hostStatuses[2].querySelector('[data-euiicon-type][color="subdued"]') ).not.toBeNull(); + + expect(hostStatuses[3].textContent).toEqual('Unenrolling'); + expect( + hostStatuses[3].querySelector('[data-euiicon-type][color="warning"]') + ).not.toBeNull(); }); it('should display correct policy status', async () => { diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx index 4c8d2c5a6df4e..c5ed71cba46d9 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx @@ -226,7 +226,7 @@ export const HostList = () => { > diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts index cb9889ca0cb76..fe7a8296608d2 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts @@ -35,6 +35,7 @@ interface MetadataRequestContext { const HOST_STATUS_MAPPING = new Map([ ['online', HostStatus.ONLINE], ['offline', HostStatus.OFFLINE], + ['unenrolling', HostStatus.UNENROLLING], ]); /** diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts index 321eb0195aac3..8d967656065d1 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts @@ -335,7 +335,7 @@ describe('test endpoint route', () => { expect(result.host_status).toEqual(HostStatus.ERROR); }); - it('should return a single endpoint with status error when status is not offline or online', async () => { + it('should return a single endpoint with status error when status is not offline, online or enrolling', async () => { const response = createSearchResponse(new EndpointDocGenerator().generateHostMetadata()); const mockRequest = httpServerMock.createKibanaRequest({ @@ -368,7 +368,7 @@ describe('test endpoint route', () => { expect(result.host_status).toEqual(HostStatus.ERROR); }); - it('should throw error when endpoint egent is not active', async () => { + it('should throw error when endpoint agent is not active', async () => { const response = createSearchResponse(new EndpointDocGenerator().generateHostMetadata()); const mockRequest = httpServerMock.createKibanaRequest({ From 96d965d4e305e7891500b7ad6cb83f4356d6117c Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Mon, 20 Jul 2020 17:55:27 +0200 Subject: [PATCH 39/45] Unskip dashboard embeddable rendering tests (#71824) --- test/functional/apps/dashboard/embeddable_rendering.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/functional/apps/dashboard/embeddable_rendering.js b/test/functional/apps/dashboard/embeddable_rendering.js index 9ba0c07c744fc..c00f01d060f4a 100644 --- a/test/functional/apps/dashboard/embeddable_rendering.js +++ b/test/functional/apps/dashboard/embeddable_rendering.js @@ -98,8 +98,7 @@ export default function ({ getService, getPageObjects }) { await dashboardExpect.vegaTextsDoNotExist(['5,000']); }; - // FLAKY: https://github.com/elastic/kibana/issues/46305 - describe.skip('dashboard embeddable rendering', function describeIndexTests() { + describe('dashboard embeddable rendering', function describeIndexTests() { before(async () => { await esArchiver.load('dashboard/current/kibana'); await kibanaServer.uiSettings.replace({ From 2094f33537df6185ec0894316f8af107110bd071 Mon Sep 17 00:00:00 2001 From: Michael Olorunnisola Date: Mon, 20 Jul 2020 12:28:46 -0400 Subject: [PATCH 40/45] [Security Solution] Cleanup endpoint telemetry (#71950) Co-authored-by: Elastic Machine --- .../server/usage/collector.ts | 2 +- .../server/usage/endpoints/endpoint.mocks.ts | 60 +++- .../server/usage/endpoints/endpoint.test.ts | 332 +++++++++++++++--- .../usage/endpoints/fleet_saved_objects.ts | 16 +- .../server/usage/endpoints/index.ts | 239 +++++++------ 5 files changed, 480 insertions(+), 169 deletions(-) diff --git a/x-pack/plugins/security_solution/server/usage/collector.ts b/x-pack/plugins/security_solution/server/usage/collector.ts index 9740f57450e80..9a7ad6fc2db74 100644 --- a/x-pack/plugins/security_solution/server/usage/collector.ts +++ b/x-pack/plugins/security_solution/server/usage/collector.ts @@ -12,7 +12,7 @@ import { EndpointUsage, getEndpointTelemetryFromFleet } from './endpoints'; export type RegisterCollector = (deps: CollectorDependencies) => void; export interface UsageData { detections: DetectionsUsage; - endpoints: EndpointUsage; + endpoints: EndpointUsage | {}; } export async function getInternalSavedObjectsClient(core: CoreSetup) { diff --git a/x-pack/plugins/security_solution/server/usage/endpoints/endpoint.mocks.ts b/x-pack/plugins/security_solution/server/usage/endpoints/endpoint.mocks.ts index 1369a3d398265..e3f0f7bde2fed 100644 --- a/x-pack/plugins/security_solution/server/usage/endpoints/endpoint.mocks.ts +++ b/x-pack/plugins/security_solution/server/usage/endpoints/endpoint.mocks.ts @@ -14,6 +14,7 @@ import { FLEET_ENDPOINT_PACKAGE_CONSTANT } from './fleet_saved_objects'; const testAgentId = 'testAgentId'; const testConfigId = 'testConfigId'; +const testHostId = 'randoHostId'; /** Mock OS Platform for endpoint telemetry */ export const MockOSPlatform = 'somePlatform'; @@ -30,6 +31,7 @@ export const MockOSFullName = 'somePlatformFullName'; * @description We request the install and OS related telemetry information from the 'fleet-agents' saved objects in ingest_manager. This mocks that response */ export const mockFleetObjectsResponse = ( + hasDuplicates = true, lastCheckIn = new Date().toISOString() ): SavedObjectsFindResponse => ({ page: 1, @@ -56,7 +58,44 @@ export const mockFleetObjectsResponse = ( host: { hostname: 'testDesktop', name: 'testDesktop', - id: 'randoHostId', + id: testHostId, + }, + os: { + platform: MockOSPlatform, + version: MockOSVersion, + name: MockOSName, + full: MockOSFullName, + }, + }, + packages: [FLEET_ENDPOINT_PACKAGE_CONSTANT, 'system'], + last_checkin: lastCheckIn, + }, + references: [], + updated_at: lastCheckIn, + version: 'WzI4MSwxXQ==', + score: 0, + }, + { + type: AGENT_SAVED_OBJECT_TYPE, + id: testAgentId, + attributes: { + active: true, + id: 'oldTestAgentId', + config_id: 'randoConfigId', + type: 'PERMANENT', + user_provided_metadata: {}, + enrolled_at: lastCheckIn, + current_error_events: [], + local_metadata: { + elastic: { + agent: { + id: 'oldTestAgentId', + }, + }, + host: { + hostname: 'testDesktop', + name: 'testDesktop', + id: hasDuplicates ? testHostId : 'oldRandoHostId', }, os: { platform: MockOSPlatform, @@ -76,7 +115,10 @@ export const mockFleetObjectsResponse = ( ], }); -const mockPolicyPayload = (malwareStatus: 'success' | 'warning' | 'failure') => +const mockPolicyPayload = ( + policyStatus: 'success' | 'warning' | 'failure', + policyMode: 'prevent' | 'detect' | 'off' = 'prevent' +) => JSON.stringify({ 'endpoint-security': { Endpoint: { @@ -105,7 +147,7 @@ const mockPolicyPayload = (malwareStatus: 'success' | 'warning' | 'failure') => file: 'info', }, malware: { - mode: 'prevent', + mode: policyMode, }, }, windows: { @@ -122,7 +164,7 @@ const mockPolicyPayload = (malwareStatus: 'success' | 'warning' | 'failure') => file: 'info', }, malware: { - mode: 'prevent', + mode: policyMode, }, }, }, @@ -151,11 +193,11 @@ const mockPolicyPayload = (malwareStatus: 'success' | 'warning' | 'failure') => 'detect_file_open_events', 'detect_sync_image_load_events', ], - status: `${malwareStatus}`, + status: `${policyStatus}`, }, }, }, - status: `${malwareStatus}`, + status: `${policyStatus}`, }, }, }, @@ -186,7 +228,9 @@ const mockPolicyPayload = (malwareStatus: 'success' | 'warning' | 'failure') => */ export const mockFleetEventsObjectsResponse = ( running?: boolean, - updatedDate = new Date().toISOString() + updatedDate = new Date().toISOString(), + policyStatus: 'success' | 'failure' = running ? 'success' : 'failure', + policyMode: 'prevent' | 'detect' | 'off' = 'prevent' ): SavedObjectsFindResponse => { return { page: 1, @@ -204,7 +248,7 @@ export const mockFleetEventsObjectsResponse = ( message: `Application: endpoint-security--8.0.0[d8f7f6e8-9375-483c-b456-b479f1d7a4f2]: State changed to ${ running ? 'RUNNING' : 'FAILED' }: `, - payload: mockPolicyPayload(running ? 'success' : 'failure'), + payload: running ? mockPolicyPayload(policyStatus, policyMode) : undefined, config_id: testConfigId, }, references: [], diff --git a/x-pack/plugins/security_solution/server/usage/endpoints/endpoint.test.ts b/x-pack/plugins/security_solution/server/usage/endpoints/endpoint.test.ts index 06755192bd818..e2f7a3be6d80a 100644 --- a/x-pack/plugins/security_solution/server/usage/endpoints/endpoint.test.ts +++ b/x-pack/plugins/security_solution/server/usage/endpoints/endpoint.test.ts @@ -51,17 +51,31 @@ describe('test security solution endpoint telemetry', () => { `); }); + describe('when a request for endpoint agents fails', () => { + it('should return an empty object', async () => { + getFleetSavedObjectsMetadataSpy.mockImplementation(() => + Promise.reject(Error('No agents for you')) + ); + + const endpointUsage = await endpointTelemetry.getEndpointTelemetryFromFleet( + mockSavedObjectsRepository + ); + expect(getFleetSavedObjectsMetadataSpy).toHaveBeenCalled(); + expect(endpointUsage).toEqual({}); + }); + }); + describe('when an agent has not been installed', () => { it('should return the default shape if no agents are found', async () => { getFleetSavedObjectsMetadataSpy.mockImplementation(() => Promise.resolve({ saved_objects: [], total: 0, per_page: 0, page: 0 }) ); - const emptyEndpointTelemetryData = await endpointTelemetry.getEndpointTelemetryFromFleet( + const endpointUsage = await endpointTelemetry.getEndpointTelemetryFromFleet( mockSavedObjectsRepository ); expect(getFleetSavedObjectsMetadataSpy).toHaveBeenCalled(); - expect(emptyEndpointTelemetryData).toEqual({ + expect(endpointUsage).toEqual({ total_installed: 0, active_within_last_24_hours: 0, os: [], @@ -76,68 +90,274 @@ describe('test security solution endpoint telemetry', () => { }); }); - describe('when an agent has been installed', () => { - it('should show one enpoint installed but it is inactive', async () => { - getFleetSavedObjectsMetadataSpy.mockImplementation(() => - Promise.resolve(mockFleetObjectsResponse()) - ); - getLatestFleetEndpointEventSpy.mockImplementation(() => - Promise.resolve(mockFleetEventsObjectsResponse()) - ); + describe('when agent(s) have been installed', () => { + describe('when a request for events has failed', () => { + it('should show only one endpoint installed but it is inactive', async () => { + getFleetSavedObjectsMetadataSpy.mockImplementation(() => + Promise.resolve(mockFleetObjectsResponse()) + ); + getLatestFleetEndpointEventSpy.mockImplementation(() => + Promise.reject(Error('No events for you')) + ); - const emptyEndpointTelemetryData = await endpointTelemetry.getEndpointTelemetryFromFleet( - mockSavedObjectsRepository - ); - expect(emptyEndpointTelemetryData).toEqual({ - total_installed: 1, - active_within_last_24_hours: 0, - os: [ - { - full_name: MockOSFullName, - platform: MockOSPlatform, - version: MockOSVersion, - count: 1, - }, - ], - policies: { - malware: { - failure: 1, - active: 0, - inactive: 0, + const endpointUsage = await endpointTelemetry.getEndpointTelemetryFromFleet( + mockSavedObjectsRepository + ); + expect(endpointUsage).toEqual({ + total_installed: 1, + active_within_last_24_hours: 0, + os: [ + { + full_name: MockOSFullName, + platform: MockOSPlatform, + version: MockOSVersion, + count: 1, + }, + ], + policies: { + malware: { + failure: 0, + active: 0, + inactive: 0, + }, }, - }, + }); }); }); - it('should show one endpoint installed and it is active', async () => { - getFleetSavedObjectsMetadataSpy.mockImplementation(() => - Promise.resolve(mockFleetObjectsResponse()) - ); - getLatestFleetEndpointEventSpy.mockImplementation(() => - Promise.resolve(mockFleetEventsObjectsResponse(true)) - ); + describe('when a request for events is successful', () => { + it('should show one endpoint installed but endpoint has failed to run', async () => { + getFleetSavedObjectsMetadataSpy.mockImplementation(() => + Promise.resolve(mockFleetObjectsResponse()) + ); + getLatestFleetEndpointEventSpy.mockImplementation(() => + Promise.resolve(mockFleetEventsObjectsResponse()) + ); - const emptyEndpointTelemetryData = await endpointTelemetry.getEndpointTelemetryFromFleet( - mockSavedObjectsRepository - ); - expect(emptyEndpointTelemetryData).toEqual({ - total_installed: 1, - active_within_last_24_hours: 1, - os: [ - { - full_name: MockOSFullName, - platform: MockOSPlatform, - version: MockOSVersion, - count: 1, + const endpointUsage = await endpointTelemetry.getEndpointTelemetryFromFleet( + mockSavedObjectsRepository + ); + expect(endpointUsage).toEqual({ + total_installed: 1, + active_within_last_24_hours: 0, + os: [ + { + full_name: MockOSFullName, + platform: MockOSPlatform, + version: MockOSVersion, + count: 1, + }, + ], + policies: { + malware: { + failure: 0, + active: 0, + inactive: 0, + }, }, - ], - policies: { - malware: { - failure: 0, - active: 1, - inactive: 0, + }); + }); + + it('should show two endpoints installed but both endpoints have failed to run', async () => { + getFleetSavedObjectsMetadataSpy.mockImplementation(() => + Promise.resolve(mockFleetObjectsResponse(false)) + ); + getLatestFleetEndpointEventSpy.mockImplementation(() => + Promise.resolve(mockFleetEventsObjectsResponse()) + ); + + const endpointUsage = await endpointTelemetry.getEndpointTelemetryFromFleet( + mockSavedObjectsRepository + ); + expect(endpointUsage).toEqual({ + total_installed: 2, + active_within_last_24_hours: 0, + os: [ + { + full_name: MockOSFullName, + platform: MockOSPlatform, + version: MockOSVersion, + count: 2, + }, + ], + policies: { + malware: { + failure: 0, + active: 0, + inactive: 0, + }, }, - }, + }); + }); + + it('should show two endpoints installed but agents have not checked in within past day', async () => { + const twoDaysAgo = new Date(); + twoDaysAgo.setDate(twoDaysAgo.getDate() - 2); + const twoDaysAgoISOString = twoDaysAgo.toISOString(); + + getFleetSavedObjectsMetadataSpy.mockImplementation(() => + Promise.resolve(mockFleetObjectsResponse(false, twoDaysAgoISOString)) + ); + getLatestFleetEndpointEventSpy.mockImplementation( + () => Promise.resolve(mockFleetEventsObjectsResponse(true, twoDaysAgoISOString)) // agent_id doesn't matter for mock here + ); + + const endpointUsage = await endpointTelemetry.getEndpointTelemetryFromFleet( + mockSavedObjectsRepository + ); + expect(endpointUsage).toEqual({ + total_installed: 2, + active_within_last_24_hours: 0, + os: [ + { + full_name: MockOSFullName, + platform: MockOSPlatform, + version: MockOSVersion, + count: 2, + }, + ], + policies: { + malware: { + failure: 0, + active: 2, + inactive: 0, + }, + }, + }); + }); + + it('should show one endpoint installed and endpoint is running', async () => { + getFleetSavedObjectsMetadataSpy.mockImplementation(() => + Promise.resolve(mockFleetObjectsResponse()) + ); + getLatestFleetEndpointEventSpy.mockImplementation(() => + Promise.resolve(mockFleetEventsObjectsResponse(true)) + ); + + const endpointUsage = await endpointTelemetry.getEndpointTelemetryFromFleet( + mockSavedObjectsRepository + ); + expect(endpointUsage).toEqual({ + total_installed: 1, + active_within_last_24_hours: 1, + os: [ + { + full_name: MockOSFullName, + platform: MockOSPlatform, + version: MockOSVersion, + count: 1, + }, + ], + policies: { + malware: { + failure: 0, + active: 1, + inactive: 0, + }, + }, + }); + }); + + describe('malware policy', () => { + it('should have failed to enable', async () => { + getFleetSavedObjectsMetadataSpy.mockImplementation(() => + Promise.resolve(mockFleetObjectsResponse()) + ); + getLatestFleetEndpointEventSpy.mockImplementation(() => + Promise.resolve( + mockFleetEventsObjectsResponse(true, new Date().toISOString(), 'failure') + ) + ); + + const endpointUsage = await endpointTelemetry.getEndpointTelemetryFromFleet( + mockSavedObjectsRepository + ); + expect(endpointUsage).toEqual({ + total_installed: 1, + active_within_last_24_hours: 1, + os: [ + { + full_name: MockOSFullName, + platform: MockOSPlatform, + version: MockOSVersion, + count: 1, + }, + ], + policies: { + malware: { + failure: 1, + active: 0, + inactive: 0, + }, + }, + }); + }); + + it('should be enabled successfully', async () => { + getFleetSavedObjectsMetadataSpy.mockImplementation(() => + Promise.resolve(mockFleetObjectsResponse()) + ); + getLatestFleetEndpointEventSpy.mockImplementation(() => + Promise.resolve(mockFleetEventsObjectsResponse(true)) + ); + + const endpointUsage = await endpointTelemetry.getEndpointTelemetryFromFleet( + mockSavedObjectsRepository + ); + expect(endpointUsage).toEqual({ + total_installed: 1, + active_within_last_24_hours: 1, + os: [ + { + full_name: MockOSFullName, + platform: MockOSPlatform, + version: MockOSVersion, + count: 1, + }, + ], + policies: { + malware: { + failure: 0, + active: 1, + inactive: 0, + }, + }, + }); + }); + + it('should be disabled successfully', async () => { + getFleetSavedObjectsMetadataSpy.mockImplementation(() => + Promise.resolve(mockFleetObjectsResponse()) + ); + getLatestFleetEndpointEventSpy.mockImplementation(() => + Promise.resolve( + mockFleetEventsObjectsResponse(true, new Date().toISOString(), 'success', 'off') + ) + ); + + const endpointUsage = await endpointTelemetry.getEndpointTelemetryFromFleet( + mockSavedObjectsRepository + ); + expect(endpointUsage).toEqual({ + total_installed: 1, + active_within_last_24_hours: 1, + os: [ + { + full_name: MockOSFullName, + platform: MockOSPlatform, + version: MockOSVersion, + count: 1, + }, + ], + policies: { + malware: { + failure: 0, + active: 0, + inactive: 1, + }, + }, + }); + }); }); }); }); diff --git a/x-pack/plugins/security_solution/server/usage/endpoints/fleet_saved_objects.ts b/x-pack/plugins/security_solution/server/usage/endpoints/fleet_saved_objects.ts index 7e05fdec36169..42c1ec0e2eed2 100644 --- a/x-pack/plugins/security_solution/server/usage/endpoints/fleet_saved_objects.ts +++ b/x-pack/plugins/security_solution/server/usage/endpoints/fleet_saved_objects.ts @@ -16,8 +16,16 @@ export const FLEET_ENDPOINT_PACKAGE_CONSTANT = FleetDefaultPackages.endpoint; export const getFleetSavedObjectsMetadata = async (savedObjectsClient: ISavedObjectsRepository) => savedObjectsClient.find({ + // Get up to 10000 agents with endpoint installed type: AGENT_SAVED_OBJECT_TYPE, - fields: ['packages', 'last_checkin', 'local_metadata'], + fields: [ + 'packages', + 'last_checkin', + 'local_metadata.agent.id', + 'local_metadata.host.id', + 'local_metadata.elastic.agent.id', + 'local_metadata.os', + ], filter: `${AGENT_SAVED_OBJECT_TYPE}.attributes.packages: ${FLEET_ENDPOINT_PACKAGE_CONSTANT}`, perPage: 10000, sortField: 'enrolled_at', @@ -29,9 +37,11 @@ export const getLatestFleetEndpointEvent = async ( agentId: string ) => savedObjectsClient.find({ + // Get the most recent endpoint event. type: AGENT_EVENT_SAVED_OBJECT_TYPE, - filter: `${AGENT_EVENT_SAVED_OBJECT_TYPE}.attributes.agent_id: ${agentId} and ${AGENT_EVENT_SAVED_OBJECT_TYPE}.attributes.message: "${FLEET_ENDPOINT_PACKAGE_CONSTANT}"`, - perPage: 1, // Get the most recent endpoint event. + fields: ['agent_id', 'subtype', 'payload'], + filter: `${AGENT_EVENT_SAVED_OBJECT_TYPE}.attributes.message: "${FLEET_ENDPOINT_PACKAGE_CONSTANT}"`, + perPage: 1, sortField: 'timestamp', sortOrder: 'desc', search: agentId, diff --git a/x-pack/plugins/security_solution/server/usage/endpoints/index.ts b/x-pack/plugins/security_solution/server/usage/endpoints/index.ts index ab5669d503275..9e071f4adff25 100644 --- a/x-pack/plugins/security_solution/server/usage/endpoints/index.ts +++ b/x-pack/plugins/security_solution/server/usage/endpoints/index.ts @@ -3,8 +3,10 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import { cloneDeep } from 'lodash'; import { ISavedObjectsRepository } from 'src/core/server'; +import { SavedObject } from './../../../../../../src/core/types/saved_objects'; +import { Agent, NewAgentEvent } from './../../../../ingest_manager/common/types/models/agent'; import { AgentMetadata } from '../../../../ingest_manager/common/types/models/agent'; import { getFleetSavedObjectsMetadata, getLatestFleetEndpointEvent } from './fleet_saved_objects'; @@ -51,7 +53,7 @@ export interface AgentLocalMetadata extends AgentMetadata { } type OSTracker = Record; -type AgentDailyActiveTracker = Map; + /** * @description returns an empty telemetry object to be incrmented and updated within the `getEndpointTelemetryFromFleet` fn */ @@ -69,13 +71,14 @@ export const getDefaultEndpointTelemetry = (): EndpointUsage => ({ }); /** - * @description this fun + * @description this function updates the os telemetry. We use the fullName field as the key as it contains the name and version details. + * If it has already been tracked, the count will be updated, otherwise a tracker will be initialized for that fullName. */ -export const trackEndpointOSTelemetry = ( +export const updateEndpointOSTelemetry = ( os: AgentLocalMetadata['os'], osTracker: OSTracker ): OSTracker => { - const updatedOSTracker = { ...osTracker }; + const updatedOSTracker = cloneDeep(osTracker); const { version: osVersion, platform: osPlatform, full: osFullName } = os; if (osFullName && osVersion) { if (updatedOSTracker[osFullName]) updatedOSTracker[osFullName].count += 1; @@ -93,18 +96,32 @@ export const trackEndpointOSTelemetry = ( }; /** - * @description This iterates over all unique agents that currently track an endpoint package. It takes a list of agents who have checked in in the last 24 hours - * and then checks whether those agents have endpoints whose latest status is 'RUNNING' to determine an active_within_last_24_hours. Since the policy information is also tracked in these events - * we pull out the status of the current protection (malware) type. This must be done in a compound manner as the desired status is reflected in the config, and the successful application of that policy - * is tracked in the policy.applied.response.configurations[protectionsType].status. Using these two we can determine whether the policy is toggled on, off, or failed to turn on. + * @description we take the latest endpoint specific agent event, get the status of the endpoint, and if it is running + * and the agent itself has been active within the last 24 hours, we can safely assume the endpoint has been active within + * the same time span. */ -export const addEndpointDailyActivityAndPolicyDetailsToTelemetry = async ( - agentDailyActiveTracker: AgentDailyActiveTracker, - savedObjectsClient: ISavedObjectsRepository, - endpointTelemetry: EndpointUsage -): Promise => { - const updatedEndpointTelemetry = { ...endpointTelemetry }; +export const updateEndpointDailyActiveCount = ( + latestEndpointEvent: SavedObject, + lastAgentCheckin: Agent['last_checkin'], + currentCount: number +) => { + const aDayAgo = new Date(); + aDayAgo.setDate(aDayAgo.getDate() - 1); + + const agentWasActiveOverLastDay = !!lastAgentCheckin && new Date(lastAgentCheckin) > aDayAgo; + return agentWasActiveOverLastDay && latestEndpointEvent.attributes.subtype === 'RUNNING' + ? currentCount + 1 + : currentCount; +}; +/** + * @description We take the latest endpoint specific agent event, and as long as it provides the payload with policy details, we will parse that policy + * to populate the success of it's application. The policy is provided in the agent health checks. + */ +export const updateEndpointPolicyTelemetry = ( + latestEndpointEvent: SavedObject, + policiesTracker: PoliciesTelemetry +): PoliciesTelemetry => { const policyHostTypeToPolicyType = { Linux: 'linux', macOs: 'mac', @@ -112,58 +129,60 @@ export const addEndpointDailyActivityAndPolicyDetailsToTelemetry = async ( }; const enabledMalwarePolicyTypes = ['prevent', 'detect']; - for (const agentId of agentDailyActiveTracker.keys()) { - const { saved_objects: agentEvents } = await getLatestFleetEndpointEvent( - savedObjectsClient, - agentId - ); - - const latestEndpointEvent = agentEvents[0]; - if (latestEndpointEvent) { - /* - We can assume that if the last status of the endpoint is RUNNING and the agent has checked in within the last 24 hours - then the endpoint has still been running within the last 24 hours. - */ - const { subtype, payload } = latestEndpointEvent.attributes; - const endpointIsActive = - subtype === 'RUNNING' && agentDailyActiveTracker.get(agentId) === true; - - if (endpointIsActive) { - updatedEndpointTelemetry.active_within_last_24_hours += 1; - } + // The policy details are sent as a string on the 'payload' attribute of the agent event + const { payload } = latestEndpointEvent.attributes; - // The policy details are sent as a string on the 'payload' attribute of the agent event - const endpointPolicyDetails = payload ? JSON.parse(payload) : null; - if (endpointPolicyDetails) { - // We get the setting the user desired to enable (treating prevent and detect as 'active' states) and then see if it succeded or failed. - const hostType = - policyHostTypeToPolicyType[ - endpointPolicyDetails['endpoint-security']?.host?.os?.name as EndpointOSNames - ]; - const userDesiredMalwareState = - endpointPolicyDetails['endpoint-security'].Endpoint?.configuration?.inputs[0]?.policy[ - hostType - ]?.malware?.mode; - - const isAnActiveMalwareState = enabledMalwarePolicyTypes.includes(userDesiredMalwareState); - const malwareStatus = - endpointPolicyDetails['endpoint-security'].Endpoint?.policy?.applied?.response - ?.configurations?.malware?.status; - - if (isAnActiveMalwareState && malwareStatus !== 'failure') { - updatedEndpointTelemetry.policies.malware.active += 1; - } - if (!isAnActiveMalwareState) { - updatedEndpointTelemetry.policies.malware.inactive += 1; - } - if (isAnActiveMalwareState && malwareStatus === 'failure') { - updatedEndpointTelemetry.policies.malware.failure += 1; - } - } - } + if (!payload) { + // This payload may not always be provided depending on the state of the endpoint. Guard again situations where it is not sent + return policiesTracker; + } + + let endpointPolicyPayload; + try { + endpointPolicyPayload = JSON.parse(latestEndpointEvent.attributes.payload); + } catch (error) { + return policiesTracker; + } + + // Get the platform: windows, mac, or linux + const hostType = + policyHostTypeToPolicyType[ + endpointPolicyPayload['endpoint-security']?.host?.os?.name as EndpointOSNames + ]; + // Get whether the malware setting for the platform on the most recently provided config is active (prevent or detect is on) or off + const userDesiredMalwareState = + endpointPolicyPayload['endpoint-security'].Endpoint?.configuration?.inputs[0]?.policy[hostType] + ?.malware?.mode; + + // Get the status of the application of the malware protection + const malwareStatus = + endpointPolicyPayload['endpoint-security'].Endpoint?.policy?.applied?.response?.configurations + ?.malware?.status; + + if (!userDesiredMalwareState || !malwareStatus) { + // If we get policy information without the mode or status, then nothing to track or update + return policiesTracker; } - return updatedEndpointTelemetry; + const updatedPoliciesTracker = { + malware: { ...policiesTracker.malware }, + }; + + const isAnActiveMalwareState = enabledMalwarePolicyTypes.includes(userDesiredMalwareState); + + // we only check for 'not failure' as the 'warning' state for malware is still technically actively enabled (with warnings) + const successfullyEnabled = !!malwareStatus && malwareStatus !== 'failure'; + const failedToEnable = !!malwareStatus && malwareStatus === 'failure'; + + if (isAnActiveMalwareState && successfullyEnabled) { + updatedPoliciesTracker.malware.active += 1; + } else if (!isAnActiveMalwareState && successfullyEnabled) { + updatedPoliciesTracker.malware.inactive += 1; + } else if (isAnActiveMalwareState && failedToEnable) { + updatedPoliciesTracker.malware.failure += 1; + } + + return updatedPoliciesTracker; }; /** @@ -173,53 +192,71 @@ export const addEndpointDailyActivityAndPolicyDetailsToTelemetry = async ( * to confirm whether or not the endpoint is still active */ export const getEndpointTelemetryFromFleet = async ( - savedObjectsClient: ISavedObjectsRepository -): Promise => { - // Retrieve every agent that references the endpoint as an installed package. It will not be listed if it was never installed - const { saved_objects: endpointAgents } = await getFleetSavedObjectsMetadata(savedObjectsClient); + soClient: ISavedObjectsRepository +): Promise => { + // Retrieve every agent (max 10000) that references the endpoint as an installed package. It will not be listed if it was never installed + let endpointAgents; + try { + const response = await getFleetSavedObjectsMetadata(soClient); + endpointAgents = response.saved_objects; + } catch (error) { + // Better to provide an empty object rather than default telemetry as this better informs us of an error + return {}; + } + + const endpointAgentsCount = endpointAgents.length; const endpointTelemetry = getDefaultEndpointTelemetry(); // If there are no installed endpoints return the default telemetry object - if (!endpointAgents || endpointAgents.length < 1) return endpointTelemetry; + if (!endpointAgents || endpointAgentsCount < 1) return endpointTelemetry; // Use unique hosts to prevent any potential duplicates const uniqueHostIds: Set = new Set(); - // Need agents to get events data for those that have run in last 24 hours as well as policy details - const agentDailyActiveTracker: AgentDailyActiveTracker = new Map(); - - const aDayAgo = new Date(); - aDayAgo.setDate(aDayAgo.getDate() - 1); let osTracker: OSTracker = {}; + let dailyActiveCount = 0; + let policyTracker: PoliciesTelemetry = { malware: { active: 0, inactive: 0, failure: 0 } }; + + for (let i = 0; i < endpointAgentsCount; i += 1) { + const { attributes: metadataAttributes } = endpointAgents[i]; + const { last_checkin: lastCheckin, local_metadata: localMetadata } = metadataAttributes; + const { host, os, elastic } = localMetadata as AgentLocalMetadata; // AgentMetadata is just an empty blob, casting for our use case + + if (!uniqueHostIds.has(host.id)) { + uniqueHostIds.add(host.id); + const agentId = elastic?.agent?.id; + osTracker = updateEndpointOSTelemetry(os, osTracker); + + if (agentId) { + let agentEvents; + try { + const response = await getLatestFleetEndpointEvent(soClient, agentId); + agentEvents = response.saved_objects; + } catch (error) { + // If the request fails we do not obtain `active within last 24 hours for this agent` or policy specifics + } - const endpointMetadataTelemetry = endpointAgents.reduce( - (metadataTelemetry, { attributes: metadataAttributes }) => { - const { last_checkin: lastCheckin, local_metadata: localMetadata } = metadataAttributes; - const { host, os, elastic } = localMetadata as AgentLocalMetadata; // AgentMetadata is just an empty blob, casting for our use case - - if (host && uniqueHostIds.has(host.id)) { - // use hosts since new agents could potentially be re-installed on existing hosts - return metadataTelemetry; - } else { - uniqueHostIds.add(host.id); - const isActiveWithinLastDay = !!lastCheckin && new Date(lastCheckin) > aDayAgo; - agentDailyActiveTracker.set(elastic.agent.id, isActiveWithinLastDay); - osTracker = trackEndpointOSTelemetry(os, osTracker); - return metadataTelemetry; + // AgentEvents will have a max length of 1 + if (agentEvents && agentEvents.length > 0) { + const latestEndpointEvent = agentEvents[0]; + dailyActiveCount = updateEndpointDailyActiveCount( + latestEndpointEvent, + lastCheckin, + dailyActiveCount + ); + policyTracker = updateEndpointPolicyTelemetry(latestEndpointEvent, policyTracker); + } } - }, - endpointTelemetry - ); + } + } - // All unique hosts with an endpoint installed. + // All unique hosts with an endpoint installed, thus all unique endpoint installs endpointTelemetry.total_installed = uniqueHostIds.size; + // Set the daily active count for the endpoints + endpointTelemetry.active_within_last_24_hours = dailyActiveCount; // Get the objects to populate our OS Telemetry - endpointMetadataTelemetry.os = Object.values(osTracker); - // Populate endpoint telemetry with the finalized 24 hour count and policy details - const finalizedEndpointTelemetryData = await addEndpointDailyActivityAndPolicyDetailsToTelemetry( - agentDailyActiveTracker, - savedObjectsClient, - endpointMetadataTelemetry - ); - - return finalizedEndpointTelemetryData; + endpointTelemetry.os = Object.values(osTracker); + // Provide the updated policy information + endpointTelemetry.policies = policyTracker; + + return endpointTelemetry; }; From 5741a868bc524dbb8363b85f4a0f37fd8ab321f8 Mon Sep 17 00:00:00 2001 From: spalger Date: Mon, 20 Jul 2020 09:32:41 -0700 Subject: [PATCH 41/45] Revert "skip flaky suite (#72146)" This reverts commit 45a4393459e0400171564f1d096784ebc97cc8ed. --- test/functional/apps/dashboard/dashboard_error_handling.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/functional/apps/dashboard/dashboard_error_handling.ts b/test/functional/apps/dashboard/dashboard_error_handling.ts index 38803739ff129..6bd8327a110b9 100644 --- a/test/functional/apps/dashboard/dashboard_error_handling.ts +++ b/test/functional/apps/dashboard/dashboard_error_handling.ts @@ -28,8 +28,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { /** * Common test suite for testing exception scenarious within dashboard */ - // Flaky: https://github.com/elastic/kibana/issues/72146 - describe.skip('dashboard error handling', () => { + describe('dashboard error handling', () => { before(async () => { await esArchiver.loadIfNeeded('dashboard/current/kibana'); await PageObjects.common.navigateToApp('dashboard'); From 75e4c7a2b73c5e0606e11cb3fecf08337a0ea81e Mon Sep 17 00:00:00 2001 From: Robert Austin Date: Mon, 20 Jul 2020 12:40:59 -0400 Subject: [PATCH 42/45] [Resolver] no longer pass related event stats to process node component (#72435) --- .../public/resolver/store/data/selectors.ts | 38 ++++--------------- .../public/resolver/store/selectors.ts | 6 ++- .../public/resolver/view/map.tsx | 4 -- .../public/resolver/view/panel.tsx | 5 ++- .../resolver/view/process_event_dot.tsx | 20 +++------- 5 files changed, 20 insertions(+), 53 deletions(-) diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts index 109b3abddcc77..4098c6fc6c5dd 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts @@ -116,13 +116,14 @@ export const tree = createSelector(graphableProcesses, function indexedTree( */ export const relatedEventsStats: ( state: DataState -) => Map | null = createSelector( +) => (nodeID: string) => ResolverNodeStats | undefined = createSelector( resolverTreeResponse, (resolverTree?: ResolverTree) => { if (resolverTree) { - return resolverTreeModel.relatedEventsStats(resolverTree); + const map = resolverTreeModel.relatedEventsStats(resolverTree); + return (nodeID: string) => map.get(nodeID); } else { - return null; + return () => undefined; } } ); @@ -213,12 +214,8 @@ export const relatedEventInfoByEntityId: ( relatedEventsStats /* eslint-enable no-shadow */ ) { - if (!relatedEventsStats) { - // If there are no related event stats, there are no related event info objects - return () => null; - } return (entityId) => { - const stats = relatedEventsStats.get(entityId); + const stats = relatedEventsStats(entityId); if (!stats) { return null; } @@ -524,37 +521,16 @@ export function databaseDocumentIDToAbort(state: DataState): string | null { } } -/** - * `ResolverNodeStats` for a process (`ResolverEvent`) - */ -const relatedEventStatsForProcess: ( - state: DataState -) => (event: ResolverEvent) => ResolverNodeStats | null = createSelector( - relatedEventsStats, - (statsMap) => { - if (!statsMap) { - return () => null; - } - return (event: ResolverEvent) => { - const nodeStats = statsMap.get(uniquePidForProcess(event)); - if (!nodeStats) { - return null; - } - return nodeStats; - }; - } -); - /** * The sum of all related event categories for a process. */ export const relatedEventTotalForProcess: ( state: DataState ) => (event: ResolverEvent) => number | null = createSelector( - relatedEventStatsForProcess, + relatedEventsStats, (statsForProcess) => { return (event: ResolverEvent) => { - const stats = statsForProcess(event); + const stats = statsForProcess(uniquePidForProcess(event)); if (!stats) { return null; } diff --git a/x-pack/plugins/security_solution/public/resolver/store/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/selectors.ts index 040e2920ce554..09293d0b3b683 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/selectors.ts @@ -10,7 +10,7 @@ import * as dataSelectors from './data/selectors'; import * as uiSelectors from './ui/selectors'; import { ResolverState, IsometricTaxiLayout } from '../types'; import { uniquePidForProcess } from '../models/process_event'; -import { ResolverEvent } from '../../../common/endpoint/types'; +import { ResolverEvent, ResolverNodeStats } from '../../../common/endpoint/types'; /** * A matrix that when applied to a Vector2 will convert it from world coordinates to screen coordinates. @@ -99,7 +99,9 @@ export const terminatedProcesses = composeSelectors( /** * Returns a map of `ResolverEvent` entity_id to their related event and alert statistics */ -export const relatedEventsStats = composeSelectors( +export const relatedEventsStats: ( + state: ResolverState +) => (nodeID: string) => ResolverNodeStats | undefined = composeSelectors( dataStateSelector, dataSelectors.relatedEventsStats ); diff --git a/x-pack/plugins/security_solution/public/resolver/view/map.tsx b/x-pack/plugins/security_solution/public/resolver/view/map.tsx index b366e2f220652..930e96c3f3e40 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/map.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/map.tsx @@ -60,7 +60,6 @@ export const ResolverMap = React.memo(function ({ const { processNodePositions, connectingEdgeLineSegments } = useSelector( selectors.visibleNodesAndEdgeLines )(timeAtRender); - const relatedEventsStats = useSelector(selectors.relatedEventsStats); const terminatedProcesses = useSelector(selectors.terminatedProcesses); const { projectionMatrix, ref, onMouseDown } = useCamera(); const isLoading = useSelector(selectors.isLoading); @@ -110,9 +109,6 @@ export const ResolverMap = React.memo(function ({ position={position} projectionMatrix={projectionMatrix} event={processEvent} - relatedEventsStatsForProcess={ - relatedEventsStats ? relatedEventsStats.get(entityId(processEvent)) : undefined - } isProcessTerminated={terminatedProcesses.has(processEntityId)} isProcessOrigin={false} timeAtRender={timeAtRender} diff --git a/x-pack/plugins/security_solution/public/resolver/view/panel.tsx b/x-pack/plugins/security_solution/public/resolver/view/panel.tsx index 47ce9b949fa59..efb2d95396ef5 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panel.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panel.tsx @@ -99,8 +99,9 @@ const PanelContent = memo(function PanelContent() { const relatedEventStats = useSelector(selectors.relatedEventsStats); const { crumbId, crumbEvent } = queryParams; - const relatedStatsForIdFromParams: ResolverNodeStats | undefined = - idFromParams && relatedEventStats ? relatedEventStats.get(idFromParams) : undefined; + const relatedStatsForIdFromParams: ResolverNodeStats | undefined = idFromParams + ? relatedEventStats(idFromParams) + : undefined; /** * Determine which set of breadcrumbs to display based on the query parameters diff --git a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx index 7666d1ac7c88a..aab4193bf031d 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx @@ -14,7 +14,7 @@ import { NodeSubMenu, subMenuAssets } from './submenu'; import { applyMatrix3 } from '../models/vector2'; import { Vector2, Matrix3 } from '../types'; import { SymbolIds, useResolverTheme, calculateResolverFontSize } from './assets'; -import { ResolverEvent, ResolverNodeStats } from '../../../common/endpoint/types'; +import { ResolverEvent } from '../../../common/endpoint/types'; import { useResolverDispatch } from './use_resolver_dispatch'; import * as eventModel from '../../../common/endpoint/models/event'; import * as processEventModel from '../models/process_event'; @@ -73,7 +73,6 @@ const UnstyledProcessEventDot = React.memo( projectionMatrix, isProcessTerminated, isProcessOrigin, - relatedEventsStatsForProcess, timeAtRender, }: { /** @@ -100,12 +99,6 @@ const UnstyledProcessEventDot = React.memo( * Whether or not to show the process as the originating event. */ isProcessOrigin: boolean; - /** - * A collection of events related to the current node and statistics (e.g. counts indexed by event type) - * to provide the user some visibility regarding the contents thereof. - * Statistics for the number of related events and alerts for this process node - */ - relatedEventsStatsForProcess?: ResolverNodeStats; /** * The time (unix epoch) at render. @@ -127,6 +120,7 @@ const UnstyledProcessEventDot = React.memo( const activeDescendantId = useSelector(selectors.uiActiveDescendantId); const selectedDescendantId = useSelector(selectors.uiSelectedDescendantId); const nodeID = processEventModel.uniquePidForProcess(event); + const relatedEventStats = useSelector(selectors.relatedEventsStats)(nodeID); // define a standard way of giving HTML IDs to nodes based on their entity_id/nodeID. // this is used to link nodes via aria attributes @@ -270,15 +264,13 @@ const UnstyledProcessEventDot = React.memo( const relatedEventOptions = useMemo(() => { const relatedStatsList = []; - if (!relatedEventsStatsForProcess) { + if (!relatedEventStats) { // Return an empty set of options if there are no stats to report return []; } // If we have entries to show, map them into options to display in the selectable list - for (const [category, total] of Object.entries( - relatedEventsStatsForProcess.events.byCategory - )) { + for (const [category, total] of Object.entries(relatedEventStats.events.byCategory)) { relatedStatsList.push({ prefix: , optionTitle: category, @@ -296,9 +288,9 @@ const UnstyledProcessEventDot = React.memo( }); } return relatedStatsList; - }, [relatedEventsStatsForProcess, dispatch, event, pushToQueryParams, nodeID]); + }, [relatedEventStats, dispatch, event, pushToQueryParams, nodeID]); - const relatedEventStatusOrOptions = !relatedEventsStatsForProcess + const relatedEventStatusOrOptions = !relatedEventStats ? subMenuAssets.initialMenuStatus : relatedEventOptions; From afae94a85ec8d58221f673c23f76aa664fe0cea0 Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Mon, 20 Jul 2020 11:00:06 -0600 Subject: [PATCH 43/45] [SIEM][Detection Engine][Lists] Adds conflict versioning and io-ts improvements to lists (#72337) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary * Adds conflict versioning by exposing the "_version" from the saved object system. It renames "version" to "_version" so that we can use regular "version" later for versioning things for pre-packaged lists abilities. * Utilizes `t.OutputOf` in the requests and the data types to give us more correctly types * Removes the `Identity` utility as that is adding confusion and can confuse vs code rather than improves things * Removes extra types that were causing confusion which was an idiom from io-ts * Changes the wording of `Partial` by removing that and instead focuses the request types on either client side or server side at this point. NOTE: The UI can migrate to holding onto the `_version` and then push it back down when it wants to migrate to using the conflict resolution. If the UI does not push it down, then a value of undefined will be used which is indicating that no conflict errors are wanted. Output example of posting an exception list: ❯ ./post_exception_list.sh ```ts { "_tags": [ "endpoint", "process", "malware", "os:linux" ], "_version": "Wzk4NiwxXQ==", "created_at": "2020-07-17T18:59:22.872Z", "created_by": "yo", "description": "This is a sample endpoint type exception", "id": "a08795b0-c85f-11ea-b1a6-c155df988a92", "list_id": "simple_list", "name": "Sample Endpoint Exception List", "namespace_type": "single", "tags": [ "user added string for a tag", "malware" ], "tie_breaker_id": "b789ec05-3e0f-4344-a156-0c0f5b6e2f9c", "type": "detection", "updated_at": "2020-07-17T18:59:22.891Z", "updated_by": "yo" } ``` Output example of posting an exception list item ❯ ./post_exception_list_item.sh ```ts { "_tags": [ "endpoint", "process", "malware", "os:linux" ], "_version": "Wzk4NywxXQ==", "comments": [], "created_at": "2020-07-17T18:59:30.286Z", "created_by": "yo", "description": "This is a sample endpoint type exception", "entries": [ { "field": "actingProcess.file.signer", "operator": "excluded", "type": "exists" }, { "field": "host.name", "operator": "included", "type": "match_any", "value": [ "some host", "another host" ] } ], "id": "a4f2b800-c85f-11ea-b1a6-c155df988a92", "item_id": "simple_list_item", "list_id": "simple_list", "name": "Sample Endpoint Exception List", "namespace_type": "single", "tags": [ "user added string for a tag", "malware" ], "tie_breaker_id": "1dc456bc-7aa9-44b4-bca3-131689cf729f", "type": "simple", "updated_at": "2020-07-17T18:59:30.304Z", "updated_by": "yo" } ``` Output example of when you get an exception list: ❯ ./get_exception_list.sh simple_list ```ts { "_tags": [ "endpoint", "process", "malware", "os:linux" ], "_version": "WzEwNzcsMV0=", "created_at": "2020-07-17T18:59:22.872Z", "created_by": "yo", "description": "Different description", "id": "a08795b0-c85f-11ea-b1a6-c155df988a92", "list_id": "simple_list", "name": "Sample Endpoint Exception List", "namespace_type": "single", "tags": [ "user added string for a tag", "malware" ], "tie_breaker_id": "b789ec05-3e0f-4344-a156-0c0f5b6e2f9c", "type": "endpoint", "updated_at": "2020-07-17T20:01:24.958Z", "updated_by": "yo" } ``` Example of the error you get if you do an update of an exception list and someone else has changed it: ```ts { "message": "[exception-list:a08795b0-c85f-11ea-b1a6-c155df988a92]: version conflict, required seqNo [1074], primary term [1]. current document has seqNo [1077] and primary term [1]: [version_conflict_engine_exception] [exception-list:a08795b0-c85f-11ea-b1a6-c155df988a92]: version conflict, required seqNo [1074], primary term [1]. current document has seqNo [1077] and primary term [1], with { index_uuid=\"a2mgXBO6Tl2ULDq-MTs1Tw\" & shard=\"0\" & index=\".kibana-hassanabad_1\" }", "status_code": 409 } ``` Lists are the same way and flavor, they encode the _version the same way that saved objects do. To see those work you run these scripts: ```ts ./post_list.sh ./post_list_item.sh ./find_list.sh ./find_list_item.sh ``` ### Checklist - [x] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios --- x-pack/plugins/lists/common/constants.mock.ts | 1 + .../lists/common/schemas/common/schemas.ts | 4 ++ .../elastic_query/create_es_bulk_type.ts | 2 +- .../index_es_list_item_schema.ts | 2 +- .../elastic_query/index_es_list_schema.ts | 2 +- .../update_es_list_item_schema.ts | 2 +- .../elastic_query/update_es_list_schema.ts | 2 +- .../create_endpoint_list_item_schema.ts | 28 ++++++-------- .../create_exception_list_item_schema.ts | 33 +++++++---------- .../request/create_exception_list_schema.ts | 24 ++++++------ .../request/create_list_item_schema.ts | 8 ++-- .../schemas/request/create_list_schema.ts | 6 +-- .../delete_endpoint_list_item_schema.ts | 7 +++- .../delete_exception_list_item_schema.ts | 5 ++- .../request/delete_exception_list_schema.ts | 8 +++- .../request/delete_list_item_schema.ts | 8 ++-- .../schemas/request/delete_list_schema.ts | 3 +- .../request/export_list_item_query_schema.ts | 5 ++- .../find_endpoint_list_item_schema.mock.ts | 8 ++-- .../find_endpoint_list_item_schema.test.ts | 6 +-- .../request/find_endpoint_list_item_schema.ts | 9 +---- .../find_exception_list_item_schema.mock.ts | 12 +++--- .../find_exception_list_item_schema.test.ts | 16 +++++--- .../find_exception_list_item_schema.ts | 15 ++------ .../find_exception_list_schema.mock.ts | 8 ++-- .../find_exception_list_schema.test.ts | 15 +++++--- .../request/find_exception_list_schema.ts | 15 ++------ .../request/find_list_item_schema.mock.ts | 9 ++--- .../request/find_list_item_schema.test.ts | 10 ++--- .../schemas/request/find_list_item_schema.ts | 8 ++-- .../schemas/request/find_list_schema.mock.ts | 1 + .../schemas/request/find_list_schema.test.ts | 5 +-- .../schemas/request/find_list_schema.ts | 3 +- .../request/import_list_item_query_schema.ts | 8 ++-- .../request/import_list_item_schema.ts | 3 +- .../schemas/request/patch_list_item_schema.ts | 12 +++--- .../schemas/request/patch_list_schema.ts | 10 ++--- .../request/read_endpoint_list_item_schema.ts | 9 +---- .../read_exception_list_item_schema.ts | 13 ++----- .../request/read_exception_list_schema.ts | 13 ++----- .../schemas/request/read_list_item_schema.ts | 6 +-- .../schemas/request/read_list_schema.ts | 2 +- .../update_endpoint_list_item_schema.mock.ts | 1 + .../update_endpoint_list_item_schema.ts | 28 +++++++------- .../update_exception_list_item_schema.mock.ts | 1 + .../update_exception_list_item_schema.ts | 33 +++++++---------- .../update_exception_list_schema.mock.ts | 1 + .../request/update_exception_list_schema.ts | 30 ++++++++------- .../request/update_list_item_schema.ts | 11 ++++-- .../schemas/request/update_list_schema.ts | 9 +++-- .../create_endpoint_list_schema.test.ts | 2 +- .../exception_list_item_schema.mock.ts | 36 ++++++++++++------ .../response/exception_list_item_schema.ts | 2 + .../response/exception_list_schema.mock.ts | 25 +++++++++---- .../schemas/response/exception_list_schema.ts | 2 + .../schemas/response/list_item_schema.mock.ts | 1 + .../schemas/response/list_item_schema.ts | 2 + .../schemas/response/list_schema.mock.ts | 1 + .../common/schemas/response/list_schema.ts | 2 + .../schemas/types/default_comments_array.ts | 8 +--- .../types/default_create_comments_array.ts | 4 +- .../schemas/types/default_entries_array.ts | 8 +--- .../common/schemas/types/default_namespace.ts | 4 +- .../types/default_namespace_array.test.ts | 18 ++++----- .../schemas/types/default_namespace_array.ts | 4 +- .../types/default_update_comments_array.ts | 4 +- .../schemas/types/empty_string_array.ts | 2 - .../types/non_empty_string_array.test.ts | 16 ++++---- .../schemas/types/non_empty_string_array.ts | 4 +- x-pack/plugins/lists/common/types.ts | 8 ---- .../server/routes/find_list_item_route.ts | 4 +- .../server/routes/patch_list_item_route.ts | 3 +- .../lists/server/routes/patch_list_route.ts | 4 +- .../routes/update_endpoint_list_item_route.ts | 2 + .../update_exception_list_item_route.ts | 2 + .../routes/update_exception_list_route.ts | 2 + .../server/routes/update_list_item_route.ts | 3 +- .../lists/server/routes/update_list_route.ts | 4 +- .../updates/simple_update.json | 2 +- .../exception_lists/exception_list_client.ts | 6 +++ .../exception_list_client_types.ts | 4 ++ .../exception_lists/update_exception_list.ts | 6 +++ .../update_exception_list_item.ts | 6 +++ .../server/services/exception_lists/utils.ts | 8 ++++ .../server/services/items/create_list_item.ts | 2 + .../server/services/items/find_list_item.ts | 7 +++- .../server/services/items/get_list_item.ts | 7 +++- .../services/items/update_list_item.mock.ts | 1 + .../server/services/items/update_list_item.ts | 7 ++++ .../server/services/lists/create_list.ts | 2 + .../lists/server/services/lists/find_list.ts | 7 +++- .../lists/server/services/lists/get_list.ts | 7 +++- .../server/services/lists/list_client.ts | 4 ++ .../services/lists/list_client_types.ts | 3 ++ .../server/services/lists/update_list.mock.ts | 1 + .../server/services/lists/update_list.ts | 7 ++++ .../server/services/utils/decode_version.ts | 37 +++++++++++++++++++ .../services/utils/encode_hit_version.ts | 27 ++++++++++++++ .../utils/transform_elastic_to_list.ts | 3 ++ .../utils/transform_elastic_to_list_item.ts | 2 + .../schemas/types/default_actions_array.ts | 6 +-- .../schemas/types/default_boolean_false.ts | 4 +- .../schemas/types/default_boolean_true.ts | 4 +- .../schemas/types/default_empty_string.ts | 4 +- .../schemas/types/default_export_file_name.ts | 4 +- .../schemas/types/default_from_string.ts | 4 +- .../schemas/types/default_interval_string.ts | 4 +- .../schemas/types/default_language_string.ts | 4 +- .../types/default_max_signals_number.ts | 8 +--- .../schemas/types/default_page.ts | 4 +- .../schemas/types/default_per_page.ts | 4 +- .../types/default_risk_score_mapping_array.ts | 8 ++-- .../types/default_severity_mapping_array.ts | 8 ++-- .../schemas/types/default_string_array.ts | 7 ++-- .../types/default_string_boolean_false.ts | 2 +- .../schemas/types/default_threat_array.ts | 4 +- .../schemas/types/default_throttle_null.ts | 4 +- .../schemas/types/default_to_string.ts | 4 +- .../schemas/types/default_uuid.ts | 4 +- .../schemas/types/default_version_number.ts | 4 +- .../schemas/types/lists_default_array.test.ts | 4 +- .../schemas/types/lists_default_array.ts | 4 +- .../schemas/types/only_false_allowed.ts | 4 +- .../schemas/types/positive_integer.ts | 2 - .../positive_integer_greater_than_zero.ts | 2 - .../schemas/types/references_default_array.ts | 4 +- .../components/exceptions/helpers.test.tsx | 16 ++++---- .../exception_item/exception_details.test.tsx | 8 ++-- 128 files changed, 517 insertions(+), 435 deletions(-) create mode 100644 x-pack/plugins/lists/server/services/utils/decode_version.ts create mode 100644 x-pack/plugins/lists/server/services/utils/encode_hit_version.ts diff --git a/x-pack/plugins/lists/common/constants.mock.ts b/x-pack/plugins/lists/common/constants.mock.ts index 4924ba24426af..6ed1d19611c68 100644 --- a/x-pack/plugins/lists/common/constants.mock.ts +++ b/x-pack/plugins/lists/common/constants.mock.ts @@ -60,3 +60,4 @@ export const TAGS = []; export const COMMENTS = []; export const FILTER = 'name:Nicolas Bourbaki'; export const CURSOR = 'c29tZXN0cmluZ2ZvcnlvdQ=='; +export const _VERSION = 'WzI5NywxXQ=='; diff --git a/x-pack/plugins/lists/common/schemas/common/schemas.ts b/x-pack/plugins/lists/common/schemas/common/schemas.ts index 6199a5f16f109..8f1666bb542d9 100644 --- a/x-pack/plugins/lists/common/schemas/common/schemas.ts +++ b/x-pack/plugins/lists/common/schemas/common/schemas.ts @@ -307,3 +307,7 @@ export type Deserializer = t.TypeOf; export const deserializerOrUndefined = t.union([deserializer, t.undefined]); export type DeserializerOrUndefined = t.TypeOf; + +export const _version = t.string; +export const _versionOrUndefined = t.union([_version, t.undefined]); +export type _VersionOrUndefined = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/elastic_query/create_es_bulk_type.ts b/x-pack/plugins/lists/common/schemas/elastic_query/create_es_bulk_type.ts index 4a825382c06e4..3104ee27c57de 100644 --- a/x-pack/plugins/lists/common/schemas/elastic_query/create_es_bulk_type.ts +++ b/x-pack/plugins/lists/common/schemas/elastic_query/create_es_bulk_type.ts @@ -14,4 +14,4 @@ export const createEsBulkTypeSchema = t.exact( }) ); -export type CreateEsBulkTypeSchema = t.TypeOf; +export type CreateEsBulkTypeSchema = t.OutputOf; diff --git a/x-pack/plugins/lists/common/schemas/elastic_query/index_es_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/elastic_query/index_es_list_item_schema.ts index 006600ee5b7fd..8dc5a376d1495 100644 --- a/x-pack/plugins/lists/common/schemas/elastic_query/index_es_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/elastic_query/index_es_list_item_schema.ts @@ -38,4 +38,4 @@ export const indexEsListItemSchema = t.intersection([ esDataTypeUnion, ]); -export type IndexEsListItemSchema = t.TypeOf; +export type IndexEsListItemSchema = t.OutputOf; diff --git a/x-pack/plugins/lists/common/schemas/elastic_query/index_es_list_schema.ts b/x-pack/plugins/lists/common/schemas/elastic_query/index_es_list_schema.ts index fd1018bc46a8d..3ee598291149f 100644 --- a/x-pack/plugins/lists/common/schemas/elastic_query/index_es_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/elastic_query/index_es_list_schema.ts @@ -38,4 +38,4 @@ export const indexEsListSchema = t.exact( }) ); -export type IndexEsListSchema = t.TypeOf; +export type IndexEsListSchema = t.OutputOf; diff --git a/x-pack/plugins/lists/common/schemas/elastic_query/update_es_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/elastic_query/update_es_list_item_schema.ts index e4cf46bc39429..20187de535a8e 100644 --- a/x-pack/plugins/lists/common/schemas/elastic_query/update_es_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/elastic_query/update_es_list_item_schema.ts @@ -21,4 +21,4 @@ export const updateEsListItemSchema = t.intersection([ esDataTypeUnion, ]); -export type UpdateEsListItemSchema = t.TypeOf; +export type UpdateEsListItemSchema = t.OutputOf; diff --git a/x-pack/plugins/lists/common/schemas/elastic_query/update_es_list_schema.ts b/x-pack/plugins/lists/common/schemas/elastic_query/update_es_list_schema.ts index 8f23f3744e563..80b9733908d39 100644 --- a/x-pack/plugins/lists/common/schemas/elastic_query/update_es_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/elastic_query/update_es_list_schema.ts @@ -26,4 +26,4 @@ export const updateEsListSchema = t.exact( }) ); -export type UpdateEsListSchema = t.TypeOf; +export type UpdateEsListSchema = t.OutputOf; diff --git a/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.ts index 5311c7a43cdb5..3f0e1a12894d4 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.ts @@ -19,7 +19,7 @@ import { name, tags, } from '../common/schemas'; -import { Identity, RequiredKeepUndefined } from '../../types'; +import { RequiredKeepUndefined } from '../../types'; import { CreateCommentsArray, DefaultCreateCommentsArray, DefaultEntryArray } from '../types'; import { EntriesArray } from '../types/entries'; import { DefaultUuid } from '../../siem_common_deps'; @@ -44,20 +44,16 @@ export const createEndpointListItemSchema = t.intersection([ ), ]); -export type CreateEndpointListItemSchemaPartial = Identity< - t.TypeOf ->; -export type CreateEndpointListItemSchema = RequiredKeepUndefined< - t.TypeOf ->; +export type CreateEndpointListItemSchema = t.OutputOf; // This type is used after a decode since some things are defaults after a decode. -export type CreateEndpointListItemSchemaDecoded = Identity< - Omit & { - _tags: _Tags; - comments: CreateCommentsArray; - tags: Tags; - item_id: ItemId; - entries: EntriesArray; - } ->; +export type CreateEndpointListItemSchemaDecoded = Omit< + RequiredKeepUndefined>, + '_tags' | 'tags' | 'item_id' | 'entries' | 'comments' +> & { + _tags: _Tags; + comments: CreateCommentsArray; + tags: Tags; + item_id: ItemId; + entries: EntriesArray; +}; diff --git a/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.ts index 4b7db3eee35bc..c2ccf18ed8720 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.ts @@ -21,7 +21,7 @@ import { namespace_type, tags, } from '../common/schemas'; -import { Identity, RequiredKeepUndefined } from '../../types'; +import { RequiredKeepUndefined } from '../../types'; import { CreateCommentsArray, DefaultCreateCommentsArray, @@ -53,24 +53,17 @@ export const createExceptionListItemSchema = t.intersection([ ), ]); -export type CreateExceptionListItemSchemaPartial = Identity< - t.TypeOf ->; -export type CreateExceptionListItemSchema = RequiredKeepUndefined< - t.TypeOf ->; +export type CreateExceptionListItemSchema = t.OutputOf; // This type is used after a decode since some things are defaults after a decode. -export type CreateExceptionListItemSchemaDecoded = Identity< - Omit< - CreateExceptionListItemSchema, - '_tags' | 'tags' | 'item_id' | 'entries' | 'namespace_type' | 'comments' - > & { - _tags: _Tags; - comments: CreateCommentsArray; - tags: Tags; - item_id: ItemId; - entries: EntriesArray; - namespace_type: NamespaceType; - } ->; +export type CreateExceptionListItemSchemaDecoded = Omit< + RequiredKeepUndefined>, + '_tags' | 'tags' | 'item_id' | 'entries' | 'namespace_type' | 'comments' +> & { + _tags: _Tags; + comments: CreateCommentsArray; + tags: Tags; + item_id: ItemId; + entries: EntriesArray; + namespace_type: NamespaceType; +}; diff --git a/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.ts index 66cca4ab9ca53..8f714760621ff 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.ts @@ -20,7 +20,7 @@ import { namespace_type, tags, } from '../common/schemas'; -import { Identity, RequiredKeepUndefined } from '../../types'; +import { RequiredKeepUndefined } from '../../types'; import { DefaultUuid } from '../../siem_common_deps'; import { NamespaceType } from '../types'; @@ -43,17 +43,15 @@ export const createExceptionListSchema = t.intersection([ ), ]); -export type CreateExceptionListSchemaPartial = Identity>; -export type CreateExceptionListSchema = RequiredKeepUndefined< - t.TypeOf ->; +export type CreateExceptionListSchema = t.OutputOf; // This type is used after a decode since some things are defaults after a decode. -export type CreateExceptionListSchemaDecoded = Identity< - Omit & { - _tags: _Tags; - tags: Tags; - list_id: ListId; - namespace_type: NamespaceType; - } ->; +export type CreateExceptionListSchemaDecoded = Omit< + RequiredKeepUndefined>, + '_tags' | 'tags' | 'list_id' | 'namespace_type' +> & { + _tags: _Tags; + tags: Tags; + list_id: ListId; + namespace_type: NamespaceType; +}; diff --git a/x-pack/plugins/lists/common/schemas/request/create_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/create_list_item_schema.ts index 6d16f2074864c..351eae48a638d 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_list_item_schema.ts @@ -9,7 +9,7 @@ import * as t from 'io-ts'; import { id, list_id, meta, value } from '../common/schemas'; -import { Identity, RequiredKeepUndefined } from '../../types'; +import { RequiredKeepUndefined } from '../../types'; export const createListItemSchema = t.intersection([ t.exact( @@ -21,5 +21,7 @@ export const createListItemSchema = t.intersection([ t.exact(t.partial({ id, meta })), ]); -export type CreateListItemSchemaPartial = Identity>; -export type CreateListItemSchema = RequiredKeepUndefined>; +export type CreateListItemSchema = t.OutputOf; +export type CreateListItemSchemaDecoded = RequiredKeepUndefined< + t.TypeOf +>; diff --git a/x-pack/plugins/lists/common/schemas/request/create_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/create_list_schema.ts index fcf4465f88c8d..38d6167ea63f3 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_list_schema.ts @@ -7,7 +7,7 @@ import * as t from 'io-ts'; import { description, deserializer, id, meta, name, serializer, type } from '../common/schemas'; -import { Identity, RequiredKeepUndefined } from '../../types'; +import { RequiredKeepUndefined } from '../../types'; export const createListSchema = t.intersection([ t.exact( @@ -20,5 +20,5 @@ export const createListSchema = t.intersection([ t.exact(t.partial({ deserializer, id, meta, serializer })), ]); -export type CreateListSchemaPartial = Identity>; -export type CreateListSchema = RequiredKeepUndefined>; +export type CreateListSchema = t.OutputOf; +export type CreateListSchemaDecoded = RequiredKeepUndefined>; diff --git a/x-pack/plugins/lists/common/schemas/request/delete_endpoint_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/delete_endpoint_list_item_schema.ts index 311af3a4c0437..5af5bcd17e744 100644 --- a/x-pack/plugins/lists/common/schemas/request/delete_endpoint_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/delete_endpoint_list_item_schema.ts @@ -9,6 +9,7 @@ import * as t from 'io-ts'; import { id, item_id } from '../common/schemas'; +import { RequiredKeepUndefined } from '../../types'; export const deleteEndpointListItemSchema = t.exact( t.partial({ @@ -17,7 +18,9 @@ export const deleteEndpointListItemSchema = t.exact( }) ); -export type DeleteEndpointListItemSchema = t.TypeOf; +export type DeleteEndpointListItemSchema = t.OutputOf; // This type is used after a decode since some things are defaults after a decode. -export type DeleteEndpointListItemSchemaDecoded = DeleteEndpointListItemSchema; +export type DeleteEndpointListItemSchemaDecoded = RequiredKeepUndefined< + t.TypeOf +>; diff --git a/x-pack/plugins/lists/common/schemas/request/delete_exception_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/delete_exception_list_item_schema.ts index 909960c9fffc0..da6516f4b6fe4 100644 --- a/x-pack/plugins/lists/common/schemas/request/delete_exception_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/delete_exception_list_item_schema.ts @@ -10,6 +10,7 @@ import * as t from 'io-ts'; import { id, item_id, namespace_type } from '../common/schemas'; import { NamespaceType } from '../types'; +import { RequiredKeepUndefined } from '../../types'; export const deleteExceptionListItemSchema = t.exact( t.partial({ @@ -19,11 +20,11 @@ export const deleteExceptionListItemSchema = t.exact( }) ); -export type DeleteExceptionListItemSchema = t.TypeOf; +export type DeleteExceptionListItemSchema = t.OutputOf; // This type is used after a decode since some things are defaults after a decode. export type DeleteExceptionListItemSchemaDecoded = Omit< - DeleteExceptionListItemSchema, + RequiredKeepUndefined>, 'namespace_type' > & { namespace_type: NamespaceType; diff --git a/x-pack/plugins/lists/common/schemas/request/delete_exception_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/delete_exception_list_schema.ts index 3bf5e7a4d0782..0911a9342f7a9 100644 --- a/x-pack/plugins/lists/common/schemas/request/delete_exception_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/delete_exception_list_schema.ts @@ -10,6 +10,7 @@ import * as t from 'io-ts'; import { id, list_id, namespace_type } from '../common/schemas'; import { NamespaceType } from '../types'; +import { RequiredKeepUndefined } from '../../types'; export const deleteExceptionListSchema = t.exact( t.partial({ @@ -19,9 +20,12 @@ export const deleteExceptionListSchema = t.exact( }) ); -export type DeleteExceptionListSchema = t.TypeOf; +export type DeleteExceptionListSchema = t.OutputOf; // This type is used after a decode since some things are defaults after a decode. -export type DeleteExceptionListSchemaDecoded = Omit & { +export type DeleteExceptionListSchemaDecoded = Omit< + RequiredKeepUndefined>, + 'namespace_type' +> & { namespace_type: NamespaceType; }; diff --git a/x-pack/plugins/lists/common/schemas/request/delete_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/delete_list_item_schema.ts index 91887395e747d..5e2425271c463 100644 --- a/x-pack/plugins/lists/common/schemas/request/delete_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/delete_list_item_schema.ts @@ -9,7 +9,7 @@ import * as t from 'io-ts'; import { id, list_id, valueOrUndefined } from '../common/schemas'; -import { Identity, RequiredKeepUndefined } from '../../types'; +import { RequiredKeepUndefined } from '../../types'; export const deleteListItemSchema = t.intersection([ t.exact( @@ -20,5 +20,7 @@ export const deleteListItemSchema = t.intersection([ t.exact(t.partial({ id, list_id })), ]); -export type DeleteListItemSchemaPartial = Identity>; -export type DeleteListItemSchema = RequiredKeepUndefined>; +export type DeleteListItemSchema = t.OutputOf; +export type DeleteListItemSchemaDecoded = RequiredKeepUndefined< + t.TypeOf +>; diff --git a/x-pack/plugins/lists/common/schemas/request/delete_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/delete_list_schema.ts index 6f6fc7a9ea33c..830e7fe695d1d 100644 --- a/x-pack/plugins/lists/common/schemas/request/delete_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/delete_list_schema.ts @@ -9,6 +9,7 @@ import * as t from 'io-ts'; import { id } from '../common/schemas'; +import { RequiredKeepUndefined } from '../../types'; export const deleteListSchema = t.exact( t.type({ @@ -16,5 +17,5 @@ export const deleteListSchema = t.exact( }) ); -export type DeleteListSchema = t.TypeOf; +export type DeleteListSchema = RequiredKeepUndefined>; export type DeleteListSchemaEncoded = t.OutputOf; diff --git a/x-pack/plugins/lists/common/schemas/request/export_list_item_query_schema.ts b/x-pack/plugins/lists/common/schemas/request/export_list_item_query_schema.ts index 58092ffc563b1..8d14f015d3805 100644 --- a/x-pack/plugins/lists/common/schemas/request/export_list_item_query_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/export_list_item_query_schema.ts @@ -9,6 +9,7 @@ import * as t from 'io-ts'; import { list_id } from '../common/schemas'; +import { RequiredKeepUndefined } from '../../types'; export const exportListItemQuerySchema = t.exact( t.type({ @@ -17,5 +18,7 @@ export const exportListItemQuerySchema = t.exact( }) ); -export type ExportListItemQuerySchema = t.TypeOf; +export type ExportListItemQuerySchema = RequiredKeepUndefined< + t.TypeOf +>; export type ExportListItemQuerySchemaEncoded = t.OutputOf; diff --git a/x-pack/plugins/lists/common/schemas/request/find_endpoint_list_item_schema.mock.ts b/x-pack/plugins/lists/common/schemas/request/find_endpoint_list_item_schema.mock.ts index bff55dedf3064..469936eae96c9 100644 --- a/x-pack/plugins/lists/common/schemas/request/find_endpoint_list_item_schema.mock.ts +++ b/x-pack/plugins/lists/common/schemas/request/find_endpoint_list_item_schema.mock.ts @@ -7,11 +7,11 @@ import { FILTER } from '../../constants.mock'; import { - FindEndpointListItemSchemaPartial, - FindEndpointListItemSchemaPartialDecoded, + FindEndpointListItemSchema, + FindEndpointListItemSchemaDecoded, } from './find_endpoint_list_item_schema'; -export const getFindEndpointListItemSchemaMock = (): FindEndpointListItemSchemaPartial => ({ +export const getFindEndpointListItemSchemaMock = (): FindEndpointListItemSchema => ({ filter: FILTER, page: '1', per_page: '25', @@ -19,7 +19,7 @@ export const getFindEndpointListItemSchemaMock = (): FindEndpointListItemSchemaP sort_order: undefined, }); -export const getFindEndpointListItemSchemaDecodedMock = (): FindEndpointListItemSchemaPartialDecoded => ({ +export const getFindEndpointListItemSchemaDecodedMock = (): FindEndpointListItemSchemaDecoded => ({ filter: FILTER, page: 1, per_page: 25, diff --git a/x-pack/plugins/lists/common/schemas/request/find_endpoint_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/find_endpoint_list_item_schema.test.ts index f9eeaa33230f9..8249b1e2d49c2 100644 --- a/x-pack/plugins/lists/common/schemas/request/find_endpoint_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/find_endpoint_list_item_schema.test.ts @@ -14,7 +14,7 @@ import { getFindEndpointListItemSchemaMock, } from './find_endpoint_list_item_schema.mock'; import { - FindEndpointListItemSchemaPartial, + FindEndpointListItemSchema, findEndpointListItemSchema, } from './find_endpoint_list_item_schema'; @@ -29,7 +29,7 @@ describe('find_endpoint_list_item_schema', () => { }); test('it should validate and empty object since everything is optional and has defaults', () => { - const payload: FindEndpointListItemSchemaPartial = {}; + const payload: FindEndpointListItemSchema = {}; const decoded = findEndpointListItemSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); @@ -98,7 +98,7 @@ describe('find_endpoint_list_item_schema', () => { }); test('it should not allow an extra key to be sent in', () => { - const payload: FindEndpointListItemSchemaPartial & { + const payload: FindEndpointListItemSchema & { extraKey: string; } = { ...getFindEndpointListItemSchemaMock(), extraKey: 'some new value' }; const decoded = findEndpointListItemSchema.decode(payload); diff --git a/x-pack/plugins/lists/common/schemas/request/find_endpoint_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/find_endpoint_list_item_schema.ts index c9ee46994d720..bc839ce1346f3 100644 --- a/x-pack/plugins/lists/common/schemas/request/find_endpoint_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/find_endpoint_list_item_schema.ts @@ -22,16 +22,9 @@ export const findEndpointListItemSchema = t.exact( }) ); -export type FindEndpointListItemSchemaPartial = t.OutputOf; - -// This type is used after a decode since some things are defaults after a decode. -export type FindEndpointListItemSchemaPartialDecoded = t.TypeOf; +export type FindEndpointListItemSchema = t.OutputOf; // This type is used after a decode since some things are defaults after a decode. export type FindEndpointListItemSchemaDecoded = RequiredKeepUndefined< - FindEndpointListItemSchemaPartialDecoded ->; - -export type FindEndpointListItemSchema = RequiredKeepUndefined< t.TypeOf >; diff --git a/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.mock.ts b/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.mock.ts index f22e6685fe0ac..d2733531eada4 100644 --- a/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.mock.ts +++ b/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.mock.ts @@ -7,11 +7,11 @@ import { FILTER, LIST_ID, NAMESPACE_TYPE } from '../../constants.mock'; import { - FindExceptionListItemSchemaPartial, - FindExceptionListItemSchemaPartialDecoded, + FindExceptionListItemSchema, + FindExceptionListItemSchemaDecoded, } from './find_exception_list_item_schema'; -export const getFindExceptionListItemSchemaMock = (): FindExceptionListItemSchemaPartial => ({ +export const getFindExceptionListItemSchemaMock = (): FindExceptionListItemSchema => ({ filter: FILTER, list_id: LIST_ID, namespace_type: NAMESPACE_TYPE, @@ -21,7 +21,7 @@ export const getFindExceptionListItemSchemaMock = (): FindExceptionListItemSchem sort_order: undefined, }); -export const getFindExceptionListItemSchemaMultipleMock = (): FindExceptionListItemSchemaPartial => ({ +export const getFindExceptionListItemSchemaMultipleMock = (): FindExceptionListItemSchema => ({ filter: 'name:Sofia Kovalevskaya,name:Hypatia,name:Sophie Germain', list_id: 'list-1,list-2,list-3', namespace_type: 'single,single,agnostic', @@ -31,7 +31,7 @@ export const getFindExceptionListItemSchemaMultipleMock = (): FindExceptionListI sort_order: undefined, }); -export const getFindExceptionListItemSchemaDecodedMock = (): FindExceptionListItemSchemaPartialDecoded => ({ +export const getFindExceptionListItemSchemaDecodedMock = (): FindExceptionListItemSchemaDecoded => ({ filter: [FILTER], list_id: [LIST_ID], namespace_type: [NAMESPACE_TYPE], @@ -41,7 +41,7 @@ export const getFindExceptionListItemSchemaDecodedMock = (): FindExceptionListIt sort_order: undefined, }); -export const getFindExceptionListItemSchemaDecodedMultipleMock = (): FindExceptionListItemSchemaPartialDecoded => ({ +export const getFindExceptionListItemSchemaDecodedMultipleMock = (): FindExceptionListItemSchemaDecoded => ({ filter: ['name:Sofia Kovalevskaya', 'name:Hypatia', 'name:Sophie Germain'], list_id: ['list-1', 'list-2', 'list-3'], namespace_type: ['single', 'single', 'agnostic'], diff --git a/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.test.ts index ba64bb434d50b..f402f22b093ad 100644 --- a/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.test.ts @@ -17,8 +17,8 @@ import { getFindExceptionListItemSchemaMultipleMock, } from './find_exception_list_item_schema.mock'; import { - FindExceptionListItemSchemaPartial, - FindExceptionListItemSchemaPartialDecoded, + FindExceptionListItemSchema, + FindExceptionListItemSchemaDecoded, findExceptionListItemSchema, } from './find_exception_list_item_schema'; @@ -42,15 +42,19 @@ describe('find_list_item_schema', () => { }); test('it should validate just a list_id where it decodes into an array for list_id and adds a namespace_type of "single" as an array', () => { - const payload: FindExceptionListItemSchemaPartial = { list_id: LIST_ID }; + const payload: FindExceptionListItemSchema = { list_id: LIST_ID }; const decoded = findExceptionListItemSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); - const expected: FindExceptionListItemSchemaPartialDecoded = { + const expected: FindExceptionListItemSchemaDecoded = { filter: [], list_id: [LIST_ID], namespace_type: ['single'], + page: undefined, + per_page: undefined, + sort_field: undefined, + sort_order: undefined, }; expect(message.schema).toEqual(expected); }); @@ -86,7 +90,7 @@ describe('find_list_item_schema', () => { const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); - const expected: FindExceptionListItemSchemaPartialDecoded = { + const expected: FindExceptionListItemSchemaDecoded = { ...getFindExceptionListItemSchemaDecodedMock(), filter: [], }; @@ -118,7 +122,7 @@ describe('find_list_item_schema', () => { }); test('it should not allow an extra key to be sent in', () => { - const payload: FindExceptionListItemSchemaPartial & { + const payload: FindExceptionListItemSchema & { extraKey: string; } = { ...getFindExceptionListItemSchemaMock(), extraKey: 'some new value' }; const decoded = findExceptionListItemSchema.decode(payload); diff --git a/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.ts index aa53fa0fd912c..634c080d70b75 100644 --- a/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.ts @@ -36,22 +36,13 @@ export const findExceptionListItemSchema = t.intersection([ ), ]); -export type FindExceptionListItemSchemaPartial = t.OutputOf; +export type FindExceptionListItemSchema = t.OutputOf; // This type is used after a decode since some things are defaults after a decode. -export type FindExceptionListItemSchemaPartialDecoded = Omit< - t.TypeOf, +export type FindExceptionListItemSchemaDecoded = Omit< + RequiredKeepUndefined>, 'namespace_type' | 'filter' > & { filter: EmptyStringArrayDecoded; namespace_type: DefaultNamespaceArrayTypeDecoded; }; - -// This type is used after a decode since some things are defaults after a decode. -export type FindExceptionListItemSchemaDecoded = RequiredKeepUndefined< - FindExceptionListItemSchemaPartialDecoded ->; - -export type FindExceptionListItemSchema = RequiredKeepUndefined< - t.TypeOf ->; diff --git a/x-pack/plugins/lists/common/schemas/request/find_exception_list_schema.mock.ts b/x-pack/plugins/lists/common/schemas/request/find_exception_list_schema.mock.ts index 8080d10ca451c..96f4b7e1cbd63 100644 --- a/x-pack/plugins/lists/common/schemas/request/find_exception_list_schema.mock.ts +++ b/x-pack/plugins/lists/common/schemas/request/find_exception_list_schema.mock.ts @@ -7,11 +7,11 @@ import { FILTER, NAMESPACE_TYPE } from '../../constants.mock'; import { - FindExceptionListSchemaPartial, - FindExceptionListSchemaPartialDecoded, + FindExceptionListSchema, + FindExceptionListSchemaDecoded, } from './find_exception_list_schema'; -export const getFindExceptionListSchemaMock = (): FindExceptionListSchemaPartial => ({ +export const getFindExceptionListSchemaMock = (): FindExceptionListSchema => ({ filter: FILTER, namespace_type: NAMESPACE_TYPE, page: '1', @@ -20,7 +20,7 @@ export const getFindExceptionListSchemaMock = (): FindExceptionListSchemaPartial sort_order: undefined, }); -export const getFindExceptionListSchemaDecodedMock = (): FindExceptionListSchemaPartialDecoded => ({ +export const getFindExceptionListSchemaDecodedMock = (): FindExceptionListSchemaDecoded => ({ filter: FILTER, namespace_type: NAMESPACE_TYPE, page: 1, diff --git a/x-pack/plugins/lists/common/schemas/request/find_exception_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/find_exception_list_schema.test.ts index 42356066176d5..ef96346c732b8 100644 --- a/x-pack/plugins/lists/common/schemas/request/find_exception_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/find_exception_list_schema.test.ts @@ -14,8 +14,8 @@ import { getFindExceptionListSchemaMock, } from './find_exception_list_schema.mock'; import { - FindExceptionListSchemaPartial, - FindExceptionListSchemaPartialDecoded, + FindExceptionListSchema, + FindExceptionListSchemaDecoded, findExceptionListSchema, } from './find_exception_list_schema'; @@ -30,13 +30,18 @@ describe('find_exception_list_schema', () => { }); test('it should validate and empty object since everything is optional and will respond only with namespace_type filled out to be "single"', () => { - const payload: FindExceptionListSchemaPartial = {}; + const payload: FindExceptionListSchema = {}; const decoded = findExceptionListSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); - const expected: FindExceptionListSchemaPartialDecoded = { + const expected: FindExceptionListSchemaDecoded = { + filter: undefined, namespace_type: 'single', + page: undefined, + per_page: undefined, + sort_field: undefined, + sort_order: undefined, }; expect(message.schema).toEqual(expected); }); @@ -102,7 +107,7 @@ describe('find_exception_list_schema', () => { }); test('it should not allow an extra key to be sent in', () => { - const payload: FindExceptionListSchemaPartial & { + const payload: FindExceptionListSchema & { extraKey: string; } = { ...getFindExceptionListSchemaMock(), extraKey: 'some new value' }; const decoded = findExceptionListSchema.decode(payload); diff --git a/x-pack/plugins/lists/common/schemas/request/find_exception_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/find_exception_list_schema.ts index 4fa9d2e42c5d1..7ce01c79bbe42 100644 --- a/x-pack/plugins/lists/common/schemas/request/find_exception_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/find_exception_list_schema.ts @@ -24,21 +24,12 @@ export const findExceptionListSchema = t.exact( }) ); -export type FindExceptionListSchemaPartial = t.OutputOf; +export type FindExceptionListSchema = t.OutputOf; // This type is used after a decode since some things are defaults after a decode. -export type FindExceptionListSchemaPartialDecoded = Omit< - t.TypeOf, +export type FindExceptionListSchemaDecoded = Omit< + RequiredKeepUndefined>, 'namespace_type' > & { namespace_type: NamespaceType; }; - -// This type is used after a decode since some things are defaults after a decode. -export type FindExceptionListSchemaDecoded = RequiredKeepUndefined< - FindExceptionListSchemaPartialDecoded ->; - -export type FindExceptionListSchema = RequiredKeepUndefined< - t.TypeOf ->; diff --git a/x-pack/plugins/lists/common/schemas/request/find_list_item_schema.mock.ts b/x-pack/plugins/lists/common/schemas/request/find_list_item_schema.mock.ts index a1e91f6acd264..ccde93eec4580 100644 --- a/x-pack/plugins/lists/common/schemas/request/find_list_item_schema.mock.ts +++ b/x-pack/plugins/lists/common/schemas/request/find_list_item_schema.mock.ts @@ -6,12 +6,9 @@ import { CURSOR, FILTER, LIST_ID } from '../../constants.mock'; -import { - FindListItemSchemaPartial, - FindListItemSchemaPartialDecoded, -} from './find_list_item_schema'; +import { FindListItemSchema, FindListItemSchemaDecoded } from './find_list_item_schema'; -export const getFindListItemSchemaMock = (): FindListItemSchemaPartial => ({ +export const getFindListItemSchemaMock = (): FindListItemSchema => ({ cursor: CURSOR, filter: FILTER, list_id: LIST_ID, @@ -21,7 +18,7 @@ export const getFindListItemSchemaMock = (): FindListItemSchemaPartial => ({ sort_order: undefined, }); -export const getFindListItemSchemaDecodedMock = (): FindListItemSchemaPartialDecoded => ({ +export const getFindListItemSchemaDecodedMock = (): FindListItemSchemaDecoded => ({ cursor: CURSOR, filter: FILTER, list_id: LIST_ID, diff --git a/x-pack/plugins/lists/common/schemas/request/find_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/find_list_item_schema.test.ts index 42803fffc53c2..59d4b4485b578 100644 --- a/x-pack/plugins/lists/common/schemas/request/find_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/find_list_item_schema.test.ts @@ -15,8 +15,8 @@ import { getFindListItemSchemaMock, } from './find_list_item_schema.mock'; import { - FindListItemSchemaPartial, - FindListItemSchemaPartialDecoded, + FindListItemSchema, + FindListItemSchemaDecoded, findListItemSchema, } from './find_list_item_schema'; @@ -31,12 +31,12 @@ describe('find_list_item_schema', () => { }); test('it should validate just a list_id where it decodes into an array for list_id and adds a namespace_type of "single"', () => { - const payload: FindListItemSchemaPartial = { list_id: LIST_ID }; + const payload: FindListItemSchema = { list_id: LIST_ID }; const decoded = findListItemSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); - const expected: FindListItemSchemaPartialDecoded = { + const expected: FindListItemSchemaDecoded = { cursor: undefined, filter: undefined, list_id: LIST_ID, @@ -97,7 +97,7 @@ describe('find_list_item_schema', () => { }); test('it should not allow an extra key to be sent in', () => { - const payload: FindListItemSchemaPartial & { + const payload: FindListItemSchema & { extraKey: string; } = { ...getFindListItemSchemaMock(), extraKey: 'some new value' }; const decoded = findListItemSchema.decode(payload); diff --git a/x-pack/plugins/lists/common/schemas/request/find_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/find_list_item_schema.ts index bbd3c7b5ec642..ba3dfc6ee33ec 100644 --- a/x-pack/plugins/lists/common/schemas/request/find_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/find_list_item_schema.ts @@ -9,7 +9,7 @@ import * as t from 'io-ts'; import { cursor, filter, list_id, sort_field, sort_order } from '../common/schemas'; -import { Identity, RequiredKeepUndefined } from '../../types'; +import { RequiredKeepUndefined } from '../../types'; import { StringToPositiveNumber } from '../types/string_to_positive_number'; export const findListItemSchema = t.intersection([ @@ -26,9 +26,7 @@ export const findListItemSchema = t.intersection([ ), ]); -export type FindListItemSchemaPartial = Identity>; +export type FindListItemSchema = t.OutputOf; // This type is used after a decode since some things are defaults after a decode. -export type FindListItemSchemaPartialDecoded = RequiredKeepUndefined< - t.TypeOf ->; +export type FindListItemSchemaDecoded = RequiredKeepUndefined>; diff --git a/x-pack/plugins/lists/common/schemas/request/find_list_schema.mock.ts b/x-pack/plugins/lists/common/schemas/request/find_list_schema.mock.ts index dcb18dac8cfb6..bb9e15a439b3b 100644 --- a/x-pack/plugins/lists/common/schemas/request/find_list_schema.mock.ts +++ b/x-pack/plugins/lists/common/schemas/request/find_list_schema.mock.ts @@ -17,6 +17,7 @@ export const getFindListSchemaMock = (): FindListSchemaEncoded => ({ }); export const getFindListSchemaDecodedMock = (): FindListSchema => ({ + cursor: undefined, filter: FILTER, page: 1, per_page: 25, diff --git a/x-pack/plugins/lists/common/schemas/request/find_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/find_list_schema.test.ts index a343fb4b08bfc..63f29a64b4bf9 100644 --- a/x-pack/plugins/lists/common/schemas/request/find_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/find_list_schema.test.ts @@ -10,7 +10,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; import { getFindListSchemaDecodedMock, getFindListSchemaMock } from './find_list_schema.mock'; -import { FindListSchema, FindListSchemaEncoded, findListSchema } from './find_list_schema'; +import { FindListSchemaEncoded, findListSchema } from './find_list_schema'; describe('find_list_schema', () => { test('it should validate a typical find item request', () => { @@ -28,8 +28,7 @@ describe('find_list_schema', () => { const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); - const expected: FindListSchema = {}; - expect(message.schema).toEqual(expected); + expect(message.schema).toEqual(payload); }); test('it should validate with page missing', () => { diff --git a/x-pack/plugins/lists/common/schemas/request/find_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/find_list_schema.ts index 212232f6bc9c1..e5020cc8eff84 100644 --- a/x-pack/plugins/lists/common/schemas/request/find_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/find_list_schema.ts @@ -10,6 +10,7 @@ import * as t from 'io-ts'; import { cursor, filter, sort_field, sort_order } from '../common/schemas'; import { StringToPositiveNumber } from '../types/string_to_positive_number'; +import { RequiredKeepUndefined } from '../../types'; export const findListSchema = t.exact( t.partial({ @@ -22,5 +23,5 @@ export const findListSchema = t.exact( }) ); -export type FindListSchema = t.TypeOf; +export type FindListSchema = RequiredKeepUndefined>; export type FindListSchemaEncoded = t.OutputOf; diff --git a/x-pack/plugins/lists/common/schemas/request/import_list_item_query_schema.ts b/x-pack/plugins/lists/common/schemas/request/import_list_item_query_schema.ts index 2c671466702e0..e45f77ca18ae1 100644 --- a/x-pack/plugins/lists/common/schemas/request/import_list_item_query_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/import_list_item_query_schema.ts @@ -8,14 +8,14 @@ import * as t from 'io-ts'; +import { RequiredKeepUndefined } from '../../types'; import { deserializer, list_id, serializer, type } from '../common/schemas'; -import { Identity } from '../../types'; export const importListItemQuerySchema = t.exact( t.partial({ deserializer, list_id, serializer, type }) ); -export type ImportListItemQuerySchemaPartial = Identity>; - -export type ImportListItemQuerySchema = t.TypeOf; +export type ImportListItemQuerySchema = RequiredKeepUndefined< + t.TypeOf +>; export type ImportListItemQuerySchemaEncoded = t.OutputOf; diff --git a/x-pack/plugins/lists/common/schemas/request/import_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/import_list_item_schema.ts index 7370eecf690c7..671aeda757eff 100644 --- a/x-pack/plugins/lists/common/schemas/request/import_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/import_list_item_schema.ts @@ -9,6 +9,7 @@ import * as t from 'io-ts'; import { file } from '../common/schemas'; +import { RequiredKeepUndefined } from '../../types'; export const importListItemSchema = t.exact( t.type({ @@ -16,5 +17,5 @@ export const importListItemSchema = t.exact( }) ); -export type ImportListItemSchema = t.TypeOf; +export type ImportListItemSchema = RequiredKeepUndefined>; export type ImportListItemSchemaEncoded = t.OutputOf; diff --git a/x-pack/plugins/lists/common/schemas/request/patch_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/patch_list_item_schema.ts index 2016069f32fb3..9c5284c15ca99 100644 --- a/x-pack/plugins/lists/common/schemas/request/patch_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/patch_list_item_schema.ts @@ -8,8 +8,8 @@ import * as t from 'io-ts'; -import { id, meta, value } from '../common/schemas'; -import { Identity, RequiredKeepUndefined } from '../../types'; +import { _version, id, meta, value } from '../common/schemas'; +import { RequiredKeepUndefined } from '../../types'; export const patchListItemSchema = t.intersection([ t.exact( @@ -17,8 +17,10 @@ export const patchListItemSchema = t.intersection([ id, }) ), - t.exact(t.partial({ meta, value })), + t.exact(t.partial({ _version, meta, value })), ]); -export type PatchListItemSchemaPartial = Identity>; -export type PatchListItemSchema = RequiredKeepUndefined>; +export type PatchListItemSchema = t.OutputOf; +export type PatchListItemSchemaDecoded = RequiredKeepUndefined< + t.TypeOf +>; diff --git a/x-pack/plugins/lists/common/schemas/request/patch_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/patch_list_schema.ts index 653a42dc91653..e0cd1571afc81 100644 --- a/x-pack/plugins/lists/common/schemas/request/patch_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/patch_list_schema.ts @@ -8,8 +8,8 @@ import * as t from 'io-ts'; -import { description, id, meta, name } from '../common/schemas'; -import { Identity, RequiredKeepUndefined } from '../../types'; +import { _version, description, id, meta, name } from '../common/schemas'; +import { RequiredKeepUndefined } from '../../types'; export const patchListSchema = t.intersection([ t.exact( @@ -17,8 +17,8 @@ export const patchListSchema = t.intersection([ id, }) ), - t.exact(t.partial({ description, meta, name })), + t.exact(t.partial({ _version, description, meta, name })), ]); -export type PatchListSchemaPartial = Identity>; -export type PatchListSchema = RequiredKeepUndefined>>; +export type PatchListSchema = t.OutputOf; +export type PatchListSchemaDecoded = RequiredKeepUndefined>; diff --git a/x-pack/plugins/lists/common/schemas/request/read_endpoint_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/read_endpoint_list_item_schema.ts index 22750f5db6a1d..d6c54e289effe 100644 --- a/x-pack/plugins/lists/common/schemas/request/read_endpoint_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/read_endpoint_list_item_schema.ts @@ -18,14 +18,9 @@ export const readEndpointListItemSchema = t.exact( }) ); -export type ReadEndpointListItemSchemaPartial = t.TypeOf; - -// This type is used after a decode since some things are defaults after a decode. -export type ReadEndpointListItemSchemaPartialDecoded = ReadEndpointListItemSchemaPartial; +export type ReadEndpointListItemSchema = t.OutputOf; // This type is used after a decode since some things are defaults after a decode. export type ReadEndpointListItemSchemaDecoded = RequiredKeepUndefined< - ReadEndpointListItemSchemaPartialDecoded + t.TypeOf >; - -export type ReadEndpointListItemSchema = RequiredKeepUndefined; diff --git a/x-pack/plugins/lists/common/schemas/request/read_exception_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/read_exception_list_item_schema.ts index d8864a6fc66e5..a2ba8126c7788 100644 --- a/x-pack/plugins/lists/common/schemas/request/read_exception_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/read_exception_list_item_schema.ts @@ -20,19 +20,12 @@ export const readExceptionListItemSchema = t.exact( }) ); -export type ReadExceptionListItemSchemaPartial = t.TypeOf; +export type ReadExceptionListItemSchema = t.OutputOf; // This type is used after a decode since some things are defaults after a decode. -export type ReadExceptionListItemSchemaPartialDecoded = Omit< - ReadExceptionListItemSchemaPartial, +export type ReadExceptionListItemSchemaDecoded = Omit< + RequiredKeepUndefined>, 'namespace_type' > & { namespace_type: NamespaceType; }; - -// This type is used after a decode since some things are defaults after a decode. -export type ReadExceptionListItemSchemaDecoded = RequiredKeepUndefined< - ReadExceptionListItemSchemaPartialDecoded ->; - -export type ReadExceptionListItemSchema = RequiredKeepUndefined; diff --git a/x-pack/plugins/lists/common/schemas/request/read_exception_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/read_exception_list_schema.ts index 613fb22a99d61..f22eca6a8ab15 100644 --- a/x-pack/plugins/lists/common/schemas/request/read_exception_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/read_exception_list_schema.ts @@ -20,19 +20,12 @@ export const readExceptionListSchema = t.exact( }) ); -export type ReadExceptionListSchemaPartial = t.TypeOf; +export type ReadExceptionListSchema = t.OutputOf; // This type is used after a decode since some things are defaults after a decode. -export type ReadExceptionListSchemaPartialDecoded = Omit< - ReadExceptionListSchemaPartial, +export type ReadExceptionListSchemaDecoded = Omit< + RequiredKeepUndefined>, 'namespace_type' > & { namespace_type: NamespaceType; }; - -// This type is used after a decode since some things are defaults after a decode. -export type ReadExceptionListSchemaDecoded = RequiredKeepUndefined< - ReadExceptionListSchemaPartialDecoded ->; - -export type ReadExceptionListSchema = RequiredKeepUndefined; diff --git a/x-pack/plugins/lists/common/schemas/request/read_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/read_list_item_schema.ts index 394c1f1e2289a..063f430aa9cea 100644 --- a/x-pack/plugins/lists/common/schemas/request/read_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/read_list_item_schema.ts @@ -9,9 +9,9 @@ import * as t from 'io-ts'; import { id, list_id, value } from '../common/schemas'; -import { Identity, RequiredKeepUndefined } from '../../types'; +import { RequiredKeepUndefined } from '../../types'; export const readListItemSchema = t.exact(t.partial({ id, list_id, value })); -export type ReadListItemSchemaPartial = Identity>; -export type ReadListItemSchema = RequiredKeepUndefined>; +export type ReadListItemSchema = t.OutputOf; +export type ReadListItemSchemaDecoded = RequiredKeepUndefined>; diff --git a/x-pack/plugins/lists/common/schemas/request/read_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/read_list_schema.ts index 8803346709c31..e395875462cb4 100644 --- a/x-pack/plugins/lists/common/schemas/request/read_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/read_list_schema.ts @@ -16,4 +16,4 @@ export const readListSchema = t.exact( }) ); -export type ReadListSchema = t.TypeOf; +export type ReadListSchema = t.OutputOf; diff --git a/x-pack/plugins/lists/common/schemas/request/update_endpoint_list_item_schema.mock.ts b/x-pack/plugins/lists/common/schemas/request/update_endpoint_list_item_schema.mock.ts index 30bbbe2d22ea4..0847389dac922 100644 --- a/x-pack/plugins/lists/common/schemas/request/update_endpoint_list_item_schema.mock.ts +++ b/x-pack/plugins/lists/common/schemas/request/update_endpoint_list_item_schema.mock.ts @@ -21,6 +21,7 @@ import { UpdateEndpointListItemSchema } from './update_endpoint_list_item_schema export const getUpdateEndpointListItemSchemaMock = (): UpdateEndpointListItemSchema => ({ _tags: _TAGS, + _version: undefined, comments: COMMENTS, description: DESCRIPTION, entries: ENTRIES, diff --git a/x-pack/plugins/lists/common/schemas/request/update_endpoint_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/update_endpoint_list_item_schema.ts index dbe38f6d468e2..4430aa98b8e3d 100644 --- a/x-pack/plugins/lists/common/schemas/request/update_endpoint_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/update_endpoint_list_item_schema.ts @@ -12,6 +12,7 @@ import { Tags, _Tags, _tags, + _version, description, exceptionListItemType, id, @@ -19,7 +20,7 @@ import { name, tags, } from '../common/schemas'; -import { Identity, RequiredKeepUndefined } from '../../types'; +import { RequiredKeepUndefined } from '../../types'; import { DefaultEntryArray, DefaultUpdateCommentsArray, @@ -38,6 +39,7 @@ export const updateEndpointListItemSchema = t.intersection([ t.exact( t.partial({ _tags, // defaults to empty array if not set during decode + _version, // defaults to undefined if not set during decode comments: DefaultUpdateCommentsArray, // defaults to empty array if not set during decode entries: DefaultEntryArray, // defaults to empty array if not set during decode id, // defaults to undefined if not set during decode @@ -48,19 +50,15 @@ export const updateEndpointListItemSchema = t.intersection([ ), ]); -export type UpdateEndpointListItemSchemaPartial = Identity< - t.TypeOf ->; -export type UpdateEndpointListItemSchema = RequiredKeepUndefined< - t.TypeOf ->; +export type UpdateEndpointListItemSchema = t.OutputOf; // This type is used after a decode since some things are defaults after a decode. -export type UpdateEndpointListItemSchemaDecoded = Identity< - Omit & { - _tags: _Tags; - comments: UpdateCommentsArray; - tags: Tags; - entries: EntriesArray; - } ->; +export type UpdateEndpointListItemSchemaDecoded = Omit< + RequiredKeepUndefined>, + '_tags' | 'tags' | 'entries' | 'comments' +> & { + _tags: _Tags; + comments: UpdateCommentsArray; + tags: Tags; + entries: EntriesArray; +}; diff --git a/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.mock.ts b/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.mock.ts index e8936f0bdc6d4..90d70c273f490 100644 --- a/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.mock.ts +++ b/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.mock.ts @@ -22,6 +22,7 @@ import { UpdateExceptionListItemSchema } from './update_exception_list_item_sche export const getUpdateExceptionListItemSchemaMock = (): UpdateExceptionListItemSchema => ({ _tags: _TAGS, + _version: undefined, comments: COMMENTS, description: DESCRIPTION, entries: ENTRIES, diff --git a/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.ts index 20a63e0fc7dac..9e0a1759fc9f4 100644 --- a/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.ts @@ -12,6 +12,7 @@ import { Tags, _Tags, _tags, + _version, description, exceptionListItemType, id, @@ -20,7 +21,7 @@ import { namespace_type, tags, } from '../common/schemas'; -import { Identity, RequiredKeepUndefined } from '../../types'; +import { RequiredKeepUndefined } from '../../types'; import { DefaultEntryArray, DefaultUpdateCommentsArray, @@ -40,6 +41,7 @@ export const updateExceptionListItemSchema = t.intersection([ t.exact( t.partial({ _tags, // defaults to empty array if not set during decode + _version, // defaults to undefined if not set during decode comments: DefaultUpdateCommentsArray, // defaults to empty array if not set during decode entries: DefaultEntryArray, // defaults to empty array if not set during decode id, // defaults to undefined if not set during decode @@ -51,23 +53,16 @@ export const updateExceptionListItemSchema = t.intersection([ ), ]); -export type UpdateExceptionListItemSchemaPartial = Identity< - t.TypeOf ->; -export type UpdateExceptionListItemSchema = RequiredKeepUndefined< - t.TypeOf ->; +export type UpdateExceptionListItemSchema = t.OutputOf; // This type is used after a decode since some things are defaults after a decode. -export type UpdateExceptionListItemSchemaDecoded = Identity< - Omit< - UpdateExceptionListItemSchema, - '_tags' | 'tags' | 'entries' | 'namespace_type' | 'comments' - > & { - _tags: _Tags; - comments: UpdateCommentsArray; - tags: Tags; - entries: EntriesArray; - namespace_type: NamespaceType; - } ->; +export type UpdateExceptionListItemSchemaDecoded = Omit< + RequiredKeepUndefined>, + '_tags' | 'tags' | 'entries' | 'namespace_type' | 'comments' +> & { + _tags: _Tags; + comments: UpdateCommentsArray; + tags: Tags; + entries: EntriesArray; + namespace_type: NamespaceType; +}; diff --git a/x-pack/plugins/lists/common/schemas/request/update_exception_list_schema.mock.ts b/x-pack/plugins/lists/common/schemas/request/update_exception_list_schema.mock.ts index 48a8baf9aea16..22af29e6af0b7 100644 --- a/x-pack/plugins/lists/common/schemas/request/update_exception_list_schema.mock.ts +++ b/x-pack/plugins/lists/common/schemas/request/update_exception_list_schema.mock.ts @@ -10,6 +10,7 @@ import { UpdateExceptionListSchema } from './update_exception_list_schema'; export const getUpdateExceptionListSchemaMock = (): UpdateExceptionListSchema => ({ _tags: _TAGS, + _version: undefined, description: DESCRIPTION, id: ID, list_id: LIST_ID, diff --git a/x-pack/plugins/lists/common/schemas/request/update_exception_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/update_exception_list_schema.ts index 0b5f3a8a01794..5d7294ae27af2 100644 --- a/x-pack/plugins/lists/common/schemas/request/update_exception_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/update_exception_list_schema.ts @@ -12,14 +12,17 @@ import { Tags, _Tags, _tags, + _version, description, exceptionListType, + id, + list_id, meta, name, namespace_type, tags, } from '../common/schemas'; -import { Identity, RequiredKeepUndefined } from '../../types'; +import { RequiredKeepUndefined } from '../../types'; import { NamespaceType } from '../types'; export const updateExceptionListSchema = t.intersection([ @@ -33,8 +36,9 @@ export const updateExceptionListSchema = t.intersection([ t.exact( t.partial({ _tags, // defaults to empty array if not set during decode - id: t.union([t.string, t.undefined]), // defaults to undefined if not set during decode - list_id: t.union([t.string, t.undefined]), // defaults to undefined if not set during decode + _version, // defaults to undefined if not set during decode + id, // defaults to undefined if not set during decode + list_id, // defaults to undefined if not set during decode meta, // defaults to undefined if not set during decode namespace_type, // defaults to 'single' if not set during decode tags, // defaults to empty array if not set during decode @@ -42,16 +46,14 @@ export const updateExceptionListSchema = t.intersection([ ), ]); -export type UpdateExceptionListSchemaPartial = Identity>; -export type UpdateExceptionListSchema = RequiredKeepUndefined< - t.TypeOf ->; +export type UpdateExceptionListSchema = t.OutputOf; // This type is used after a decode since the arrays turn into defaults of empty arrays. -export type UpdateExceptionListSchemaDecoded = Identity< - Omit & { - _tags: _Tags; - tags: Tags; - namespace_type: NamespaceType; - } ->; +export type UpdateExceptionListSchemaDecoded = Omit< + RequiredKeepUndefined>, + '_tags | tags | namespace_type' +> & { + _tags: _Tags; + tags: Tags; + namespace_type: NamespaceType; +}; diff --git a/x-pack/plugins/lists/common/schemas/request/update_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/update_list_item_schema.ts index 3a42cf28665f5..c6ed5ef0e9517 100644 --- a/x-pack/plugins/lists/common/schemas/request/update_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/update_list_item_schema.ts @@ -8,8 +8,8 @@ import * as t from 'io-ts'; -import { id, meta, value } from '../common/schemas'; -import { Identity, RequiredKeepUndefined } from '../../types'; +import { _version, id, meta, value } from '../common/schemas'; +import { RequiredKeepUndefined } from '../../types'; export const updateListItemSchema = t.intersection([ t.exact( @@ -20,10 +20,13 @@ export const updateListItemSchema = t.intersection([ ), t.exact( t.partial({ + _version, // defaults to undefined if not set during decode meta, // defaults to undefined if not set during decode }) ), ]); -export type UpdateListItemSchemaPartial = Identity>; -export type UpdateListItemSchema = RequiredKeepUndefined>; +export type UpdateListItemSchema = t.OutputOf; +export type UpdateListItemSchemaDecoded = RequiredKeepUndefined< + t.TypeOf +>; diff --git a/x-pack/plugins/lists/common/schemas/request/update_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/update_list_schema.ts index 4c5c8c429a14c..19a39d362c241 100644 --- a/x-pack/plugins/lists/common/schemas/request/update_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/update_list_schema.ts @@ -8,8 +8,8 @@ import * as t from 'io-ts'; -import { description, id, meta, name } from '../common/schemas'; -import { Identity, RequiredKeepUndefined } from '../../types'; +import { _version, description, id, meta, name } from '../common/schemas'; +import { RequiredKeepUndefined } from '../../types'; export const updateListSchema = t.intersection([ t.exact( @@ -21,10 +21,11 @@ export const updateListSchema = t.intersection([ ), t.exact( t.partial({ + _version, // defaults to undefined if not set during decode meta, // defaults to undefined if not set during decode }) ), ]); -export type UpdateListSchemaPartial = Identity>; -export type UpdateListSchema = RequiredKeepUndefined>; +export type UpdateListSchema = t.OutputOf; +export type UpdateListSchemaDecoded = RequiredKeepUndefined>; diff --git a/x-pack/plugins/lists/common/schemas/response/create_endpoint_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/response/create_endpoint_list_schema.test.ts index d346ea72ca310..646cc3d97f8ee 100644 --- a/x-pack/plugins/lists/common/schemas/response/create_endpoint_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/response/create_endpoint_list_schema.test.ts @@ -41,7 +41,7 @@ describe('create_endpoint_list_schema', () => { const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([ - 'invalid keys "_tags,["endpoint","process","malware","os:linux"],created_at,created_by,description,id,meta,{},name,namespace_type,tags,["user added string for a tag","malware"],tie_breaker_id,type,updated_at,updated_by"', + 'invalid keys "_tags,["endpoint","process","malware","os:linux"],_version,created_at,created_by,description,id,meta,{},name,namespace_type,tags,["user added string for a tag","malware"],tie_breaker_id,type,updated_at,updated_by"', ]); expect(message.schema).toEqual({}); }); diff --git a/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.mock.ts b/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.mock.ts index 9e1a88ceb28bd..c0d04c9823ca3 100644 --- a/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.mock.ts +++ b/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.mock.ts @@ -3,26 +3,38 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { ENTRIES } from '../../constants.mock'; +import { + COMMENTS, + DATE_NOW, + DESCRIPTION, + ENTRIES, + ITEM_TYPE, + META, + NAME, + NAMESPACE_TYPE, + TIE_BREAKER, + USER, +} from '../../constants.mock'; import { ExceptionListItemSchema } from './exception_list_item_schema'; export const getExceptionListItemSchemaMock = (): ExceptionListItemSchema => ({ _tags: ['endpoint', 'process', 'malware', 'os:linux'], - comments: [], - created_at: '2020-04-23T00:19:13.289Z', - created_by: 'user_name', - description: 'This is a sample endpoint type exception', + _version: undefined, + comments: COMMENTS, + created_at: DATE_NOW, + created_by: USER, + description: DESCRIPTION, entries: ENTRIES, id: '1', item_id: 'endpoint_list_item', list_id: 'endpoint_list_id', - meta: {}, - name: 'Sample Endpoint Exception List', - namespace_type: 'single', + meta: META, + name: NAME, + namespace_type: NAMESPACE_TYPE, tags: ['user added string for a tag', 'malware'], - tie_breaker_id: '77fd1909-6786-428a-a671-30229a719c1f', - type: 'simple', - updated_at: '2020-04-23T00:19:13.289Z', - updated_by: 'user_name', + tie_breaker_id: TIE_BREAKER, + type: ITEM_TYPE, + updated_at: DATE_NOW, + updated_by: USER, }); diff --git a/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.ts index c8440e9d3f3d0..54907f3f8a854 100644 --- a/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.ts @@ -10,6 +10,7 @@ import * as t from 'io-ts'; import { _tags, + _versionOrUndefined, created_at, created_by, description, @@ -30,6 +31,7 @@ import { commentsArray, entriesArray } from '../types'; export const exceptionListItemSchema = t.exact( t.type({ _tags, + _version: _versionOrUndefined, comments: commentsArray, created_at, created_by, diff --git a/x-pack/plugins/lists/common/schemas/response/exception_list_schema.mock.ts b/x-pack/plugins/lists/common/schemas/response/exception_list_schema.mock.ts index 906dcf6560ee5..f790ad9544d53 100644 --- a/x-pack/plugins/lists/common/schemas/response/exception_list_schema.mock.ts +++ b/x-pack/plugins/lists/common/schemas/response/exception_list_schema.mock.ts @@ -4,23 +4,32 @@ * you may not use this file except in compliance with the Elastic License. */ +import { + DATE_NOW, + DESCRIPTION, + ENDPOINT_TYPE, + META, + TIE_BREAKER, + USER, + _VERSION, +} from '../../constants.mock'; import { ENDPOINT_LIST_ID } from '../..'; import { ExceptionListSchema } from './exception_list_schema'; - export const getExceptionListSchemaMock = (): ExceptionListSchema => ({ _tags: ['endpoint', 'process', 'malware', 'os:linux'], - created_at: '2020-04-23T00:19:13.289Z', - created_by: 'user_name', - description: 'This is a sample endpoint type exception', + _version: _VERSION, + created_at: DATE_NOW, + created_by: USER, + description: DESCRIPTION, id: '1', list_id: ENDPOINT_LIST_ID, - meta: {}, + meta: META, name: 'Sample Endpoint Exception List', namespace_type: 'agnostic', tags: ['user added string for a tag', 'malware'], - tie_breaker_id: '77fd1909-6786-428a-a671-30229a719c1f', - type: 'endpoint', - updated_at: '2020-04-23T00:19:13.289Z', + tie_breaker_id: TIE_BREAKER, + type: ENDPOINT_TYPE, + updated_at: DATE_NOW, updated_by: 'user_name', }); diff --git a/x-pack/plugins/lists/common/schemas/response/exception_list_schema.ts b/x-pack/plugins/lists/common/schemas/response/exception_list_schema.ts index 0fb2bfca4a48f..11c23bc2ff354 100644 --- a/x-pack/plugins/lists/common/schemas/response/exception_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/response/exception_list_schema.ts @@ -10,6 +10,7 @@ import * as t from 'io-ts'; import { _tags, + _versionOrUndefined, created_at, created_by, description, @@ -28,6 +29,7 @@ import { export const exceptionListSchema = t.exact( t.type({ _tags, + _version: _versionOrUndefined, created_at, created_by, description, diff --git a/x-pack/plugins/lists/common/schemas/response/list_item_schema.mock.ts b/x-pack/plugins/lists/common/schemas/response/list_item_schema.mock.ts index 16e8057974917..e122f6a2bbe3b 100644 --- a/x-pack/plugins/lists/common/schemas/response/list_item_schema.mock.ts +++ b/x-pack/plugins/lists/common/schemas/response/list_item_schema.mock.ts @@ -17,6 +17,7 @@ import { } from '../../../common/constants.mock'; export const getListItemResponseMock = (): ListItemSchema => ({ + _version: undefined, created_at: DATE_NOW, created_by: USER, deserializer: undefined, diff --git a/x-pack/plugins/lists/common/schemas/response/list_item_schema.ts b/x-pack/plugins/lists/common/schemas/response/list_item_schema.ts index c2104aaf18b53..9ee801298f950 100644 --- a/x-pack/plugins/lists/common/schemas/response/list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/response/list_item_schema.ts @@ -9,6 +9,7 @@ import * as t from 'io-ts'; /* eslint-disable @typescript-eslint/camelcase */ import { + _versionOrUndefined, created_at, created_by, deserializerOrUndefined, @@ -25,6 +26,7 @@ import { export const listItemSchema = t.exact( t.type({ + _version: _versionOrUndefined, created_at, created_by, deserializer: deserializerOrUndefined, diff --git a/x-pack/plugins/lists/common/schemas/response/list_schema.mock.ts b/x-pack/plugins/lists/common/schemas/response/list_schema.mock.ts index c165c4ed8e745..339beddb00f8e 100644 --- a/x-pack/plugins/lists/common/schemas/response/list_schema.mock.ts +++ b/x-pack/plugins/lists/common/schemas/response/list_schema.mock.ts @@ -17,6 +17,7 @@ import { } from '../../../common/constants.mock'; export const getListResponseMock = (): ListSchema => ({ + _version: undefined, created_at: DATE_NOW, created_by: USER, description: DESCRIPTION, diff --git a/x-pack/plugins/lists/common/schemas/response/list_schema.ts b/x-pack/plugins/lists/common/schemas/response/list_schema.ts index 1950831bee694..7e2bc202a6520 100644 --- a/x-pack/plugins/lists/common/schemas/response/list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/response/list_schema.ts @@ -9,6 +9,7 @@ import * as t from 'io-ts'; import { + _versionOrUndefined, created_at, created_by, description, @@ -25,6 +26,7 @@ import { export const listSchema = t.exact( t.type({ + _version: _versionOrUndefined, created_at, created_by, description, diff --git a/x-pack/plugins/lists/common/schemas/types/default_comments_array.ts b/x-pack/plugins/lists/common/schemas/types/default_comments_array.ts index e8be299246ab8..342cf8b0d7091 100644 --- a/x-pack/plugins/lists/common/schemas/types/default_comments_array.ts +++ b/x-pack/plugins/lists/common/schemas/types/default_comments_array.ts @@ -9,17 +9,11 @@ import { Either } from 'fp-ts/lib/Either'; import { CommentsArray, comments } from './comments'; -export type DefaultCommentsArrayC = t.Type; - /** * Types the DefaultCommentsArray as: * - If null or undefined, then a default array of type entry will be set */ -export const DefaultCommentsArray: DefaultCommentsArrayC = new t.Type< - CommentsArray, - CommentsArray, - unknown ->( +export const DefaultCommentsArray = new t.Type( 'DefaultCommentsArray', t.array(comments).is, (input): Either => diff --git a/x-pack/plugins/lists/common/schemas/types/default_create_comments_array.ts b/x-pack/plugins/lists/common/schemas/types/default_create_comments_array.ts index 51431b9c39850..7fd79782836e3 100644 --- a/x-pack/plugins/lists/common/schemas/types/default_create_comments_array.ts +++ b/x-pack/plugins/lists/common/schemas/types/default_create_comments_array.ts @@ -9,13 +9,11 @@ import { Either } from 'fp-ts/lib/Either'; import { CreateCommentsArray, createComments } from './create_comments'; -export type DefaultCreateCommentsArrayC = t.Type; - /** * Types the DefaultCreateComments as: * - If null or undefined, then a default array of type entry will be set */ -export const DefaultCreateCommentsArray: DefaultCreateCommentsArrayC = new t.Type< +export const DefaultCreateCommentsArray = new t.Type< CreateCommentsArray, CreateCommentsArray, unknown diff --git a/x-pack/plugins/lists/common/schemas/types/default_entries_array.ts b/x-pack/plugins/lists/common/schemas/types/default_entries_array.ts index da67fb286affa..a85fdf8537f39 100644 --- a/x-pack/plugins/lists/common/schemas/types/default_entries_array.ts +++ b/x-pack/plugins/lists/common/schemas/types/default_entries_array.ts @@ -9,17 +9,11 @@ import { Either } from 'fp-ts/lib/Either'; import { EntriesArray, entriesArray } from './entries'; -export type DefaultEntriesArrayC = t.Type; - /** * Types the DefaultEntriesArray as: * - If null or undefined, then a default array of type entry will be set */ -export const DefaultEntryArray: DefaultEntriesArrayC = new t.Type< - EntriesArray, - EntriesArray, - unknown ->( +export const DefaultEntryArray = new t.Type( 'DefaultEntryArray', entriesArray.is, (input): Either => diff --git a/x-pack/plugins/lists/common/schemas/types/default_namespace.ts b/x-pack/plugins/lists/common/schemas/types/default_namespace.ts index ecc45d3c84313..b61497a78aff0 100644 --- a/x-pack/plugins/lists/common/schemas/types/default_namespace.ts +++ b/x-pack/plugins/lists/common/schemas/types/default_namespace.ts @@ -14,12 +14,10 @@ export type NamespaceType = t.TypeOf; * Types the DefaultNamespace as: * - If null or undefined, then a default string/enumeration of "single" will be used. */ -export const DefaultNamespace = new t.Type( +export const DefaultNamespace = new t.Type( 'DefaultNamespace', namespaceType.is, (input, context): Either => input == null ? t.success('single') : namespaceType.validate(input, context), t.identity ); - -export type DefaultNamespaceC = typeof DefaultNamespace; diff --git a/x-pack/plugins/lists/common/schemas/types/default_namespace_array.test.ts b/x-pack/plugins/lists/common/schemas/types/default_namespace_array.test.ts index 055f93069950e..255c89959b610 100644 --- a/x-pack/plugins/lists/common/schemas/types/default_namespace_array.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/default_namespace_array.test.ts @@ -9,11 +9,11 @@ import { left } from 'fp-ts/lib/Either'; import { foldLeftRight, getPaths } from '../../siem_common_deps'; -import { DefaultNamespaceArray, DefaultNamespaceArrayTypeEncoded } from './default_namespace_array'; +import { DefaultNamespaceArray, DefaultNamespaceArrayType } from './default_namespace_array'; describe('default_namespace_array', () => { test('it should validate "null" single item as an array with a "single" value', () => { - const payload: DefaultNamespaceArrayTypeEncoded = null; + const payload: DefaultNamespaceArrayType = null; const decoded = DefaultNamespaceArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -33,7 +33,7 @@ describe('default_namespace_array', () => { }); test('it should validate "undefined" item as an array with a "single" value', () => { - const payload: DefaultNamespaceArrayTypeEncoded = undefined; + const payload: DefaultNamespaceArrayType = undefined; const decoded = DefaultNamespaceArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -42,7 +42,7 @@ describe('default_namespace_array', () => { }); test('it should validate "single" as an array of a "single" value', () => { - const payload: DefaultNamespaceArrayTypeEncoded = 'single'; + const payload: DefaultNamespaceArrayType = 'single'; const decoded = DefaultNamespaceArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -51,7 +51,7 @@ describe('default_namespace_array', () => { }); test('it should validate "agnostic" as an array of a "agnostic" value', () => { - const payload: DefaultNamespaceArrayTypeEncoded = 'agnostic'; + const payload: DefaultNamespaceArrayType = 'agnostic'; const decoded = DefaultNamespaceArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -60,7 +60,7 @@ describe('default_namespace_array', () => { }); test('it should validate "single,agnostic" as an array of 2 values of ["single", "agnostic"] values', () => { - const payload: DefaultNamespaceArrayTypeEncoded = 'agnostic,single'; + const payload: DefaultNamespaceArrayType = 'agnostic,single'; const decoded = DefaultNamespaceArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -69,7 +69,7 @@ describe('default_namespace_array', () => { }); test('it should validate 3 elements of "single,agnostic,single" as an array of 3 values of ["single", "agnostic", "single"] values', () => { - const payload: DefaultNamespaceArrayTypeEncoded = 'single,agnostic,single'; + const payload: DefaultNamespaceArrayType = 'single,agnostic,single'; const decoded = DefaultNamespaceArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -78,7 +78,7 @@ describe('default_namespace_array', () => { }); test('it should validate 3 elements of "single,agnostic, single" as an array of 3 values of ["single", "agnostic", "single"] values when there are spaces', () => { - const payload: DefaultNamespaceArrayTypeEncoded = ' single, agnostic, single '; + const payload: DefaultNamespaceArrayType = ' single, agnostic, single '; const decoded = DefaultNamespaceArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -87,7 +87,7 @@ describe('default_namespace_array', () => { }); test('it should not validate 3 elements of "single,agnostic,junk" since the 3rd value is junk', () => { - const payload: DefaultNamespaceArrayTypeEncoded = 'single,agnostic,junk'; + const payload: DefaultNamespaceArrayType = 'single,agnostic,junk'; const decoded = DefaultNamespaceArray.decode(payload); const message = pipe(decoded, foldLeftRight); diff --git a/x-pack/plugins/lists/common/schemas/types/default_namespace_array.ts b/x-pack/plugins/lists/common/schemas/types/default_namespace_array.ts index c4099a48ffbcc..b0ae85e65b22a 100644 --- a/x-pack/plugins/lists/common/schemas/types/default_namespace_array.ts +++ b/x-pack/plugins/lists/common/schemas/types/default_namespace_array.ts @@ -39,7 +39,5 @@ export const DefaultNamespaceArray = new t.Type< String ); -export type DefaultNamespaceC = typeof DefaultNamespaceArray; - -export type DefaultNamespaceArrayTypeEncoded = t.OutputOf; +export type DefaultNamespaceArrayType = t.OutputOf; export type DefaultNamespaceArrayTypeDecoded = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/types/default_update_comments_array.ts b/x-pack/plugins/lists/common/schemas/types/default_update_comments_array.ts index c2593826a6358..854b7cf7ada7e 100644 --- a/x-pack/plugins/lists/common/schemas/types/default_update_comments_array.ts +++ b/x-pack/plugins/lists/common/schemas/types/default_update_comments_array.ts @@ -9,13 +9,11 @@ import { Either } from 'fp-ts/lib/Either'; import { UpdateCommentsArray, updateCommentsArray } from './update_comments'; -export type DefaultUpdateCommentsArrayC = t.Type; - /** * Types the DefaultCommentsUpdate as: * - If null or undefined, then a default array of type entry will be set */ -export const DefaultUpdateCommentsArray: DefaultUpdateCommentsArrayC = new t.Type< +export const DefaultUpdateCommentsArray = new t.Type< UpdateCommentsArray, UpdateCommentsArray, unknown diff --git a/x-pack/plugins/lists/common/schemas/types/empty_string_array.ts b/x-pack/plugins/lists/common/schemas/types/empty_string_array.ts index 389dc4a410cc9..bbaa66260e76e 100644 --- a/x-pack/plugins/lists/common/schemas/types/empty_string_array.ts +++ b/x-pack/plugins/lists/common/schemas/types/empty_string_array.ts @@ -39,7 +39,5 @@ export const EmptyStringArray = new t.Type; export type EmptyStringArrayDecoded = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/types/non_empty_string_array.test.ts b/x-pack/plugins/lists/common/schemas/types/non_empty_string_array.test.ts index 6124487cdd7fb..fac088568f85e 100644 --- a/x-pack/plugins/lists/common/schemas/types/non_empty_string_array.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/non_empty_string_array.test.ts @@ -9,11 +9,11 @@ import { left } from 'fp-ts/lib/Either'; import { foldLeftRight, getPaths } from '../../siem_common_deps'; -import { NonEmptyStringArray, NonEmptyStringArrayEncoded } from './non_empty_string_array'; +import { NonEmptyStringArray } from './non_empty_string_array'; describe('non_empty_string_array', () => { test('it should NOT validate "null"', () => { - const payload: NonEmptyStringArrayEncoded | null = null; + const payload: NonEmptyStringArray | null = null; const decoded = NonEmptyStringArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -24,7 +24,7 @@ describe('non_empty_string_array', () => { }); test('it should NOT validate "undefined"', () => { - const payload: NonEmptyStringArrayEncoded | undefined = undefined; + const payload: NonEmptyStringArray | undefined = undefined; const decoded = NonEmptyStringArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -35,7 +35,7 @@ describe('non_empty_string_array', () => { }); test('it should NOT validate a single value of an empty string ""', () => { - const payload: NonEmptyStringArrayEncoded = ''; + const payload: NonEmptyStringArray = ''; const decoded = NonEmptyStringArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -46,7 +46,7 @@ describe('non_empty_string_array', () => { }); test('it should validate a single value of "a" into an array of size 1 of ["a"]', () => { - const payload: NonEmptyStringArrayEncoded = 'a'; + const payload: NonEmptyStringArray = 'a'; const decoded = NonEmptyStringArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -55,7 +55,7 @@ describe('non_empty_string_array', () => { }); test('it should validate 2 values of "a,b" into an array of size 2 of ["a", "b"]', () => { - const payload: NonEmptyStringArrayEncoded = 'a,b'; + const payload: NonEmptyStringArray = 'a,b'; const decoded = NonEmptyStringArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -64,7 +64,7 @@ describe('non_empty_string_array', () => { }); test('it should validate 3 values of "a,b,c" into an array of size 3 of ["a", "b", "c"]', () => { - const payload: NonEmptyStringArrayEncoded = 'a,b,c'; + const payload: NonEmptyStringArray = 'a,b,c'; const decoded = NonEmptyStringArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -84,7 +84,7 @@ describe('non_empty_string_array', () => { }); test('it should validate 3 values of " a, b, c " into an array of size 3 of ["a", "b", "c"] even though they have spaces', () => { - const payload: NonEmptyStringArrayEncoded = ' a, b, c '; + const payload: NonEmptyStringArray = ' a, b, c '; const decoded = NonEmptyStringArray.decode(payload); const message = pipe(decoded, foldLeftRight); diff --git a/x-pack/plugins/lists/common/schemas/types/non_empty_string_array.ts b/x-pack/plugins/lists/common/schemas/types/non_empty_string_array.ts index c4a640e7cdbad..90475f7935875 100644 --- a/x-pack/plugins/lists/common/schemas/types/non_empty_string_array.ts +++ b/x-pack/plugins/lists/common/schemas/types/non_empty_string_array.ts @@ -35,7 +35,5 @@ export const NonEmptyStringArray = new t.Type( String ); -export type NonEmptyStringArrayC = typeof NonEmptyStringArray; - -export type NonEmptyStringArrayEncoded = t.OutputOf; +export type NonEmptyStringArray = t.OutputOf; export type NonEmptyStringArrayDecoded = t.TypeOf; diff --git a/x-pack/plugins/lists/common/types.ts b/x-pack/plugins/lists/common/types.ts index 1539c5ae01ff5..cee5567a55a6c 100644 --- a/x-pack/plugins/lists/common/types.ts +++ b/x-pack/plugins/lists/common/types.ts @@ -20,11 +20,3 @@ export type RequiredKeepUndefined = { [K in keyof T]-?: [T[K]] } extends infe ? { [K in keyof U]: U[K][0] } : never : never; - -/** - * This is just a helper to cleanup nasty intersections and unions to make them - * readable from io.ts, it's an identity that strips away the uglyness of them. - */ -export type Identity = { - [P in keyof T]: T[P]; -}; diff --git a/x-pack/plugins/lists/server/routes/find_list_item_route.ts b/x-pack/plugins/lists/server/routes/find_list_item_route.ts index 52d534b08df2b..8a0e06aa0c7d8 100644 --- a/x-pack/plugins/lists/server/routes/find_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/find_list_item_route.ts @@ -10,7 +10,7 @@ import { LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; import { validate } from '../../common/siem_common_deps'; import { - FindListItemSchemaPartialDecoded, + FindListItemSchemaDecoded, findListItemSchema, foundListItemSchema, } from '../../common/schemas'; @@ -26,7 +26,7 @@ export const findListItemRoute = (router: IRouter): void => { }, path: `${LIST_ITEM_URL}/_find`, validate: { - query: buildRouteValidation( + query: buildRouteValidation( findListItemSchema ), }, diff --git a/x-pack/plugins/lists/server/routes/patch_list_item_route.ts b/x-pack/plugins/lists/server/routes/patch_list_item_route.ts index f706559dffdbd..9a74beb45bafd 100644 --- a/x-pack/plugins/lists/server/routes/patch_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/patch_list_item_route.ts @@ -27,9 +27,10 @@ export const patchListItemRoute = (router: IRouter): void => { async (context, request, response) => { const siemResponse = buildSiemResponse(response); try { - const { value, id, meta } = request.body; + const { value, id, meta, _version } = request.body; const lists = getListClient(context); const listItem = await lists.updateListItem({ + _version, id, meta, value, diff --git a/x-pack/plugins/lists/server/routes/patch_list_route.ts b/x-pack/plugins/lists/server/routes/patch_list_route.ts index 3a0d8714a14cd..06a76559dee9a 100644 --- a/x-pack/plugins/lists/server/routes/patch_list_route.ts +++ b/x-pack/plugins/lists/server/routes/patch_list_route.ts @@ -27,9 +27,9 @@ export const patchListRoute = (router: IRouter): void => { async (context, request, response) => { const siemResponse = buildSiemResponse(response); try { - const { name, description, id, meta } = request.body; + const { name, description, id, meta, _version } = request.body; const lists = getListClient(context); - const list = await lists.updateList({ description, id, meta, name }); + const list = await lists.updateList({ _version, description, id, meta, name }); if (list == null) { return siemResponse.error({ body: `list id: "${id}" found found`, diff --git a/x-pack/plugins/lists/server/routes/update_endpoint_list_item_route.ts b/x-pack/plugins/lists/server/routes/update_endpoint_list_item_route.ts index 1ecf4e8a9765d..8415c64633a06 100644 --- a/x-pack/plugins/lists/server/routes/update_endpoint_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/update_endpoint_list_item_route.ts @@ -41,6 +41,7 @@ export const updateEndpointListItemRoute = (router: IRouter): void => { meta, type, _tags, + _version, comments, entries, item_id: itemId, @@ -49,6 +50,7 @@ export const updateEndpointListItemRoute = (router: IRouter): void => { const exceptionLists = getExceptionListClient(context); const exceptionListItem = await exceptionLists.updateEndpointListItem({ _tags, + _version, comments, description, entries, diff --git a/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts index f6c7bcebedc13..2aa1e016d51ed 100644 --- a/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts @@ -41,6 +41,7 @@ export const updateExceptionListItemRoute = (router: IRouter): void => { meta, type, _tags, + _version, comments, entries, item_id: itemId, @@ -50,6 +51,7 @@ export const updateExceptionListItemRoute = (router: IRouter): void => { const exceptionLists = getExceptionListClient(context); const exceptionListItem = await exceptionLists.updateExceptionListItem({ _tags, + _version, comments, description, entries, diff --git a/x-pack/plugins/lists/server/routes/update_exception_list_route.ts b/x-pack/plugins/lists/server/routes/update_exception_list_route.ts index cff78614d05ba..331ec064fa663 100644 --- a/x-pack/plugins/lists/server/routes/update_exception_list_route.ts +++ b/x-pack/plugins/lists/server/routes/update_exception_list_route.ts @@ -36,6 +36,7 @@ export const updateExceptionListRoute = (router: IRouter): void => { try { const { _tags, + _version, tags, name, description, @@ -54,6 +55,7 @@ export const updateExceptionListRoute = (router: IRouter): void => { } else { const list = await exceptionLists.updateExceptionList({ _tags, + _version, description, id, listId, diff --git a/x-pack/plugins/lists/server/routes/update_list_item_route.ts b/x-pack/plugins/lists/server/routes/update_list_item_route.ts index 3e231e319104b..0f5d11afcda09 100644 --- a/x-pack/plugins/lists/server/routes/update_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/update_list_item_route.ts @@ -27,9 +27,10 @@ export const updateListItemRoute = (router: IRouter): void => { async (context, request, response) => { const siemResponse = buildSiemResponse(response); try { - const { value, id, meta } = request.body; + const { value, id, meta, _version } = request.body; const lists = getListClient(context); const listItem = await lists.updateListItem({ + _version, id, meta, value, diff --git a/x-pack/plugins/lists/server/routes/update_list_route.ts b/x-pack/plugins/lists/server/routes/update_list_route.ts index a6d9f8329c7c8..2fae910c1b398 100644 --- a/x-pack/plugins/lists/server/routes/update_list_route.ts +++ b/x-pack/plugins/lists/server/routes/update_list_route.ts @@ -27,9 +27,9 @@ export const updateListRoute = (router: IRouter): void => { async (context, request, response) => { const siemResponse = buildSiemResponse(response); try { - const { name, description, id, meta } = request.body; + const { name, description, id, meta, _version } = request.body; const lists = getListClient(context); - const list = await lists.updateList({ description, id, meta, name }); + const list = await lists.updateList({ _version, description, id, meta, name }); if (list == null) { return siemResponse.error({ body: `list id: "${id}" found found`, diff --git a/x-pack/plugins/lists/server/scripts/exception_lists/updates/simple_update.json b/x-pack/plugins/lists/server/scripts/exception_lists/updates/simple_update.json index a7fbe1ea48c02..8d07b29d7b428 100644 --- a/x-pack/plugins/lists/server/scripts/exception_lists/updates/simple_update.json +++ b/x-pack/plugins/lists/server/scripts/exception_lists/updates/simple_update.json @@ -1,5 +1,5 @@ { - "list_id": "endpoint_list", + "list_id": "simple_list", "_tags": ["endpoint", "process", "malware", "os:linux"], "tags": ["user added string for a tag", "malware"], "type": "endpoint", diff --git a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts index 5c9607e2d956d..08b1f517036a9 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts @@ -131,6 +131,7 @@ export class ExceptionListClient { */ public updateEndpointListItem = async ({ _tags, + _version, comments, description, entries, @@ -145,6 +146,7 @@ export class ExceptionListClient { await this.createEndpointList(); return updateExceptionListItem({ _tags, + _version, comments, description, entries, @@ -198,6 +200,7 @@ export class ExceptionListClient { public updateExceptionList = async ({ _tags, + _version, id, description, listId, @@ -210,6 +213,7 @@ export class ExceptionListClient { const { savedObjectsClient, user } = this; return updateExceptionList({ _tags, + _version, description, id, listId, @@ -270,6 +274,7 @@ export class ExceptionListClient { public updateExceptionListItem = async ({ _tags, + _version, comments, description, entries, @@ -284,6 +289,7 @@ export class ExceptionListClient { const { savedObjectsClient, user } = this; return updateExceptionListItem({ _tags, + _version, comments, description, entries, diff --git a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts index 89f8310281648..b972b6564bb8a 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts @@ -38,6 +38,7 @@ import { UpdateCommentsArray, _Tags, _TagsOrUndefined, + _VersionOrUndefined, } from '../../../common/schemas'; export interface ConstructorOptions { @@ -64,6 +65,7 @@ export interface CreateExceptionListOptions { export interface UpdateExceptionListOptions { _tags: _TagsOrUndefined; + _version: _VersionOrUndefined; id: IdOrUndefined; listId: ListIdOrUndefined; namespaceType: NamespaceType; @@ -130,6 +132,7 @@ export interface CreateEndpointListItemOptions { export interface UpdateExceptionListItemOptions { _tags: _TagsOrUndefined; + _version: _VersionOrUndefined; comments: UpdateCommentsArray; entries: EntriesArrayOrUndefined; id: IdOrUndefined; @@ -144,6 +147,7 @@ export interface UpdateExceptionListItemOptions { export interface UpdateEndpointListItemOptions { _tags: _TagsOrUndefined; + _version: _VersionOrUndefined; comments: UpdateCommentsArray; entries: EntriesArrayOrUndefined; id: IdOrUndefined; diff --git a/x-pack/plugins/lists/server/services/exception_lists/update_exception_list.ts b/x-pack/plugins/lists/server/services/exception_lists/update_exception_list.ts index a739366c67331..99c42e56f4888 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/update_exception_list.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/update_exception_list.ts @@ -18,6 +18,7 @@ import { NamespaceType, TagsOrUndefined, _TagsOrUndefined, + _VersionOrUndefined, } from '../../../common/schemas'; import { getSavedObjectType, transformSavedObjectUpdateToExceptionList } from './utils'; @@ -26,6 +27,7 @@ import { getExceptionList } from './get_exception_list'; interface UpdateExceptionListOptions { id: IdOrUndefined; _tags: _TagsOrUndefined; + _version: _VersionOrUndefined; name: NameOrUndefined; description: DescriptionOrUndefined; savedObjectsClient: SavedObjectsClientContract; @@ -40,6 +42,7 @@ interface UpdateExceptionListOptions { export const updateExceptionList = async ({ _tags, + _version, id, savedObjectsClient, namespaceType, @@ -67,6 +70,9 @@ export const updateExceptionList = async ({ tags, type, updated_by: user, + }, + { + version: _version, } ); return transformSavedObjectUpdateToExceptionList({ exceptionList, savedObject }); diff --git a/x-pack/plugins/lists/server/services/exception_lists/update_exception_list_item.ts b/x-pack/plugins/lists/server/services/exception_lists/update_exception_list_item.ts index a5ed1e38df374..f26dd7e18dd5c 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/update_exception_list_item.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/update_exception_list_item.ts @@ -20,6 +20,7 @@ import { TagsOrUndefined, UpdateCommentsArrayOrUndefined, _TagsOrUndefined, + _VersionOrUndefined, } from '../../../common/schemas'; import { @@ -33,6 +34,7 @@ interface UpdateExceptionListItemOptions { id: IdOrUndefined; comments: UpdateCommentsArrayOrUndefined; _tags: _TagsOrUndefined; + _version: _VersionOrUndefined; name: NameOrUndefined; description: DescriptionOrUndefined; entries: EntriesArrayOrUndefined; @@ -48,6 +50,7 @@ interface UpdateExceptionListItemOptions { export const updateExceptionListItem = async ({ _tags, + _version, comments, entries, id, @@ -89,6 +92,9 @@ export const updateExceptionListItem = async ({ tags, type, updated_by: user, + }, + { + version: _version, } ); return transformSavedObjectUpdateToExceptionListItem({ diff --git a/x-pack/plugins/lists/server/services/exception_lists/utils.ts b/x-pack/plugins/lists/server/services/exception_lists/utils.ts index ded39933fe9d8..d5e1965efcc89 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/utils.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/utils.ts @@ -72,6 +72,7 @@ export const transformSavedObjectToExceptionList = ({ }): ExceptionListSchema => { const dateNow = new Date().toISOString(); const { + version: _version, attributes: { _tags, created_at, @@ -93,6 +94,7 @@ export const transformSavedObjectToExceptionList = ({ // TODO: Do a throw if after the decode this is not the correct "list_type: list" return { _tags, + _version, created_at, created_by, description, @@ -118,6 +120,7 @@ export const transformSavedObjectUpdateToExceptionList = ({ }): ExceptionListSchema => { const dateNow = new Date().toISOString(); const { + version: _version, attributes: { _tags, description, meta, name, tags, type, updated_by: updatedBy }, id, updated_at: updatedAt, @@ -127,6 +130,7 @@ export const transformSavedObjectUpdateToExceptionList = ({ // TODO: Do a throw if after the decode this is not the correct "list_type: list" return { _tags: _tags ?? exceptionList._tags, + _version, created_at: exceptionList.created_at, created_by: exceptionList.created_by, description: description ?? exceptionList.description, @@ -150,6 +154,7 @@ export const transformSavedObjectToExceptionListItem = ({ }): ExceptionListItemSchema => { const dateNow = new Date().toISOString(); const { + version: _version, attributes: { _tags, comments, @@ -174,6 +179,7 @@ export const transformSavedObjectToExceptionListItem = ({ // TODO: Do a throw if item_id or entries is not defined. return { _tags, + _version, comments: comments ?? [], created_at, created_by, @@ -202,6 +208,7 @@ export const transformSavedObjectUpdateToExceptionListItem = ({ }): ExceptionListItemSchema => { const dateNow = new Date().toISOString(); const { + version: _version, attributes: { _tags, comments, @@ -223,6 +230,7 @@ export const transformSavedObjectUpdateToExceptionListItem = ({ // defaulting return { _tags: _tags ?? exceptionListItem._tags, + _version, comments: comments ?? exceptionListItem.comments, created_at: exceptionListItem.created_at, created_by: exceptionListItem.created_by, diff --git a/x-pack/plugins/lists/server/services/items/create_list_item.ts b/x-pack/plugins/lists/server/services/items/create_list_item.ts index aa17fc00b25c6..5332e2f6c7f4e 100644 --- a/x-pack/plugins/lists/server/services/items/create_list_item.ts +++ b/x-pack/plugins/lists/server/services/items/create_list_item.ts @@ -18,6 +18,7 @@ import { Type, } from '../../../common/schemas'; import { transformListItemToElasticQuery } from '../utils'; +import { encodeHitVersion } from '../utils/encode_hit_version'; export interface CreateListItemOptions { deserializer: DeserializerOrUndefined; @@ -75,6 +76,7 @@ export const createListItem = async ({ }); return { + _version: encodeHitVersion(response), id: response._id, type, value, diff --git a/x-pack/plugins/lists/server/services/items/find_list_item.ts b/x-pack/plugins/lists/server/services/items/find_list_item.ts index 93fa682631b06..a997b10004e76 100644 --- a/x-pack/plugins/lists/server/services/items/find_list_item.ts +++ b/x-pack/plugins/lists/server/services/items/find_list_item.ts @@ -5,6 +5,7 @@ */ import { LegacyAPICaller } from 'kibana/server'; +import { SearchResponse } from 'elasticsearch'; import { Filter, @@ -82,7 +83,10 @@ export const findListItem = async ({ }); if (scroll.validSearchAfterFound) { - const response = await callCluster('search', { + // Note: This typing of response = await callCluster> + // is because when you pass in seq_no_primary_term: true it does a "fall through" type and you have + // to explicitly define the type . + const response = await callCluster>('search', { body: { query, search_after: scroll.searchAfter, @@ -90,6 +94,7 @@ export const findListItem = async ({ }, ignoreUnavailable: true, index: listItemIndex, + seq_no_primary_term: true, size: perPage, }); return { diff --git a/x-pack/plugins/lists/server/services/items/get_list_item.ts b/x-pack/plugins/lists/server/services/items/get_list_item.ts index 6f2a7ad63a973..ad9ae8763fe94 100644 --- a/x-pack/plugins/lists/server/services/items/get_list_item.ts +++ b/x-pack/plugins/lists/server/services/items/get_list_item.ts @@ -5,6 +5,7 @@ */ import { LegacyAPICaller } from 'kibana/server'; +import { SearchResponse } from 'elasticsearch'; import { Id, ListItemSchema, SearchEsListItemSchema } from '../../../common/schemas'; import { transformElasticToListItem } from '../utils'; @@ -21,7 +22,10 @@ export const getListItem = async ({ callCluster, listItemIndex, }: GetListItemOptions): Promise => { - const listItemES = await callCluster('search', { + // Note: This typing of response = await callCluster> + // is because when you pass in seq_no_primary_term: true it does a "fall through" type and you have + // to explicitly define the type . + const listItemES = await callCluster>('search', { body: { query: { term: { @@ -31,6 +35,7 @@ export const getListItem = async ({ }, ignoreUnavailable: true, index: listItemIndex, + seq_no_primary_term: true, }); if (listItemES.hits.hits.length) { diff --git a/x-pack/plugins/lists/server/services/items/update_list_item.mock.ts b/x-pack/plugins/lists/server/services/items/update_list_item.mock.ts index 7ee8664b04d6b..8fc4b55338344 100644 --- a/x-pack/plugins/lists/server/services/items/update_list_item.mock.ts +++ b/x-pack/plugins/lists/server/services/items/update_list_item.mock.ts @@ -15,6 +15,7 @@ import { } from '../../../common/constants.mock'; export const getUpdateListItemOptionsMock = (): UpdateListItemOptions => ({ + _version: undefined, callCluster: getCallClusterMock(), dateNow: DATE_NOW, id: LIST_ITEM_ID, diff --git a/x-pack/plugins/lists/server/services/items/update_list_item.ts b/x-pack/plugins/lists/server/services/items/update_list_item.ts index eb20f1cfe3b30..8387d916b12f2 100644 --- a/x-pack/plugins/lists/server/services/items/update_list_item.ts +++ b/x-pack/plugins/lists/server/services/items/update_list_item.ts @@ -12,12 +12,16 @@ import { ListItemSchema, MetaOrUndefined, UpdateEsListItemSchema, + _VersionOrUndefined, } from '../../../common/schemas'; import { transformListItemToElasticQuery } from '../utils'; +import { decodeVersion } from '../utils/decode_version'; +import { encodeHitVersion } from '../utils/encode_hit_version'; import { getListItem } from './get_list_item'; export interface UpdateListItemOptions { + _version: _VersionOrUndefined; id: Id; value: string | null | undefined; callCluster: LegacyAPICaller; @@ -28,6 +32,7 @@ export interface UpdateListItemOptions { } export const updateListItem = async ({ + _version, id, value, callCluster, @@ -57,6 +62,7 @@ export const updateListItem = async ({ }; const response = await callCluster('update', { + ...decodeVersion(_version), body: { doc, }, @@ -65,6 +71,7 @@ export const updateListItem = async ({ refresh: 'wait_for', }); return { + _version: encodeHitVersion(response), created_at: listItem.created_at, created_by: listItem.created_by, deserializer: listItem.deserializer, diff --git a/x-pack/plugins/lists/server/services/lists/create_list.ts b/x-pack/plugins/lists/server/services/lists/create_list.ts index 3d396cf4d5af9..f97399e6dc131 100644 --- a/x-pack/plugins/lists/server/services/lists/create_list.ts +++ b/x-pack/plugins/lists/server/services/lists/create_list.ts @@ -8,6 +8,7 @@ import uuid from 'uuid'; import { CreateDocumentResponse } from 'elasticsearch'; import { LegacyAPICaller } from 'kibana/server'; +import { encodeHitVersion } from '../utils/encode_hit_version'; import { Description, DeserializerOrUndefined, @@ -70,6 +71,7 @@ export const createList = async ({ refresh: 'wait_for', }); return { + _version: encodeHitVersion(response), id: response._id, ...body, }; diff --git a/x-pack/plugins/lists/server/services/lists/find_list.ts b/x-pack/plugins/lists/server/services/lists/find_list.ts index 86cead9e868d6..363794b6affe4 100644 --- a/x-pack/plugins/lists/server/services/lists/find_list.ts +++ b/x-pack/plugins/lists/server/services/lists/find_list.ts @@ -5,6 +5,7 @@ */ import { LegacyAPICaller } from 'kibana/server'; +import { SearchResponse } from 'elasticsearch'; import { Filter, @@ -71,7 +72,10 @@ export const findList = async ({ }); if (scroll.validSearchAfterFound) { - const response = await callCluster('search', { + // Note: This typing of response = await callCluster> + // is because when you pass in seq_no_primary_term: true it does a "fall through" type and you have + // to explicitly define the type . + const response = await callCluster>('search', { body: { query, search_after: scroll.searchAfter, @@ -79,6 +83,7 @@ export const findList = async ({ }, ignoreUnavailable: true, index: listIndex, + seq_no_primary_term: true, size: perPage, }); return { diff --git a/x-pack/plugins/lists/server/services/lists/get_list.ts b/x-pack/plugins/lists/server/services/lists/get_list.ts index 13550eb7d30dd..860e8e9f97f87 100644 --- a/x-pack/plugins/lists/server/services/lists/get_list.ts +++ b/x-pack/plugins/lists/server/services/lists/get_list.ts @@ -5,6 +5,7 @@ */ import { LegacyAPICaller } from 'kibana/server'; +import { SearchResponse } from 'elasticsearch'; import { Id, ListSchema, SearchEsListSchema } from '../../../common/schemas'; import { transformElasticToList } from '../utils/transform_elastic_to_list'; @@ -20,7 +21,10 @@ export const getList = async ({ callCluster, listIndex, }: GetListOptions): Promise => { - const response = await callCluster('search', { + // Note: This typing of response = await callCluster> + // is because when you pass in seq_no_primary_term: true it does a "fall through" type and you have + // to explicitly define the type . + const response = await callCluster>('search', { body: { query: { term: { @@ -30,6 +34,7 @@ export const getList = async ({ }, ignoreUnavailable: true, index: listIndex, + seq_no_primary_term: true, }); const list = transformElasticToList({ response }); return list[0] ?? null; diff --git a/x-pack/plugins/lists/server/services/lists/list_client.ts b/x-pack/plugins/lists/server/services/lists/list_client.ts index 4acc2e7092491..9bece64fa943f 100644 --- a/x-pack/plugins/lists/server/services/lists/list_client.ts +++ b/x-pack/plugins/lists/server/services/lists/list_client.ts @@ -395,6 +395,7 @@ export class ListClient { }; public updateListItem = async ({ + _version, id, value, meta, @@ -402,6 +403,7 @@ export class ListClient { const { callCluster, user } = this; const listItemIndex = this.getListItemIndex(); return updateListItem({ + _version, callCluster, id, listItemIndex, @@ -412,6 +414,7 @@ export class ListClient { }; public updateList = async ({ + _version, id, name, description, @@ -420,6 +423,7 @@ export class ListClient { const { callCluster, user } = this; const listIndex = this.getListIndex(); return updateList({ + _version, callCluster, description, id, diff --git a/x-pack/plugins/lists/server/services/lists/list_client_types.ts b/x-pack/plugins/lists/server/services/lists/list_client_types.ts index 68a018fa2fc16..7fa1727be118b 100644 --- a/x-pack/plugins/lists/server/services/lists/list_client_types.ts +++ b/x-pack/plugins/lists/server/services/lists/list_client_types.ts @@ -26,6 +26,7 @@ import { SortFieldOrUndefined, SortOrderOrUndefined, Type, + _VersionOrUndefined, } from '../../../common/schemas'; import { ConfigType } from '../../config'; @@ -106,12 +107,14 @@ export interface CreateListItemOptions { } export interface UpdateListItemOptions { + _version: _VersionOrUndefined; id: Id; value: string | null | undefined; meta: MetaOrUndefined; } export interface UpdateListOptions { + _version: _VersionOrUndefined; id: Id; name: NameOrUndefined; description: DescriptionOrUndefined; diff --git a/x-pack/plugins/lists/server/services/lists/update_list.mock.ts b/x-pack/plugins/lists/server/services/lists/update_list.mock.ts index ff974b6e7352b..fc3d63277c5b5 100644 --- a/x-pack/plugins/lists/server/services/lists/update_list.mock.ts +++ b/x-pack/plugins/lists/server/services/lists/update_list.mock.ts @@ -16,6 +16,7 @@ import { } from '../../../common/constants.mock'; export const getUpdateListOptionsMock = (): UpdateListOptions => ({ + _version: undefined, callCluster: getCallClusterMock(), dateNow: DATE_NOW, description: DESCRIPTION, diff --git a/x-pack/plugins/lists/server/services/lists/update_list.ts b/x-pack/plugins/lists/server/services/lists/update_list.ts index f84ca787eaa7c..fba57ca744f9d 100644 --- a/x-pack/plugins/lists/server/services/lists/update_list.ts +++ b/x-pack/plugins/lists/server/services/lists/update_list.ts @@ -7,6 +7,8 @@ import { CreateDocumentResponse } from 'elasticsearch'; import { LegacyAPICaller } from 'kibana/server'; +import { decodeVersion } from '../utils/decode_version'; +import { encodeHitVersion } from '../utils/encode_hit_version'; import { DescriptionOrUndefined, Id, @@ -14,11 +16,13 @@ import { MetaOrUndefined, NameOrUndefined, UpdateEsListSchema, + _VersionOrUndefined, } from '../../../common/schemas'; import { getList } from '.'; export interface UpdateListOptions { + _version: _VersionOrUndefined; id: Id; callCluster: LegacyAPICaller; listIndex: string; @@ -30,6 +34,7 @@ export interface UpdateListOptions { } export const updateList = async ({ + _version, id, name, description, @@ -52,12 +57,14 @@ export const updateList = async ({ updated_by: user, }; const response = await callCluster('update', { + ...decodeVersion(_version), body: { doc }, id, index: listIndex, refresh: 'wait_for', }); return { + _version: encodeHitVersion(response), created_at: list.created_at, created_by: list.created_by, description: description ?? list.description, diff --git a/x-pack/plugins/lists/server/services/utils/decode_version.ts b/x-pack/plugins/lists/server/services/utils/decode_version.ts new file mode 100644 index 0000000000000..e5fb9b54bcf97 --- /dev/null +++ b/x-pack/plugins/lists/server/services/utils/decode_version.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// Similar to the src/core/server/saved_objects/version/decode_version.ts +// with the notable differences in that it is more tolerant and does not throw saved object specific errors +// but rather just returns an empty object if it cannot parse the version or cannot find one. +export const decodeVersion = ( + version: string | undefined +): + | { + ifSeqNo: number; + ifPrimaryTerm: number; + } + | {} => { + if (version != null) { + try { + const decoded = Buffer.from(version, 'base64').toString('utf8'); + const parsed = JSON.parse(decoded); + if (Array.isArray(parsed) && Number.isInteger(parsed[0]) && Number.isInteger(parsed[1])) { + return { + ifPrimaryTerm: parsed[1], + ifSeqNo: parsed[0], + }; + } else { + return {}; + } + } catch (err) { + // do nothing here, this is on purpose and we want to return any empty object when we can't parse. + return {}; + } + } else { + return {}; + } +}; diff --git a/x-pack/plugins/lists/server/services/utils/encode_hit_version.ts b/x-pack/plugins/lists/server/services/utils/encode_hit_version.ts new file mode 100644 index 0000000000000..42dccfbac7340 --- /dev/null +++ b/x-pack/plugins/lists/server/services/utils/encode_hit_version.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; + * you may not use this file except in compliance with the Elastic License. + */ + +/** + * Very similar to the encode_hit_version from saved object system from here: + * src/core/server/saved_objects/version/encode_hit_version.ts + * + * with the most notably change is that it doesn't do any throws but rather just returns undefined + * if _seq_no or _primary_term does not exist. + * @param response The response to encode into a version by using _seq_no and _primary_term + */ +export const encodeHitVersion = (hit: T): string | undefined => { + // Have to do this "as cast" here as these two types aren't included in the SearchResponse hit type + const { _seq_no: seqNo, _primary_term: primaryTerm } = (hit as unknown) as { + _seq_no: number; + _primary_term: number; + }; + + if (seqNo == null || primaryTerm == null) { + return undefined; + } else { + return Buffer.from(JSON.stringify([seqNo, primaryTerm]), 'utf8').toString('base64'); + } +}; diff --git a/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list.ts b/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list.ts index bb1ae1d4b9ff3..fb226d91fe395 100644 --- a/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list.ts +++ b/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list.ts @@ -8,6 +8,8 @@ import { SearchResponse } from 'elasticsearch'; import { ListArraySchema, SearchEsListSchema } from '../../../common/schemas'; +import { encodeHitVersion } from './encode_hit_version'; + export interface TransformElasticToListOptions { response: SearchResponse; } @@ -17,6 +19,7 @@ export const transformElasticToList = ({ }: TransformElasticToListOptions): ListArraySchema => { return response.hits.hits.map((hit) => { return { + _version: encodeHitVersion(hit), id: hit._id, ...hit._source, }; diff --git a/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.ts b/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.ts index a59b3b383cd2a..26fe15e9106fe 100644 --- a/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.ts +++ b/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.ts @@ -9,6 +9,7 @@ import { SearchResponse } from 'elasticsearch'; import { ListItemArraySchema, SearchEsListItemSchema, Type } from '../../../common/schemas'; import { ErrorWithStatusCode } from '../../error_with_status_code'; +import { encodeHitVersion } from './encode_hit_version'; import { findSourceValue } from './find_source_value'; export interface TransformElasticToListItemOptions { @@ -40,6 +41,7 @@ export const transformElasticToListItem = ({ throw new ErrorWithStatusCode(`Was expected ${type} to not be null/undefined`, 400); } else { return { + _version: encodeHitVersion(hit), created_at, created_by, deserializer, diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_actions_array.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_actions_array.ts index c69ae591f5ddc..e6b00aeac0d9c 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_actions_array.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_actions_array.ts @@ -10,14 +10,12 @@ import { actions, Actions } from '../common/schemas'; /** * Types the DefaultStringArray as: - * - If null or undefined, then a default action array will be set + * - If undefined, then a default action array will be set */ -export const DefaultActionsArray = new t.Type( +export const DefaultActionsArray = new t.Type( 'DefaultActionsArray', actions.is, (input, context): Either => input == null ? t.success([]) : actions.validate(input, context), t.identity ); - -export type DefaultActionsArrayC = typeof DefaultActionsArray; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_boolean_false.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_boolean_false.ts index 0cab6525779a6..5ace191c5f86f 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_boolean_false.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_boolean_false.ts @@ -11,12 +11,10 @@ import { Either } from 'fp-ts/lib/Either'; * Types the DefaultBooleanFalse as: * - If null or undefined, then a default false will be set */ -export const DefaultBooleanFalse = new t.Type( +export const DefaultBooleanFalse = new t.Type( 'DefaultBooleanFalse', t.boolean.is, (input, context): Either => input == null ? t.success(false) : t.boolean.validate(input, context), t.identity ); - -export type DefaultBooleanFalseC = typeof DefaultBooleanFalse; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_boolean_true.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_boolean_true.ts index 6997652b72636..92167287d23f5 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_boolean_true.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_boolean_true.ts @@ -11,12 +11,10 @@ import { Either } from 'fp-ts/lib/Either'; * Types the DefaultBooleanTrue as: * - If null or undefined, then a default true will be set */ -export const DefaultBooleanTrue = new t.Type( +export const DefaultBooleanTrue = new t.Type( 'DefaultBooleanTrue', t.boolean.is, (input, context): Either => input == null ? t.success(true) : t.boolean.validate(input, context), t.identity ); - -export type DefaultBooleanTrueC = typeof DefaultBooleanTrue; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_empty_string.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_empty_string.ts index a1103c4aa8d0e..185cccd86313e 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_empty_string.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_empty_string.ts @@ -11,12 +11,10 @@ import { Either } from 'fp-ts/lib/Either'; * Types the DefaultEmptyString as: * - If null or undefined, then a default of an empty string "" will be used */ -export const DefaultEmptyString = new t.Type( +export const DefaultEmptyString = new t.Type( 'DefaultEmptyString', t.string.is, (input, context): Either => input == null ? t.success('') : t.string.validate(input, context), t.identity ); - -export type DefaultEmptyStringC = typeof DefaultEmptyString; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_export_file_name.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_export_file_name.ts index 4c7f663e7f46d..12efda77e5435 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_export_file_name.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_export_file_name.ts @@ -11,12 +11,10 @@ import { Either } from 'fp-ts/lib/Either'; * Types the DefaultExportFileName as: * - If null or undefined, then a default of "export.ndjson" will be used */ -export const DefaultExportFileName = new t.Type( +export const DefaultExportFileName = new t.Type( 'DefaultExportFileName', t.string.is, (input, context): Either => input == null ? t.success('export.ndjson') : t.string.validate(input, context), t.identity ); - -export type DefaultExportFileNameC = typeof DefaultExportFileName; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_from_string.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_from_string.ts index b6b432858eb92..a85ea58b26478 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_from_string.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_from_string.ts @@ -11,12 +11,10 @@ import { Either } from 'fp-ts/lib/Either'; * Types the DefaultFromString as: * - If null or undefined, then a default of the string "now-6m" will be used */ -export const DefaultFromString = new t.Type( +export const DefaultFromString = new t.Type( 'DefaultFromString', t.string.is, (input, context): Either => input == null ? t.success('now-6m') : t.string.validate(input, context), t.identity ); - -export type DefaultFromStringC = typeof DefaultFromString; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_interval_string.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_interval_string.ts index 9492374ffe91e..4001af46e7ddf 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_interval_string.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_interval_string.ts @@ -11,12 +11,10 @@ import { Either } from 'fp-ts/lib/Either'; * Types the DefaultIntervalString as: * - If null or undefined, then a default of the string "5m" will be used */ -export const DefaultIntervalString = new t.Type( +export const DefaultIntervalString = new t.Type( 'DefaultIntervalString', t.string.is, (input, context): Either => input == null ? t.success('5m') : t.string.validate(input, context), t.identity ); - -export type DefaultIntervalStringC = typeof DefaultIntervalString; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_language_string.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_language_string.ts index 1e05a46d7273c..afa83c484698f 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_language_string.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_language_string.ts @@ -12,12 +12,10 @@ import { language } from '../common/schemas'; * Types the DefaultLanguageString as: * - If null or undefined, then a default of the string "kuery" will be used */ -export const DefaultLanguageString = new t.Type( +export const DefaultLanguageString = new t.Type( 'DefaultLanguageString', t.string.is, (input, context): Either => input == null ? t.success('kuery') : language.validate(input, context), t.identity ); - -export type DefaultLanguageStringC = typeof DefaultLanguageString; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_max_signals_number.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_max_signals_number.ts index d3c48b5522f57..518af32dcf2b4 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_max_signals_number.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_max_signals_number.ts @@ -16,11 +16,7 @@ import { DEFAULT_MAX_SIGNALS } from '../../../constants'; * - greater than 1 * - If undefined then it will use DEFAULT_MAX_SIGNALS (100) as the default */ -export const DefaultMaxSignalsNumber: DefaultMaxSignalsNumberC = new t.Type< - number, - number, - unknown ->( +export const DefaultMaxSignalsNumber = new t.Type( 'DefaultMaxSignals', t.number.is, (input, context): Either => { @@ -28,5 +24,3 @@ export const DefaultMaxSignalsNumber: DefaultMaxSignalsNumberC = new t.Type< }, t.identity ); - -export type DefaultMaxSignalsNumberC = t.Type; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_page.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_page.ts index 96e01d381e34b..f3a997e3cc897 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_page.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_page.ts @@ -14,7 +14,7 @@ import { PositiveIntegerGreaterThanZero } from './positive_integer_greater_than_ * - If null or undefined, then a default of 1 will be used * - If the number is 0 or less this will not validate as it has to be a positive number greater than zero */ -export const DefaultPage = new t.Type( +export const DefaultPage = new t.Type( 'DefaultPerPage', t.number.is, (input, context): Either => { @@ -28,5 +28,3 @@ export const DefaultPage = new t.Type( }, t.identity ); - -export type DefaultPageC = typeof DefaultPage; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_per_page.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_per_page.ts index b78de8b35cede..72e817b10a600 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_per_page.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_per_page.ts @@ -14,7 +14,7 @@ import { PositiveIntegerGreaterThanZero } from './positive_integer_greater_than_ * - If null or undefined, then a default of 20 will be used * - If the number is 0 or less this will not validate as it has to be a positive number greater than zero */ -export const DefaultPerPage = new t.Type( +export const DefaultPerPage = new t.Type( 'DefaultPerPage', t.number.is, (input, context): Either => { @@ -28,5 +28,3 @@ export const DefaultPerPage = new t.Type( }, t.identity ); - -export type DefaultPerPageC = typeof DefaultPerPage; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_risk_score_mapping_array.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_risk_score_mapping_array.ts index ba74045b4e32c..bf88ece913767 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_risk_score_mapping_array.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_risk_score_mapping_array.ts @@ -13,12 +13,14 @@ import { risk_score_mapping, RiskScoreMapping } from '../common/schemas'; * Types the DefaultStringArray as: * - If null or undefined, then a default risk_score_mapping array will be set */ -export const DefaultRiskScoreMappingArray = new t.Type( +export const DefaultRiskScoreMappingArray = new t.Type< + RiskScoreMapping, + RiskScoreMapping | undefined, + unknown +>( 'DefaultRiskScoreMappingArray', risk_score_mapping.is, (input, context): Either => input == null ? t.success([]) : risk_score_mapping.validate(input, context), t.identity ); - -export type DefaultRiskScoreMappingArrayC = typeof DefaultRiskScoreMappingArray; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_severity_mapping_array.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_severity_mapping_array.ts index 8e68b73148af1..56b0ac1b75982 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_severity_mapping_array.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_severity_mapping_array.ts @@ -13,12 +13,14 @@ import { severity_mapping, SeverityMapping } from '../common/schemas'; * Types the DefaultStringArray as: * - If null or undefined, then a default severity_mapping array will be set */ -export const DefaultSeverityMappingArray = new t.Type( +export const DefaultSeverityMappingArray = new t.Type< + SeverityMapping, + SeverityMapping | undefined, + unknown +>( 'DefaultSeverityMappingArray', severity_mapping.is, (input, context): Either => input == null ? t.success([]) : severity_mapping.validate(input, context), t.identity ); - -export type DefaultSeverityMappingArrayC = typeof DefaultSeverityMappingArray; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_string_array.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_string_array.ts index a8c53c230acd9..a973bbb37cac7 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_string_array.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_string_array.ts @@ -9,14 +9,13 @@ import { Either } from 'fp-ts/lib/Either'; /** * Types the DefaultStringArray as: - * - If null or undefined, then a default array will be set + * - If undefined, then a default array will be set + * - If an array is sent in, then the array will be validated to ensure all elements are a string */ -export const DefaultStringArray = new t.Type( +export const DefaultStringArray = new t.Type( 'DefaultStringArray', t.array(t.string).is, (input, context): Either => input == null ? t.success([]) : t.array(t.string).validate(input, context), t.identity ); - -export type DefaultStringArrayC = typeof DefaultStringArray; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_string_boolean_false.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_string_boolean_false.ts index aa070c171d7ea..eba805048fe3a 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_string_boolean_false.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_string_boolean_false.ts @@ -12,7 +12,7 @@ import { Either } from 'fp-ts/lib/Either'; * - If a string this will convert the string to a boolean * - If null or undefined, then a default false will be set */ -export const DefaultStringBooleanFalse = new t.Type( +export const DefaultStringBooleanFalse = new t.Type( 'DefaultStringBooleanFalse', t.boolean.is, (input, context): Either => { diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_threat_array.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_threat_array.ts index 5499a3c1e3064..3b3c20f003e6e 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_threat_array.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_threat_array.ts @@ -12,12 +12,10 @@ import { Threat, threat } from '../common/schemas'; * Types the DefaultThreatArray as: * - If null or undefined, then an empty array will be set */ -export const DefaultThreatArray = new t.Type( +export const DefaultThreatArray = new t.Type( 'DefaultThreatArray', threat.is, (input, context): Either => input == null ? t.success([]) : threat.validate(input, context), t.identity ); - -export type DefaultThreatArrayC = typeof DefaultThreatArray; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_throttle_null.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_throttle_null.ts index b76a35c0265a0..c4cf9a07ed359 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_throttle_null.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_throttle_null.ts @@ -12,12 +12,10 @@ import { ThrottleOrNull, throttle } from '../common/schemas'; * Types the DefaultThrottleNull as: * - If null or undefined, then a null will be set */ -export const DefaultThrottleNull = new t.Type( +export const DefaultThrottleNull = new t.Type( 'DefaultThreatNull', throttle.is, (input, context): Either => input == null ? t.success(null) : throttle.validate(input, context), t.identity ); - -export type DefaultThrottleNullC = typeof DefaultThrottleNull; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_to_string.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_to_string.ts index 158eedc121c53..6fe247f05e7e6 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_to_string.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_to_string.ts @@ -11,12 +11,10 @@ import { Either } from 'fp-ts/lib/Either'; * Types the DefaultToString as: * - If null or undefined, then a default of the string "now" will be used */ -export const DefaultToString = new t.Type( +export const DefaultToString = new t.Type( 'DefaultToString', t.string.is, (input, context): Either => input == null ? t.success('now') : t.string.validate(input, context), t.identity ); - -export type DefaultToStringC = typeof DefaultToString; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_uuid.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_uuid.ts index 74e32e083cc44..5f1d51ba84369 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_uuid.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_uuid.ts @@ -15,12 +15,10 @@ import { NonEmptyString } from './non_empty_string'; * - If null or undefined, then a default string uuid.v4() will be * created otherwise it will be checked just against an empty string */ -export const DefaultUuid = new t.Type( +export const DefaultUuid = new t.Type( 'DefaultUuid', t.string.is, (input, context): Either => input == null ? t.success(uuid.v4()) : NonEmptyString.validate(input, context), t.identity ); - -export type DefaultUuidC = typeof DefaultUuid; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_version_number.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_version_number.ts index 832c942291c32..bbba7c5b8f3bb 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_version_number.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_version_number.ts @@ -12,12 +12,10 @@ import { version, Version } from '../common/schemas'; * Types the DefaultVersionNumber as: * - If null or undefined, then a default of the number 1 will be used */ -export const DefaultVersionNumber = new t.Type( +export const DefaultVersionNumber = new t.Type( 'DefaultVersionNumber', version.is, (input, context): Either => input == null ? t.success(1) : version.validate(input, context), t.identity ); - -export type DefaultVersionNumberC = typeof DefaultVersionNumber; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists_default_array.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists_default_array.test.ts index 2268e47bd1149..59819947ddddf 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists_default_array.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists_default_array.test.ts @@ -9,7 +9,7 @@ import { left } from 'fp-ts/lib/Either'; import { foldLeftRight, getPaths } from '../../../test_utils'; -import { DefaultListArray, DefaultListArrayC } from './lists_default_array'; +import { DefaultListArray } from './lists_default_array'; import { getListArrayMock } from './lists.mock'; describe('lists_default_array', () => { @@ -51,7 +51,7 @@ describe('lists_default_array', () => { test('it should not validate an array of non accepted types', () => { // Terrible casting for purpose of tests - const payload = ([1] as unknown) as DefaultListArrayC; + const payload = [1] as unknown; const decoded = DefaultListArray.decode(payload); const message = pipe(decoded, foldLeftRight); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists_default_array.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists_default_array.ts index ac5666cad23a7..9260d7cbdb2a8 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists_default_array.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists_default_array.ts @@ -9,13 +9,11 @@ import { Either } from 'fp-ts/lib/Either'; import { ListArray, list } from './lists'; -export type DefaultListArrayC = t.Type; - /** * Types the DefaultListArray as: * - If null or undefined, then a default array of type list will be set */ -export const DefaultListArray: DefaultListArrayC = new t.Type( +export const DefaultListArray = new t.Type( 'DefaultListArray', t.array(list).is, (input, context): Either => diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/only_false_allowed.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/only_false_allowed.ts index b22562e2ab9dc..add926598a137 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/only_false_allowed.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/only_false_allowed.ts @@ -13,7 +13,7 @@ import { Either } from 'fp-ts/lib/Either'; * - If true is sent in then this will return an error * - If false is sent in then this will allow it only false */ -export const OnlyFalseAllowed = new t.Type( +export const OnlyFalseAllowed = new t.Type( 'DefaultBooleanTrue', t.boolean.is, (input, context): Either => { @@ -29,5 +29,3 @@ export const OnlyFalseAllowed = new t.Type( }, t.identity ); - -export type OnlyFalseAllowedC = typeof OnlyFalseAllowed; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/positive_integer.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/positive_integer.ts index 298487a3fae98..0dc6e2c700395 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/positive_integer.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/positive_integer.ts @@ -22,5 +22,3 @@ export const PositiveInteger = new t.Type( }, t.identity ); - -export type PositiveIntegerC = typeof PositiveInteger; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/positive_integer_greater_than_zero.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/positive_integer_greater_than_zero.ts index 8aeaf99a19eb1..2aa17c6c68955 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/positive_integer_greater_than_zero.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/positive_integer_greater_than_zero.ts @@ -22,5 +22,3 @@ export const PositiveIntegerGreaterThanZero = new t.Type( +export const ReferencesDefaultArray = new t.Type( 'referencesWithDefaultArray', t.array(t.string).is, (input, context): Either => input == null ? t.success([]) : t.array(t.string).validate(input, context), t.identity ); - -export type ReferencesDefaultArrayC = typeof ReferencesDefaultArray; 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 daa8589613daa..7171d3c6b815e 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 @@ -398,11 +398,11 @@ describe('Exception helpers', () => { title: 'OS', }, { - description: 'April 23rd 2020 @ 00:19:13', + description: 'April 20th 2020 @ 15:25:31', title: 'Date created', }, { - description: 'user_name', + description: 'some user', title: 'Created by', }, ]; @@ -417,11 +417,11 @@ describe('Exception helpers', () => { const result = getDescriptionListContent(payload); const expected: DescriptionListItem[] = [ { - description: 'April 23rd 2020 @ 00:19:13', + description: 'April 20th 2020 @ 15:25:31', title: 'Date created', }, { - description: 'user_name', + description: 'some user', title: 'Created by', }, { @@ -440,11 +440,11 @@ describe('Exception helpers', () => { const result = getDescriptionListContent(payload); const expected: DescriptionListItem[] = [ { - description: 'April 23rd 2020 @ 00:19:13', + description: 'April 20th 2020 @ 15:25:31', title: 'Date created', }, { - description: 'user_name', + description: 'some user', title: 'Created by', }, ]; @@ -520,12 +520,12 @@ describe('Exception helpers', () => { const expected = { _tags: ['endpoint', 'process', 'malware', 'os:linux'], comments: [], - description: 'This is a sample endpoint type exception', + description: 'some description', entries: ENTRIES, id: '1', item_id: 'endpoint_list_item', meta: {}, - name: 'Sample Endpoint Exception List', + name: 'some name', namespace_type: 'single', tags: ['user added string for a tag', 'malware'], type: 'simple', diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.test.tsx index f5b34b7838d25..4fc744c2c9d01 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.test.tsx @@ -179,7 +179,7 @@ describe('ExceptionDetails', () => { expect(wrapper.find('EuiDescriptionListTitle').at(1).text()).toEqual('Date created'); expect(wrapper.find('EuiDescriptionListDescription').at(1).text()).toEqual( - 'April 23rd 2020 @ 00:19:13' + 'April 20th 2020 @ 15:25:31' ); }); @@ -196,7 +196,7 @@ describe('ExceptionDetails', () => { ); expect(wrapper.find('EuiDescriptionListTitle').at(2).text()).toEqual('Created by'); - expect(wrapper.find('EuiDescriptionListDescription').at(2).text()).toEqual('user_name'); + expect(wrapper.find('EuiDescriptionListDescription').at(2).text()).toEqual('some user'); }); test('it renders the description if one is included on the exception item', () => { @@ -212,8 +212,6 @@ describe('ExceptionDetails', () => { ); expect(wrapper.find('EuiDescriptionListTitle').at(3).text()).toEqual('Comment'); - expect(wrapper.find('EuiDescriptionListDescription').at(3).text()).toEqual( - 'This is a sample endpoint type exception' - ); + expect(wrapper.find('EuiDescriptionListDescription').at(3).text()).toEqual('some description'); }); }); From 8a4daffcfdb90b8ff776ea051266516441c6fda2 Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Mon, 20 Jul 2020 11:00:59 -0600 Subject: [PATCH 44/45] [SIEM][Detection Engine][Lists] Adds list permissions (#72335) ## Summary * Adds list permissions as a feature control to SIEM. * Separates the controls between two, one of which is `access:lists-all` and the other is `access:lists-read` * Grants SIEM the ability to utilize both depending on which feature mode the space is in. --- .../routes/create_endpoint_list_item_route.ts | 2 +- .../routes/create_endpoint_list_route.ts | 2 +- .../routes/create_exception_list_item_route.ts | 2 +- .../routes/create_exception_list_route.ts | 2 +- .../server/routes/create_list_index_route.ts | 2 +- .../server/routes/create_list_item_route.ts | 2 +- .../lists/server/routes/create_list_route.ts | 2 +- .../routes/delete_endpoint_list_item_route.ts | 2 +- .../routes/delete_exception_list_item_route.ts | 2 +- .../routes/delete_exception_list_route.ts | 2 +- .../server/routes/delete_list_index_route.ts | 2 +- .../server/routes/delete_list_item_route.ts | 2 +- .../lists/server/routes/delete_list_route.ts | 2 +- .../server/routes/export_list_item_route.ts | 2 +- .../routes/find_endpoint_list_item_route.ts | 2 +- .../routes/find_exception_list_item_route.ts | 2 +- .../server/routes/find_exception_list_route.ts | 2 +- .../server/routes/find_list_item_route.ts | 2 +- .../lists/server/routes/find_list_route.ts | 2 +- .../server/routes/import_list_item_route.ts | 2 +- .../server/routes/patch_list_item_route.ts | 2 +- .../lists/server/routes/patch_list_route.ts | 2 +- .../routes/read_endpoint_list_item_route.ts | 2 +- .../routes/read_exception_list_item_route.ts | 2 +- .../server/routes/read_exception_list_route.ts | 2 +- .../server/routes/read_list_index_route.ts | 2 +- .../server/routes/read_list_item_route.ts | 2 +- .../lists/server/routes/read_list_route.ts | 2 +- .../server/routes/read_privileges_route.ts | 2 +- .../routes/update_endpoint_list_item_route.ts | 2 +- .../routes/update_exception_list_item_route.ts | 2 +- .../routes/update_exception_list_route.ts | 2 +- .../server/routes/update_list_item_route.ts | 2 +- .../lists/server/routes/update_list_route.ts | 2 +- .../plugins/security_solution/server/plugin.ts | 18 ++++++++++++++++-- 35 files changed, 50 insertions(+), 36 deletions(-) diff --git a/x-pack/plugins/lists/server/routes/create_endpoint_list_item_route.ts b/x-pack/plugins/lists/server/routes/create_endpoint_list_item_route.ts index b6eacc3b7dd04..5ff2a9d9df9f4 100644 --- a/x-pack/plugins/lists/server/routes/create_endpoint_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/create_endpoint_list_item_route.ts @@ -21,7 +21,7 @@ export const createEndpointListItemRoute = (router: IRouter): void => { router.post( { options: { - tags: ['access:lists'], + tags: ['access:lists-all'], }, path: ENDPOINT_LIST_ITEM_URL, validate: { diff --git a/x-pack/plugins/lists/server/routes/create_endpoint_list_route.ts b/x-pack/plugins/lists/server/routes/create_endpoint_list_route.ts index cac69ce65623f..b1e589be67cd1 100644 --- a/x-pack/plugins/lists/server/routes/create_endpoint_list_route.ts +++ b/x-pack/plugins/lists/server/routes/create_endpoint_list_route.ts @@ -26,7 +26,7 @@ export const createEndpointListRoute = (router: IRouter): void => { router.post( { options: { - tags: ['access:lists'], + tags: ['access:lists-all'], }, path: ENDPOINT_LIST_URL, validate: false, diff --git a/x-pack/plugins/lists/server/routes/create_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/create_exception_list_item_route.ts index c331eeb4bd2d0..e4885c7393bd4 100644 --- a/x-pack/plugins/lists/server/routes/create_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/create_exception_list_item_route.ts @@ -22,7 +22,7 @@ export const createExceptionListItemRoute = (router: IRouter): void => { router.post( { options: { - tags: ['access:lists'], + tags: ['access:lists-all'], }, path: EXCEPTION_LIST_ITEM_URL, validate: { diff --git a/x-pack/plugins/lists/server/routes/create_exception_list_route.ts b/x-pack/plugins/lists/server/routes/create_exception_list_route.ts index bd29a65c9450a..897d82d6a9ba0 100644 --- a/x-pack/plugins/lists/server/routes/create_exception_list_route.ts +++ b/x-pack/plugins/lists/server/routes/create_exception_list_route.ts @@ -21,7 +21,7 @@ export const createExceptionListRoute = (router: IRouter): void => { router.post( { options: { - tags: ['access:lists'], + tags: ['access:lists-all'], }, path: EXCEPTION_LIST_URL, validate: { diff --git a/x-pack/plugins/lists/server/routes/create_list_index_route.ts b/x-pack/plugins/lists/server/routes/create_list_index_route.ts index 5ec2b36da61b0..1bffdd6bd5b5f 100644 --- a/x-pack/plugins/lists/server/routes/create_list_index_route.ts +++ b/x-pack/plugins/lists/server/routes/create_list_index_route.ts @@ -17,7 +17,7 @@ export const createListIndexRoute = (router: IRouter): void => { router.post( { options: { - tags: ['access:lists'], + tags: ['access:lists-all'], }, path: LIST_INDEX, validate: false, diff --git a/x-pack/plugins/lists/server/routes/create_list_item_route.ts b/x-pack/plugins/lists/server/routes/create_list_item_route.ts index 8ac5db3c7fd1c..656d6af2c6c9a 100644 --- a/x-pack/plugins/lists/server/routes/create_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/create_list_item_route.ts @@ -17,7 +17,7 @@ export const createListItemRoute = (router: IRouter): void => { router.post( { options: { - tags: ['access:lists'], + tags: ['access:lists-all'], }, path: LIST_ITEM_URL, validate: { diff --git a/x-pack/plugins/lists/server/routes/create_list_route.ts b/x-pack/plugins/lists/server/routes/create_list_route.ts index eee7517523b0f..ff041699054c9 100644 --- a/x-pack/plugins/lists/server/routes/create_list_route.ts +++ b/x-pack/plugins/lists/server/routes/create_list_route.ts @@ -17,7 +17,7 @@ export const createListRoute = (router: IRouter): void => { router.post( { options: { - tags: ['access:lists'], + tags: ['access:lists-all'], }, path: LIST_URL, validate: { diff --git a/x-pack/plugins/lists/server/routes/delete_endpoint_list_item_route.ts b/x-pack/plugins/lists/server/routes/delete_endpoint_list_item_route.ts index b8946c542b27e..2d5028bd9525a 100644 --- a/x-pack/plugins/lists/server/routes/delete_endpoint_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/delete_endpoint_list_item_route.ts @@ -21,7 +21,7 @@ export const deleteEndpointListItemRoute = (router: IRouter): void => { router.delete( { options: { - tags: ['access:lists'], + tags: ['access:lists-all'], }, path: ENDPOINT_LIST_ITEM_URL, validate: { diff --git a/x-pack/plugins/lists/server/routes/delete_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/delete_exception_list_item_route.ts index f363252dada50..06ff051925407 100644 --- a/x-pack/plugins/lists/server/routes/delete_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/delete_exception_list_item_route.ts @@ -21,7 +21,7 @@ export const deleteExceptionListItemRoute = (router: IRouter): void => { router.delete( { options: { - tags: ['access:lists'], + tags: ['access:lists-all'], }, path: EXCEPTION_LIST_ITEM_URL, validate: { diff --git a/x-pack/plugins/lists/server/routes/delete_exception_list_route.ts b/x-pack/plugins/lists/server/routes/delete_exception_list_route.ts index b1bf705dcc5f6..f2bf517f55ae3 100644 --- a/x-pack/plugins/lists/server/routes/delete_exception_list_route.ts +++ b/x-pack/plugins/lists/server/routes/delete_exception_list_route.ts @@ -21,7 +21,7 @@ export const deleteExceptionListRoute = (router: IRouter): void => { router.delete( { options: { - tags: ['access:lists'], + tags: ['access:lists-all'], }, path: EXCEPTION_LIST_URL, validate: { diff --git a/x-pack/plugins/lists/server/routes/delete_list_index_route.ts b/x-pack/plugins/lists/server/routes/delete_list_index_route.ts index cb2e16b3602a7..be58d8aeed17d 100644 --- a/x-pack/plugins/lists/server/routes/delete_list_index_route.ts +++ b/x-pack/plugins/lists/server/routes/delete_list_index_route.ts @@ -33,7 +33,7 @@ export const deleteListIndexRoute = (router: IRouter): void => { router.delete( { options: { - tags: ['access:lists'], + tags: ['access:lists-all'], }, path: LIST_INDEX, validate: false, diff --git a/x-pack/plugins/lists/server/routes/delete_list_item_route.ts b/x-pack/plugins/lists/server/routes/delete_list_item_route.ts index bb278ba436725..50313cd1294ae 100644 --- a/x-pack/plugins/lists/server/routes/delete_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/delete_list_item_route.ts @@ -17,7 +17,7 @@ export const deleteListItemRoute = (router: IRouter): void => { router.delete( { options: { - tags: ['access:lists'], + tags: ['access:lists-all'], }, path: LIST_ITEM_URL, validate: { diff --git a/x-pack/plugins/lists/server/routes/delete_list_route.ts b/x-pack/plugins/lists/server/routes/delete_list_route.ts index 600e4b00c29ca..4eeb6d8f126ad 100644 --- a/x-pack/plugins/lists/server/routes/delete_list_route.ts +++ b/x-pack/plugins/lists/server/routes/delete_list_route.ts @@ -17,7 +17,7 @@ export const deleteListRoute = (router: IRouter): void => { router.delete( { options: { - tags: ['access:lists'], + tags: ['access:lists-all'], }, path: LIST_URL, validate: { diff --git a/x-pack/plugins/lists/server/routes/export_list_item_route.ts b/x-pack/plugins/lists/server/routes/export_list_item_route.ts index 8148c9b1ed824..98167931c4346 100644 --- a/x-pack/plugins/lists/server/routes/export_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/export_list_item_route.ts @@ -18,7 +18,7 @@ export const exportListItemRoute = (router: IRouter): void => { router.post( { options: { - tags: ['access:lists'], + tags: ['access:lists-read'], }, path: `${LIST_ITEM_URL}/_export`, validate: { diff --git a/x-pack/plugins/lists/server/routes/find_endpoint_list_item_route.ts b/x-pack/plugins/lists/server/routes/find_endpoint_list_item_route.ts index 7374ff7dc92ea..9f83761cc501a 100644 --- a/x-pack/plugins/lists/server/routes/find_endpoint_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/find_endpoint_list_item_route.ts @@ -21,7 +21,7 @@ export const findEndpointListItemRoute = (router: IRouter): void => { router.get( { options: { - tags: ['access:lists'], + tags: ['access:lists-read'], }, path: `${ENDPOINT_LIST_ITEM_URL}/_find`, validate: { diff --git a/x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts index a318d653450c7..270aad85796b2 100644 --- a/x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts @@ -21,7 +21,7 @@ export const findExceptionListItemRoute = (router: IRouter): void => { router.get( { options: { - tags: ['access:lists'], + tags: ['access:lists-read'], }, path: `${EXCEPTION_LIST_ITEM_URL}/_find`, validate: { diff --git a/x-pack/plugins/lists/server/routes/find_exception_list_route.ts b/x-pack/plugins/lists/server/routes/find_exception_list_route.ts index 97e1de834cd37..c5cae7a1e0bb8 100644 --- a/x-pack/plugins/lists/server/routes/find_exception_list_route.ts +++ b/x-pack/plugins/lists/server/routes/find_exception_list_route.ts @@ -21,7 +21,7 @@ export const findExceptionListRoute = (router: IRouter): void => { router.get( { options: { - tags: ['access:lists'], + tags: ['access:lists-read'], }, path: `${EXCEPTION_LIST_URL}/_find`, validate: { diff --git a/x-pack/plugins/lists/server/routes/find_list_item_route.ts b/x-pack/plugins/lists/server/routes/find_list_item_route.ts index 8a0e06aa0c7d8..533dc74aa3694 100644 --- a/x-pack/plugins/lists/server/routes/find_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/find_list_item_route.ts @@ -22,7 +22,7 @@ export const findListItemRoute = (router: IRouter): void => { router.get( { options: { - tags: ['access:lists'], + tags: ['access:lists-read'], }, path: `${LIST_ITEM_URL}/_find`, validate: { diff --git a/x-pack/plugins/lists/server/routes/find_list_route.ts b/x-pack/plugins/lists/server/routes/find_list_route.ts index 2fa43c6368b5c..268eb36a5e26e 100644 --- a/x-pack/plugins/lists/server/routes/find_list_route.ts +++ b/x-pack/plugins/lists/server/routes/find_list_route.ts @@ -18,7 +18,7 @@ export const findListRoute = (router: IRouter): void => { router.get( { options: { - tags: ['access:lists'], + tags: ['access:lists-read'], }, path: `${LIST_URL}/_find`, validate: { diff --git a/x-pack/plugins/lists/server/routes/import_list_item_route.ts b/x-pack/plugins/lists/server/routes/import_list_item_route.ts index 2e629d7516dd1..5e88ca0f2569a 100644 --- a/x-pack/plugins/lists/server/routes/import_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/import_list_item_route.ts @@ -26,7 +26,7 @@ export const importListItemRoute = (router: IRouter, config: ConfigType): void = maxBytes: config.maxImportPayloadBytes, parse: false, }, - tags: ['access:lists'], + tags: ['access:lists-all'], }, path: `${LIST_ITEM_URL}/_import`, validate: { diff --git a/x-pack/plugins/lists/server/routes/patch_list_item_route.ts b/x-pack/plugins/lists/server/routes/patch_list_item_route.ts index 9a74beb45bafd..d975e80079ab7 100644 --- a/x-pack/plugins/lists/server/routes/patch_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/patch_list_item_route.ts @@ -17,7 +17,7 @@ export const patchListItemRoute = (router: IRouter): void => { router.patch( { options: { - tags: ['access:lists'], + tags: ['access:lists-all'], }, path: LIST_ITEM_URL, validate: { diff --git a/x-pack/plugins/lists/server/routes/patch_list_route.ts b/x-pack/plugins/lists/server/routes/patch_list_route.ts index 06a76559dee9a..681581c6ff6bd 100644 --- a/x-pack/plugins/lists/server/routes/patch_list_route.ts +++ b/x-pack/plugins/lists/server/routes/patch_list_route.ts @@ -17,7 +17,7 @@ export const patchListRoute = (router: IRouter): void => { router.patch( { options: { - tags: ['access:lists'], + tags: ['access:lists-all'], }, path: LIST_URL, validate: { diff --git a/x-pack/plugins/lists/server/routes/read_endpoint_list_item_route.ts b/x-pack/plugins/lists/server/routes/read_endpoint_list_item_route.ts index 5e7ed901bf0cb..fd932746ce990 100644 --- a/x-pack/plugins/lists/server/routes/read_endpoint_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/read_endpoint_list_item_route.ts @@ -21,7 +21,7 @@ export const readEndpointListItemRoute = (router: IRouter): void => { router.get( { options: { - tags: ['access:lists'], + tags: ['access:lists-read'], }, path: ENDPOINT_LIST_ITEM_URL, validate: { diff --git a/x-pack/plugins/lists/server/routes/read_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/read_exception_list_item_route.ts index c4e969b27fcf4..fe8256fbda5cd 100644 --- a/x-pack/plugins/lists/server/routes/read_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/read_exception_list_item_route.ts @@ -21,7 +21,7 @@ export const readExceptionListItemRoute = (router: IRouter): void => { router.get( { options: { - tags: ['access:lists'], + tags: ['access:lists-read'], }, path: EXCEPTION_LIST_ITEM_URL, validate: { diff --git a/x-pack/plugins/lists/server/routes/read_exception_list_route.ts b/x-pack/plugins/lists/server/routes/read_exception_list_route.ts index 6cb91c10aea55..0512876d298d4 100644 --- a/x-pack/plugins/lists/server/routes/read_exception_list_route.ts +++ b/x-pack/plugins/lists/server/routes/read_exception_list_route.ts @@ -21,7 +21,7 @@ export const readExceptionListRoute = (router: IRouter): void => { router.get( { options: { - tags: ['access:lists'], + tags: ['access:lists-read'], }, path: EXCEPTION_LIST_URL, validate: { diff --git a/x-pack/plugins/lists/server/routes/read_list_index_route.ts b/x-pack/plugins/lists/server/routes/read_list_index_route.ts index 4664bed3e7a8b..87a4d85e0d254 100644 --- a/x-pack/plugins/lists/server/routes/read_list_index_route.ts +++ b/x-pack/plugins/lists/server/routes/read_list_index_route.ts @@ -17,7 +17,7 @@ export const readListIndexRoute = (router: IRouter): void => { router.get( { options: { - tags: ['access:lists'], + tags: ['access:lists-read'], }, path: LIST_INDEX, validate: false, diff --git a/x-pack/plugins/lists/server/routes/read_list_item_route.ts b/x-pack/plugins/lists/server/routes/read_list_item_route.ts index 24011d3b50d27..b7cf2b9f7123b 100644 --- a/x-pack/plugins/lists/server/routes/read_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/read_list_item_route.ts @@ -17,7 +17,7 @@ export const readListItemRoute = (router: IRouter): void => { router.get( { options: { - tags: ['access:lists'], + tags: ['access:lists-read'], }, path: LIST_ITEM_URL, validate: { diff --git a/x-pack/plugins/lists/server/routes/read_list_route.ts b/x-pack/plugins/lists/server/routes/read_list_route.ts index 34924b70fd4df..4bce09ecd3bde 100644 --- a/x-pack/plugins/lists/server/routes/read_list_route.ts +++ b/x-pack/plugins/lists/server/routes/read_list_route.ts @@ -17,7 +17,7 @@ export const readListRoute = (router: IRouter): void => { router.get( { options: { - tags: ['access:lists'], + tags: ['access:lists-read'], }, path: LIST_URL, validate: { diff --git a/x-pack/plugins/lists/server/routes/read_privileges_route.ts b/x-pack/plugins/lists/server/routes/read_privileges_route.ts index 892b6406a28ec..a4ec878613608 100644 --- a/x-pack/plugins/lists/server/routes/read_privileges_route.ts +++ b/x-pack/plugins/lists/server/routes/read_privileges_route.ts @@ -20,7 +20,7 @@ export const readPrivilegesRoute = ( router.get( { options: { - tags: ['access:lists'], + tags: ['access:lists-read'], }, path: LIST_PRIVILEGES_URL, validate: false, diff --git a/x-pack/plugins/lists/server/routes/update_endpoint_list_item_route.ts b/x-pack/plugins/lists/server/routes/update_endpoint_list_item_route.ts index 8415c64633a06..f717dc0fb3392 100644 --- a/x-pack/plugins/lists/server/routes/update_endpoint_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/update_endpoint_list_item_route.ts @@ -21,7 +21,7 @@ export const updateEndpointListItemRoute = (router: IRouter): void => { router.put( { options: { - tags: ['access:lists'], + tags: ['access:lists-all'], }, path: ENDPOINT_LIST_ITEM_URL, validate: { diff --git a/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts index 2aa1e016d51ed..293435b3f6202 100644 --- a/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts @@ -21,7 +21,7 @@ export const updateExceptionListItemRoute = (router: IRouter): void => { router.put( { options: { - tags: ['access:lists'], + tags: ['access:lists-all'], }, path: EXCEPTION_LIST_ITEM_URL, validate: { diff --git a/x-pack/plugins/lists/server/routes/update_exception_list_route.ts b/x-pack/plugins/lists/server/routes/update_exception_list_route.ts index 331ec064fa663..403a9f6db934f 100644 --- a/x-pack/plugins/lists/server/routes/update_exception_list_route.ts +++ b/x-pack/plugins/lists/server/routes/update_exception_list_route.ts @@ -21,7 +21,7 @@ export const updateExceptionListRoute = (router: IRouter): void => { router.put( { options: { - tags: ['access:lists'], + tags: ['access:lists-all'], }, path: EXCEPTION_LIST_URL, validate: { diff --git a/x-pack/plugins/lists/server/routes/update_list_item_route.ts b/x-pack/plugins/lists/server/routes/update_list_item_route.ts index 0f5d11afcda09..d479bc63b64bd 100644 --- a/x-pack/plugins/lists/server/routes/update_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/update_list_item_route.ts @@ -17,7 +17,7 @@ export const updateListItemRoute = (router: IRouter): void => { router.put( { options: { - tags: ['access:lists'], + tags: ['access:lists-all'], }, path: LIST_ITEM_URL, validate: { diff --git a/x-pack/plugins/lists/server/routes/update_list_route.ts b/x-pack/plugins/lists/server/routes/update_list_route.ts index 2fae910c1b398..78aed23db13fc 100644 --- a/x-pack/plugins/lists/server/routes/update_list_route.ts +++ b/x-pack/plugins/lists/server/routes/update_list_route.ts @@ -17,7 +17,7 @@ export const updateListRoute = (router: IRouter): void => { router.put( { options: { - tags: ['access:lists'], + tags: ['access:lists-all'], }, path: LIST_URL, validate: { diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 17192057d2ad3..22b55c64a1657 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -167,7 +167,14 @@ export class Plugin implements IPlugin Date: Mon, 20 Jul 2020 18:12:22 +0100 Subject: [PATCH 45/45] fixed alerts test --- x-pack/plugins/alerts/server/alerts_client.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/plugins/alerts/server/alerts_client.test.ts b/x-pack/plugins/alerts/server/alerts_client.test.ts index 6de9e74df47c3..c25e040ad09ce 100644 --- a/x-pack/plugins/alerts/server/alerts_client.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client.test.ts @@ -1367,7 +1367,6 @@ describe('disable()', () => { await alertsClient.disable({ id: '1' }); expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'disable'); - expect(actionsAuthorization.ensureAuthorized).toHaveBeenCalledWith('execute'); }); test('throws when user is not authorised to disable this type of alert', async () => {