diff --git a/.github/workflows/pr-project-assigner.yml b/.github/workflows/pr-project-assigner.yml index 708c9efea404b..ccaa5b11aa80c 100644 --- a/.github/workflows/pr-project-assigner.yml +++ b/.github/workflows/pr-project-assigner.yml @@ -8,7 +8,7 @@ jobs: name: Assign a PR to project based on label steps: - name: Assign to project - uses: elastic/github-actions/project-assigner@v1.0.0 + uses: elastic/github-actions/project-assigner@v1.0.1 id: project_assigner with: issue-mappings: | diff --git a/.github/workflows/project-assigner.yml b/.github/workflows/project-assigner.yml index aec3bf88f0ee2..737da4f7fe371 100644 --- a/.github/workflows/project-assigner.yml +++ b/.github/workflows/project-assigner.yml @@ -8,7 +8,7 @@ jobs: name: Assign issue or PR to project based on label steps: - name: Assign to project - uses: elastic/github-actions/project-assigner@v1.0.0 + uses: elastic/github-actions/project-assigner@v1.0.1 id: project_assigner with: issue-mappings: '[{"label": "Team:AppArch", "projectName": "kibana-app-arch", "columnId": 6173895}, {"label": "Feature:Lens", "projectName": "Lens", "columnId": 6219363}, {"label": "Team:Canvas", "projectName": "canvas", "columnId": 6187593}]' diff --git a/src/core/public/application/ui/app_container.test.tsx b/src/core/public/application/ui/app_container.test.tsx new file mode 100644 index 0000000000000..a46243a2da493 --- /dev/null +++ b/src/core/public/application/ui/app_container.test.tsx @@ -0,0 +1,88 @@ +/* + * 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 React from 'react'; +import { mount } from 'enzyme'; + +import { AppContainer } from './app_container'; +import { Mounter, AppMountParameters, AppStatus } from '../types'; + +describe('AppContainer', () => { + const appId = 'someApp'; + const setAppLeaveHandler = jest.fn(); + + const flushPromises = async () => { + await new Promise(async resolve => { + setImmediate(() => resolve()); + }); + }; + + const createResolver = (): [Promise, () => void] => { + let resolve: () => void | undefined; + const promise = new Promise(r => { + resolve = r; + }); + return [promise, resolve!]; + }; + + const createMounter = (promise: Promise): Mounter => ({ + appBasePath: '/base-path', + appRoute: '/some-route', + unmountBeforeMounting: false, + mount: async ({ element }: AppMountParameters) => { + await promise; + const container = document.createElement('div'); + container.innerHTML = 'some-content'; + element.appendChild(container); + return () => container.remove(); + }, + }); + + it('should hide the "not found" page before mounting the route', async () => { + const [waitPromise, resolvePromise] = createResolver(); + const mounter = createMounter(waitPromise); + + const wrapper = mount( + + ); + + expect(wrapper.text()).toContain('Application Not Found'); + + wrapper.setProps({ + appId, + setAppLeaveHandler, + mounter, + appStatus: AppStatus.accessible, + }); + wrapper.update(); + + expect(wrapper.text()).toEqual(''); + + resolvePromise(); + await flushPromises(); + wrapper.update(); + + expect(wrapper.text()).toContain('some-content'); + }); +}); diff --git a/src/core/public/application/ui/app_container.tsx b/src/core/public/application/ui/app_container.tsx index 66c837d238276..885157843e7df 100644 --- a/src/core/public/application/ui/app_container.tsx +++ b/src/core/public/application/ui/app_container.tsx @@ -53,25 +53,27 @@ export const AppContainer: FunctionComponent = ({ unmountRef.current = null; } }; - const mount = async () => { - if (!mounter || appStatus !== AppStatus.accessible) { - return setAppNotFound(true); - } - if (mounter.unmountBeforeMounting) { - unmount(); - } + if (!mounter || appStatus !== AppStatus.accessible) { + return setAppNotFound(true); + } + setAppNotFound(false); + if (mounter.unmountBeforeMounting) { + unmount(); + } + + const mount = async () => { unmountRef.current = (await mounter.mount({ appBasePath: mounter.appBasePath, element: elementRef.current!, onAppLeave: handler => setAppLeaveHandler(appId, handler), })) || null; - setAppNotFound(false); }; mount(); + return unmount; }, [appId, appStatus, mounter, setAppLeaveHandler]); diff --git a/src/plugins/console/server/lib/elasticsearch_proxy_config.ts b/src/plugins/console/server/lib/elasticsearch_proxy_config.ts index 901d726ac51d8..28a971794d403 100644 --- a/src/plugins/console/server/lib/elasticsearch_proxy_config.ts +++ b/src/plugins/console/server/lib/elasticsearch_proxy_config.ts @@ -21,9 +21,10 @@ import _ from 'lodash'; import http from 'http'; import https from 'https'; import url from 'url'; -import { Duration } from 'moment'; -const createAgent = (legacyConfig: any) => { +import { ESConfigForProxy } from '../types'; + +const createAgent = (legacyConfig: ESConfigForProxy) => { const target = url.parse(_.head(legacyConfig.hosts)); if (!/^https/.test(target.protocol || '')) return new http.Agent(); @@ -59,7 +60,7 @@ const createAgent = (legacyConfig: any) => { return new https.Agent(agentOptions); }; -export const getElasticsearchProxyConfig = (legacyConfig: { requestTimeout: Duration }) => { +export const getElasticsearchProxyConfig = (legacyConfig: ESConfigForProxy) => { return { timeout: legacyConfig.requestTimeout.asMilliseconds(), agent: createAgent(legacyConfig), diff --git a/src/plugins/console/server/plugin.ts b/src/plugins/console/server/plugin.ts index c8ef84aee3b61..65647bd5acb7c 100644 --- a/src/plugins/console/server/plugin.ts +++ b/src/plugins/console/server/plugin.ts @@ -60,9 +60,7 @@ export class ConsoleServerPlugin implements Plugin { const legacyConfig = readLegacyEsConfig(); return { ...elasticsearch, - hosts: legacyConfig.hosts, - requestHeadersWhitelist: legacyConfig.requestHeadersWhitelist, - customHeaders: legacyConfig.customHeaders, + ...legacyConfig, }; }, pathFilters: proxyPathFilters, diff --git a/src/plugins/console/server/types.ts b/src/plugins/console/server/types.ts index 60ce56ad39fcd..adafcd4d30526 100644 --- a/src/plugins/console/server/types.ts +++ b/src/plugins/console/server/types.ts @@ -31,4 +31,12 @@ export interface ESConfigForProxy { requestHeadersWhitelist: string[]; customHeaders: Record; requestTimeout: Duration; + ssl?: { + verificationMode: 'none' | 'certificate' | 'full'; + certificateAuthorities: string[] | string; + alwaysPresentCertificate: boolean; + certificate?: string; + key?: string; + keyPassphrase?: string; + }; } diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx new file mode 100644 index 0000000000000..378ad9509c217 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiTitle +} from '@elastic/eui'; +import cytoscape from 'cytoscape'; +import React from 'react'; +import { Buttons } from './Buttons'; +import { Info } from './Info'; +import { ServiceMetricList } from './ServiceMetricList'; + +const popoverMinWidth = 280; + +interface ContentsProps { + focusedServiceName?: string; + isService: boolean; + label: string; + onFocusClick: () => void; + selectedNodeData: cytoscape.NodeDataDefinition; + selectedNodeServiceName: string; +} + +export function Contents({ + selectedNodeData, + focusedServiceName, + isService, + label, + onFocusClick, + selectedNodeServiceName +}: ContentsProps) { + return ( + + + +

{label}

+
+ +
+ + {isService ? ( + + ) : ( + + )} + + {isService && ( + + )} +
+ ); +} diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Info.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Info.tsx index 1c5443e404f9b..d432119505382 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Info.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Info.tsx @@ -4,10 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; import { i18n } from '@kbn/i18n'; +import cytoscape from 'cytoscape'; +import React from 'react'; import styled from 'styled-components'; -import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; const ItemRow = styled.div` line-height: 2; @@ -19,8 +20,8 @@ const ItemTitle = styled.dt` const ItemDescription = styled.dd``; -interface InfoProps { - type: string; +interface InfoProps extends cytoscape.NodeDataDefinition { + type?: string; subtype?: string; } diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx new file mode 100644 index 0000000000000..b26488c5ef7de --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx @@ -0,0 +1,49 @@ +/* + * 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 { storiesOf } from '@storybook/react'; +import React from 'react'; +import { + ApmPluginContext, + ApmPluginContextValue +} from '../../../../context/ApmPluginContext'; +import { Contents } from './Contents'; + +const selectedNodeData = { + id: 'opbeans-node', + label: 'opbeans-node', + href: + '#/services/opbeans-node/service-map?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0', + agentName: 'nodejs', + type: 'service' +}; + +storiesOf('app/ServiceMap/Popover/Contents', module).add( + 'example', + () => { + return ( + + {}} + selectedNodeServiceName="opbeans-node" + /> + + ); + }, + { + info: { + propTablesExclude: [ApmPluginContext.Provider], + source: false + } + } +); diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/index.tsx index dfb78aaa0214c..e8e37cfdfb1f0 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/index.tsx @@ -4,27 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - EuiFlexGroup, - EuiFlexItem, - EuiHorizontalRule, - EuiPopover, - EuiTitle -} from '@elastic/eui'; +import { EuiPopover } from '@elastic/eui'; import cytoscape from 'cytoscape'; import React, { CSSProperties, + useCallback, useContext, useEffect, - useState, - useCallback + useRef, + useState } from 'react'; import { CytoscapeContext } from '../Cytoscape'; -import { Buttons } from './Buttons'; -import { Info } from './Info'; -import { ServiceMetricList } from './ServiceMetricList'; - -const popoverMinWidth = 280; +import { Contents } from './Contents'; interface PopoverProps { focusedServiceName?: string; @@ -35,56 +26,62 @@ export function Popover({ focusedServiceName }: PopoverProps) { const [selectedNode, setSelectedNode] = useState< cytoscape.NodeSingular | undefined >(undefined); - const onFocusClick = useCallback(() => setSelectedNode(undefined), [ + const deselect = useCallback(() => setSelectedNode(undefined), [ setSelectedNode ]); - - useEffect(() => { - const selectHandler: cytoscape.EventHandler = event => { - setSelectedNode(event.target); - }; - const unselectHandler: cytoscape.EventHandler = () => { - setSelectedNode(undefined); - }; - - if (cy) { - cy.on('select', 'node', selectHandler); - cy.on('unselect', 'node', unselectHandler); - cy.on('data viewport', unselectHandler); - } - - return () => { - if (cy) { - cy.removeListener('select', 'node', selectHandler); - cy.removeListener('unselect', 'node', unselectHandler); - cy.removeListener('data viewport', undefined, unselectHandler); - } - }; - }, [cy]); - const renderedHeight = selectedNode?.renderedHeight() ?? 0; const renderedWidth = selectedNode?.renderedWidth() ?? 0; const { x, y } = selectedNode?.renderedPosition() ?? { x: 0, y: 0 }; const isOpen = !!selectedNode; - const selectedNodeServiceName: string = selectedNode?.data('id'); const isService = selectedNode?.data('type') === 'service'; const triggerStyle: CSSProperties = { background: 'transparent', height: renderedHeight, position: 'absolute', - width: renderedWidth + width: renderedWidth, + border: '3px dotted red' }; - const trigger =
; - + const trigger =
; const zoom = cy?.zoom() ?? 1; const height = selectedNode?.height() ?? 0; - const translateY = y - (zoom + 1) * (height / 2); + const translateY = y - ((zoom + 1) * height) / 4; const popoverStyle: CSSProperties = { position: 'absolute', transform: `translate(${x}px, ${translateY}px)` }; - const data = selectedNode?.data() ?? {}; - const label = data.label || selectedNodeServiceName; + const selectedNodeData = selectedNode?.data() ?? {}; + const selectedNodeServiceName = selectedNodeData.id; + const label = selectedNodeData.label || selectedNodeServiceName; + const popoverRef = useRef(null); + + // Set up Cytoscape event handlers + useEffect(() => { + const selectHandler: cytoscape.EventHandler = event => { + setSelectedNode(event.target); + }; + + if (cy) { + cy.on('select', 'node', selectHandler); + cy.on('unselect', 'node', deselect); + cy.on('data viewport', deselect); + } + + return () => { + if (cy) { + cy.removeListener('select', 'node', selectHandler); + cy.removeListener('unselect', 'node', deselect); + cy.removeListener('data viewport', undefined, deselect); + } + }; + }, [cy, deselect]); + + // Handle positioning of popover. This makes it so the popover positions + // itself correctly and the arrows are always pointing to where they should. + useEffect(() => { + if (popoverRef.current) { + popoverRef.current.positionPopoverFluid(); + } + }, [popoverRef, x, y]); return ( {}} isOpen={isOpen} + ref={popoverRef} style={popoverStyle} > - - - -

{label}

-
- -
- - - {isService ? ( - - ) : ( - - )} - - {isService && ( - - )} -
+
); } diff --git a/x-pack/legacy/plugins/encrypted_saved_objects/index.ts b/x-pack/legacy/plugins/encrypted_saved_objects/index.ts index 85aa5c22135b6..69058a7a33f59 100644 --- a/x-pack/legacy/plugins/encrypted_saved_objects/index.ts +++ b/x-pack/legacy/plugins/encrypted_saved_objects/index.ts @@ -21,7 +21,9 @@ export const encryptedSavedObjects = (kibana: { // Some legacy plugins still use `enabled` config key, so we keep it here, but the rest of the // keys is handled by the New Platform plugin. config: (Joi: Root) => - Joi.object({ enabled: Joi.boolean().default(true) }) + Joi.object({ + enabled: Joi.boolean().default(true), + }) .unknown(true) .default(), diff --git a/x-pack/legacy/plugins/siem/index.ts b/x-pack/legacy/plugins/siem/index.ts index cd9b7f59226b0..0a3e447ac64a1 100644 --- a/x-pack/legacy/plugins/siem/index.ts +++ b/x-pack/legacy/plugins/siem/index.ts @@ -5,6 +5,7 @@ */ import { i18n } from '@kbn/i18n'; +import { get } from 'lodash/fp'; import { resolve } from 'path'; import { Server } from 'hapi'; import { Root } from 'joi'; @@ -155,6 +156,9 @@ export const siem = (kibana: any) => { const initializerContext = { ...coreContext, env } as PluginInitializerContext; const serverFacade = { config, + usingEphemeralEncryptionKey: + get('usingEphemeralEncryptionKey', newPlatform.setup.plugins.encryptedSavedObjects) ?? + false, plugins: { alerting: plugins.alerting, actions: newPlatform.start.plugins.actions, diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_pre_packaged_rules.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_pre_packaged_rules.tsx index 14d40f9ffbc37..d77d6283692a2 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_pre_packaged_rules.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_pre_packaged_rules.tsx @@ -29,6 +29,7 @@ interface UsePrePackagedRuleProps { hasIndexWrite: boolean | null; hasManageApiKey: boolean | null; isAuthenticated: boolean | null; + hasEncryptionKey: boolean | null; isSignalIndexExists: boolean | null; } @@ -38,6 +39,7 @@ interface UsePrePackagedRuleProps { * @param hasIndexWrite boolean * @param hasManageApiKey boolean * @param isAuthenticated boolean + * @param hasEncryptionKey boolean * @param isSignalIndexExists boolean * */ @@ -46,6 +48,7 @@ export const usePrePackagedRules = ({ hasIndexWrite, hasManageApiKey, isAuthenticated, + hasEncryptionKey, isSignalIndexExists, }: UsePrePackagedRuleProps): Return => { const [rulesStatus, setRuleStatus] = useState< @@ -117,6 +120,7 @@ export const usePrePackagedRules = ({ hasIndexWrite && hasManageApiKey && isAuthenticated && + hasEncryptionKey && isSignalIndexExists ) { setLoadingCreatePrePackagedRules(true); @@ -180,7 +184,14 @@ export const usePrePackagedRules = ({ isSubscribed = false; abortCtrl.abort(); }; - }, [canUserCRUD, hasIndexWrite, hasManageApiKey, isAuthenticated, isSignalIndexExists]); + }, [ + canUserCRUD, + hasIndexWrite, + hasManageApiKey, + isAuthenticated, + hasEncryptionKey, + isSignalIndexExists, + ]); return { loading, diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/types.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/types.ts index ea4860dafd40f..752de13567e5c 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/types.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/types.ts @@ -97,4 +97,5 @@ export interface Privilege { }; }; is_authenticated: boolean; + has_encryption_key: boolean; } diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.tsx index b93009c8ce2c2..55f3386b503d8 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.tsx @@ -14,6 +14,7 @@ import * as i18n from './translations'; interface Return { loading: boolean; isAuthenticated: boolean | null; + hasEncryptionKey: boolean | null; hasIndexManage: boolean | null; hasManageApiKey: boolean | null; hasIndexWrite: boolean | null; @@ -25,9 +26,17 @@ interface Return { export const usePrivilegeUser = (): Return => { const [loading, setLoading] = useState(true); const [privilegeUser, setPrivilegeUser] = useState< - Pick + Pick< + Return, + | 'isAuthenticated' + | 'hasEncryptionKey' + | 'hasIndexManage' + | 'hasManageApiKey' + | 'hasIndexWrite' + > >({ isAuthenticated: null, + hasEncryptionKey: null, hasIndexManage: null, hasManageApiKey: null, hasIndexWrite: null, @@ -50,6 +59,7 @@ export const usePrivilegeUser = (): Return => { const indexName = Object.keys(privilege.index)[0]; setPrivilegeUser({ isAuthenticated: privilege.is_authenticated, + hasEncryptionKey: privilege.has_encryption_key, hasIndexManage: privilege.index[indexName].manage, hasIndexWrite: privilege.index[indexName].create || @@ -67,6 +77,7 @@ export const usePrivilegeUser = (): Return => { if (isSubscribed) { setPrivilegeUser({ isAuthenticated: false, + hasEncryptionKey: false, hasIndexManage: false, hasManageApiKey: false, hasIndexWrite: false, diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_api_integration_callout/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_api_integration_callout/index.tsx new file mode 100644 index 0000000000000..2d517717ac59d --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_api_integration_callout/index.tsx @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiCallOut, EuiButton } from '@elastic/eui'; +import React, { memo, useCallback, useState } from 'react'; + +import * as i18n from './translations'; + +const NoApiIntegrationKeyCallOutComponent = () => { + const [showCallOut, setShowCallOut] = useState(true); + const handleCallOut = useCallback(() => setShowCallOut(false), [setShowCallOut]); + + return showCallOut ? ( + +

{i18n.NO_API_INTEGRATION_KEY_CALLOUT_MSG}

+ + {i18n.DISMISS_CALLOUT} + +
+ ) : null; +}; + +export const NoApiIntegrationKeyCallOut = memo(NoApiIntegrationKeyCallOutComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_api_integration_callout/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_api_integration_callout/translations.ts new file mode 100644 index 0000000000000..84804af8840f9 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_api_integration_callout/translations.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const NO_API_INTEGRATION_KEY_CALLOUT_TITLE = i18n.translate( + 'xpack.siem.detectionEngine.noApiIntegrationKeyCallOutTitle', + { + defaultMessage: 'API integration key required', + } +); + +export const NO_API_INTEGRATION_KEY_CALLOUT_MSG = i18n.translate( + 'xpack.siem.detectionEngine.noApiIntegrationKeyCallOutMsg', + { + defaultMessage: `A new encryption key is generated for saved objects each time you start Kibana. Without a persistent key, you cannot delete or modify rules after Kibana restarts. To set a persistent key, add the xpack.encryptedSavedObjects.encryptionKey setting with any text value of 32 or more characters to the kibana.yml file.`, + } +); + +export const DISMISS_CALLOUT = i18n.translate( + 'xpack.siem.detectionEngine.dismissNoApiIntegrationKeyButton', + { + defaultMessage: 'Dismiss', + } +); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/actions.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/actions.tsx index a33efeda2196b..003d2baa53dbc 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/actions.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/actions.tsx @@ -5,7 +5,7 @@ */ import dateMath from '@elastic/datemath'; -import { getOr } from 'lodash/fp'; +import { getOr, isEmpty } from 'lodash/fp'; import moment from 'moment'; import { updateSignalStatus } from '../../../../containers/detection_engine/signals/api'; @@ -78,6 +78,7 @@ export const sendSignalToTimelineAction = async ({ ecsData, updateTimelineIsLoading, }: SendSignalToTimelineActionProps) => { + let openSignalInBasicTimeline = true; const timelineId = ecsData.signal?.rule?.timeline_id != null ? ecsData.signal?.rule?.timeline_id[0] : ''; @@ -105,52 +106,57 @@ export const sendSignalToTimelineAction = async ({ id: timelineId, }, }); - const timelineTemplate: TimelineResult = omitTypenameInTimeline( getOr({}, 'data.getOneTimeline', responseTimeline) ); - const { timeline } = formatTimelineResultToModel(timelineTemplate, true); - const query = replaceTemplateFieldFromQuery( - timeline.kqlQuery?.filterQuery?.kuery?.expression ?? '', - ecsData - ); - const filters = replaceTemplateFieldFromMatchFilters(timeline.filters ?? [], ecsData); - const dataProviders = replaceTemplateFieldFromDataProviders( - timeline.dataProviders ?? [], - ecsData - ); - createTimeline({ - from, - timeline: { - ...timeline, - dataProviders, - eventType: 'all', - filters, - dateRange: { - start: from, - end: to, - }, - kqlQuery: { - filterQuery: { - kuery: { + if (!isEmpty(timelineTemplate)) { + openSignalInBasicTimeline = false; + const { timeline } = formatTimelineResultToModel(timelineTemplate, true); + const query = replaceTemplateFieldFromQuery( + timeline.kqlQuery?.filterQuery?.kuery?.expression ?? '', + ecsData + ); + const filters = replaceTemplateFieldFromMatchFilters(timeline.filters ?? [], ecsData); + const dataProviders = replaceTemplateFieldFromDataProviders( + timeline.dataProviders ?? [], + ecsData + ); + createTimeline({ + from, + timeline: { + ...timeline, + dataProviders, + eventType: 'all', + filters, + dateRange: { + start: from, + end: to, + }, + kqlQuery: { + filterQuery: { + kuery: { + kind: timeline.kqlQuery?.filterQuery?.kuery?.kind ?? 'kuery', + expression: query, + }, + serializedQuery: convertKueryToElasticSearchQuery(query), + }, + filterQueryDraft: { kind: timeline.kqlQuery?.filterQuery?.kuery?.kind ?? 'kuery', expression: query, }, - serializedQuery: convertKueryToElasticSearchQuery(query), - }, - filterQueryDraft: { - kind: timeline.kqlQuery?.filterQuery?.kuery?.kind ?? 'kuery', - expression: query, }, + show: true, }, - show: true, - }, - to, - }); + to, + }); + } } catch { + openSignalInBasicTimeline = true; updateTimelineIsLoading({ id: 'timeline-1', isLoading: false }); } - } else { + } + + if (openSignalInBasicTimeline) { createTimeline({ from, timeline: { diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/user_info/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/user_info/index.tsx index 0f6a51e52cd2e..a96913f2ad541 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/user_info/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/user_info/index.tsx @@ -18,6 +18,7 @@ export interface State { hasManageApiKey: boolean | null; isSignalIndexExists: boolean | null; isAuthenticated: boolean | null; + hasEncryptionKey: boolean | null; loading: boolean; signalIndexName: string | null; } @@ -29,6 +30,7 @@ const initialState: State = { hasManageApiKey: null, isSignalIndexExists: null, isAuthenticated: null, + hasEncryptionKey: null, loading: true, signalIndexName: null, }; @@ -55,6 +57,10 @@ export type Action = type: 'updateIsAuthenticated'; isAuthenticated: boolean | null; } + | { + type: 'updateHasEncryptionKey'; + hasEncryptionKey: boolean | null; + } | { type: 'updateCanUserCRUD'; canUserCRUD: boolean | null; @@ -102,6 +108,12 @@ export const userInfoReducer = (state: State, action: Action): State => { isAuthenticated: action.isAuthenticated, }; } + case 'updateHasEncryptionKey': { + return { + ...state, + hasEncryptionKey: action.hasEncryptionKey, + }; + } case 'updateCanUserCRUD': { return { ...state, @@ -142,6 +154,7 @@ export const useUserInfo = (): State => { hasManageApiKey, isSignalIndexExists, isAuthenticated, + hasEncryptionKey, loading, signalIndexName, }, @@ -150,6 +163,7 @@ export const useUserInfo = (): State => { const { loading: privilegeLoading, isAuthenticated: isApiAuthenticated, + hasEncryptionKey: isApiEncryptionKey, hasIndexManage: hasApiIndexManage, hasIndexWrite: hasApiIndexWrite, hasManageApiKey: hasApiManageApiKey, @@ -205,6 +219,12 @@ export const useUserInfo = (): State => { } }, [loading, isAuthenticated, isApiAuthenticated]); + useEffect(() => { + if (!loading && hasEncryptionKey !== isApiEncryptionKey && isApiEncryptionKey != null) { + dispatch({ type: 'updateHasEncryptionKey', hasEncryptionKey: isApiEncryptionKey }); + } + }, [loading, hasEncryptionKey, isApiEncryptionKey]); + useEffect(() => { if (!loading && canUserCRUD !== capabilitiesCanUserCRUD && capabilitiesCanUserCRUD != null) { dispatch({ type: 'updateCanUserCRUD', canUserCRUD: capabilitiesCanUserCRUD }); @@ -220,6 +240,7 @@ export const useUserInfo = (): State => { useEffect(() => { if ( isAuthenticated && + hasEncryptionKey && hasIndexManage && isSignalIndexExists != null && !isSignalIndexExists && @@ -227,12 +248,13 @@ export const useUserInfo = (): State => { ) { createSignalIndex(); } - }, [createSignalIndex, isAuthenticated, isSignalIndexExists, hasIndexManage]); + }, [createSignalIndex, isAuthenticated, hasEncryptionKey, isSignalIndexExists, hasIndexManage]); return { loading, isSignalIndexExists, isAuthenticated, + hasEncryptionKey, canUserCRUD, hasIndexManage, hasIndexWrite, diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx index b6ddb4de9fd39..d854c377e6ec8 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx @@ -18,7 +18,10 @@ import { GlobalTime } from '../../containers/global_time'; import { indicesExistOrDataTemporarilyUnavailable, WithSource } from '../../containers/source'; import { AlertsTable } from '../../components/alerts_viewer/alerts_table'; import { FiltersGlobal } from '../../components/filters_global'; -import { DETECTION_ENGINE_PAGE_NAME } from '../../components/link_to/redirect_to_detection_engine'; +import { + getDetectionEngineTabUrl, + getRulesUrl, +} from '../../components/link_to/redirect_to_detection_engine'; import { SiemSearchBar } from '../../components/search_bar'; import { WrapperPage } from '../../components/wrapper_page'; import { State } from '../../store'; @@ -30,6 +33,7 @@ import { InputsRange } from '../../store/inputs/model'; import { AlertsByCategory } from '../overview/alerts_by_category'; import { useSignalInfo } from './components/signals_info'; import { SignalsTable } from './components/signals'; +import { NoApiIntegrationKeyCallOut } from './components/no_api_integration_callout'; import { NoWriteSignalsCallOut } from './components/no_write_signals_callout'; import { SignalsHistogramPanel } from './components/signals_histogram_panel'; import { signalsHistogramOptions } from './components/signals_histogram_panel/config'; @@ -79,6 +83,7 @@ const DetectionEnginePageComponent: React.FC loading, isSignalIndexExists, isAuthenticated: isUserAuthenticated, + hasEncryptionKey, canUserCRUD, signalIndexName, hasIndexWrite, @@ -101,7 +106,7 @@ const DetectionEnginePageComponent: React.FC isSelected={tab.id === tabName} disabled={tab.disabled} key={tab.id} - href={`#/${DETECTION_ENGINE_PAGE_NAME}/${tab.id}`} + href={getDetectionEngineTabUrl(tab.id)} > {tab.name} @@ -134,6 +139,7 @@ const DetectionEnginePageComponent: React.FC return ( <> + {hasEncryptionKey != null && !hasEncryptionKey && } {hasIndexWrite != null && !hasIndexWrite && } {({ indicesExist, indexPattern }) => { @@ -155,7 +161,7 @@ const DetectionEnginePageComponent: React.FC } title={i18n.PAGE_TITLE} > - + {i18n.BUTTON_MANAGE_RULES} @@ -184,7 +190,7 @@ const DetectionEnginePageComponent: React.FC Cancel diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/index.tsx index 91b2ee283609f..9a68797aea79b 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/index.tsx @@ -90,6 +90,11 @@ export const ImportRuleModalComponent = ({ } }, [selectedFiles, overwrite]); + const handleCloseModal = useCallback(() => { + setSelectedFiles(null); + closeModal(); + }, [closeModal]); + return ( <> {showModal && ( @@ -125,7 +130,7 @@ export const ImportRuleModalComponent = ({ - {i18n.CANCEL_BUTTON} + {i18n.CANCEL_BUTTON} = ({ min: 0, fullWidth: false, disabled: isLoading, - options: severityOptions, showTicks: true, tickInterval: 25, }, @@ -239,8 +238,13 @@ const StepAboutRuleComponent: FC = ({ {({ severity }) => { const newRiskScore = defaultRiskScoreBySeverity[severity as SeverityValue]; + const severityField = form.getFields().severity; const riskScoreField = form.getFields().riskScore; - if (newRiskScore != null && riskScoreField.value !== newRiskScore) { + if ( + severityField.value !== severity && + newRiskScore != null && + riskScoreField.value !== newRiskScore + ) { riskScoreField.setValue(newRiskScore); } return null; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx index 55b838077988c..3adc22329ac4f 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx @@ -23,6 +23,7 @@ import { StepDefineRule } from '../components/step_define_rule'; import { StepScheduleRule } from '../components/step_schedule_rule'; import { DetectionEngineHeaderPage } from '../../components/detection_engine_header_page'; import * as RuleI18n from '../translations'; +import { redirectToDetections } from '../helpers'; import { AboutStepRule, DefineStepRule, RuleStep, RuleStepData, ScheduleStepRule } from '../types'; import { formatRule } from './helpers'; import * as i18n from './translations'; @@ -69,6 +70,7 @@ const CreateRulePageComponent: React.FC = () => { loading, isSignalIndexExists, isAuthenticated, + hasEncryptionKey, canUserCRUD, hasManageApiKey, } = useUserInfo(); @@ -239,11 +241,7 @@ const CreateRulePageComponent: React.FC = () => { return ; } - if ( - isSignalIndexExists != null && - isAuthenticated != null && - (!isSignalIndexExists || !isAuthenticated) - ) { + if (redirectToDetections(isSignalIndexExists, isAuthenticated, hasEncryptionKey)) { return ; } else if (userHasNoPermissions) { return ; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx index 7b615d5f159c2..bac1494c4fdd8 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx @@ -25,9 +25,9 @@ import { connect } from 'react-redux'; import { FiltersGlobal } from '../../../../components/filters_global'; import { FormattedDate } from '../../../../components/formatted_date'; import { - getDetectionEngineUrl, getEditRuleUrl, getRulesUrl, + DETECTION_ENGINE_PAGE_NAME, } from '../../../../components/link_to/redirect_to_detection_engine'; import { SiemSearchBar } from '../../../../components/search_bar'; import { WrapperPage } from '../../../../components/wrapper_page'; @@ -54,7 +54,7 @@ import * as detectionI18n from '../../translations'; import { ReadOnlyCallOut } from '../components/read_only_callout'; import { RuleSwitch } from '../components/rule_switch'; import { StepPanel } from '../components/step_panel'; -import { getStepsData } from '../helpers'; +import { getStepsData, redirectToDetections } from '../helpers'; import * as ruleI18n from '../translations'; import * as i18n from './translations'; import { GlobalTime } from '../../../../containers/global_time'; @@ -113,6 +113,7 @@ const RuleDetailsPageComponent: FC = ({ loading, isSignalIndexExists, isAuthenticated, + hasEncryptionKey, canUserCRUD, hasManageApiKey, hasIndexWrite, @@ -236,12 +237,8 @@ const RuleDetailsPageComponent: FC = ({ [ruleEnabled, setRuleEnabled] ); - if ( - isSignalIndexExists != null && - isAuthenticated != null && - (!isSignalIndexExists || !isAuthenticated) - ) { - return ; + if (redirectToDetections(isSignalIndexExists, isAuthenticated, hasEncryptionKey)) { + return ; } return ( diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx index 65f4bd2edf7cd..99fcff6b8d2fd 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx @@ -32,7 +32,7 @@ import { StepAboutRule } from '../components/step_about_rule'; import { StepDefineRule } from '../components/step_define_rule'; import { StepScheduleRule } from '../components/step_schedule_rule'; import { formatRule } from '../create/helpers'; -import { getStepsData } from '../helpers'; +import { getStepsData, redirectToDetections } from '../helpers'; import * as ruleI18n from '../translations'; import { RuleStep, DefineStepRule, AboutStepRule, ScheduleStepRule } from '../types'; import * as i18n from './translations'; @@ -56,6 +56,7 @@ const EditRulePageComponent: FC = () => { loading: initLoading, isSignalIndexExists, isAuthenticated, + hasEncryptionKey, canUserCRUD, hasManageApiKey, } = useUserInfo(); @@ -270,11 +271,7 @@ const EditRulePageComponent: FC = () => { return ; } - if ( - isSignalIndexExists != null && - isAuthenticated != null && - (!isSignalIndexExists || !isAuthenticated) - ) { + if (redirectToDetections(isSignalIndexExists, isAuthenticated, hasEncryptionKey)) { return ; } else if (userHasNoPermissions) { return ; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx index ce0d50d9b6106..4e98fc17404c9 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx @@ -138,3 +138,13 @@ export const setFieldValue = ( form.setFieldValue(key, val); } }); + +export const redirectToDetections = ( + isSignalIndexExists: boolean | null, + isAuthenticated: boolean | null, + hasEncryptionKey: boolean | null +) => + isSignalIndexExists != null && + isAuthenticated != null && + hasEncryptionKey != null && + (!isSignalIndexExists || !isAuthenticated || !hasEncryptionKey); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/index.tsx index 1c0ed34e92793..0c53ad19a3574 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/index.tsx @@ -10,6 +10,7 @@ import { Redirect } from 'react-router-dom'; import { usePrePackagedRules } from '../../../containers/detection_engine/rules'; import { + DETECTION_ENGINE_PAGE_NAME, getDetectionEngineUrl, getCreateRuleUrl, } from '../../../components/link_to/redirect_to_detection_engine'; @@ -22,7 +23,7 @@ import { AllRules } from './all'; import { ImportRuleModal } from './components/import_rule_modal'; import { ReadOnlyCallOut } from './components/read_only_callout'; import { UpdatePrePackagedRulesCallOut } from './components/pre_packaged_rules/update_callout'; -import { getPrePackagedRuleStatus } from './helpers'; +import { getPrePackagedRuleStatus, redirectToDetections } from './helpers'; import * as i18n from './translations'; type Func = () => void; @@ -35,6 +36,7 @@ const RulesPageComponent: React.FC = () => { loading, isSignalIndexExists, isAuthenticated, + hasEncryptionKey, canUserCRUD, hasIndexWrite, hasManageApiKey, @@ -54,6 +56,7 @@ const RulesPageComponent: React.FC = () => { hasManageApiKey, isSignalIndexExists, isAuthenticated, + hasEncryptionKey, }); const prePackagedRuleStatus = getPrePackagedRuleStatus( rulesInstalled, @@ -83,12 +86,8 @@ const RulesPageComponent: React.FC = () => { refreshRulesData.current = refreshRule; }, []); - if ( - isSignalIndexExists != null && - isAuthenticated != null && - (!isSignalIndexExists || !isAuthenticated) - ) { - return ; + if (redirectToDetections(isSignalIndexExists, isAuthenticated, hasEncryptionKey)) { + return ; } return ( diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts index eea25a1e89cc8..6a42aed123fa3 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -390,6 +390,7 @@ export const getMockPrivileges = () => ({ }, application: {}, is_authenticated: false, + has_encryption_key: true, }); export const getFindResultStatus = (): SavedObjectsFindResponse => ({ diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts index 803d9d645aadb..5ea4dc7595b2b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts @@ -29,8 +29,10 @@ export const createReadPrivilegesRulesRoute = (server: ServerFacade): Hapi.Serve const callWithRequest = callWithRequestFactory(request, server); const index = getIndex(request, server); const permissions = await readPrivileges(callWithRequest, index); + const usingEphemeralEncryptionKey = server.usingEphemeralEncryptionKey; return merge(permissions, { is_authenticated: request?.auth?.isAuthenticated ?? false, + has_encryption_key: !usingEphemeralEncryptionKey, }); } catch (err) { return transformError(err); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/export_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/export_rules_route.ts index b9ff2e6018624..ce62469342883 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/export_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/export_rules_route.ts @@ -36,28 +36,32 @@ export const createExportRulesRoute = (server: ServerFacade): Hapi.ServerRoute = return headers.response().code(404); } - const exportSizeLimit = server.config().get('savedObjects.maxImportExportSize'); - if (request.payload?.objects != null && request.payload.objects.length > exportSizeLimit) { - return Boom.badRequest(`Can't export more than ${exportSizeLimit} rules`); - } else { - const nonPackagedRulesCount = await getNonPackagedRulesCount({ alertsClient }); - if (nonPackagedRulesCount > exportSizeLimit) { + try { + const exportSizeLimit = server.config().get('savedObjects.maxImportExportSize'); + if (request.payload?.objects != null && request.payload.objects.length > exportSizeLimit) { return Boom.badRequest(`Can't export more than ${exportSizeLimit} rules`); + } else { + const nonPackagedRulesCount = await getNonPackagedRulesCount({ alertsClient }); + if (nonPackagedRulesCount > exportSizeLimit) { + return Boom.badRequest(`Can't export more than ${exportSizeLimit} rules`); + } } - } - const exported = - request.payload?.objects != null - ? await getExportByObjectIds(alertsClient, request.payload.objects) - : await getExportAll(alertsClient); + const exported = + request.payload?.objects != null + ? await getExportByObjectIds(alertsClient, request.payload.objects) + : await getExportAll(alertsClient); - const response = request.query.exclude_export_details - ? headers.response(exported.rulesNdjson) - : headers.response(`${exported.rulesNdjson}${exported.exportDetails}`); + const response = request.query.exclude_export_details + ? headers.response(exported.rulesNdjson) + : headers.response(`${exported.rulesNdjson}${exported.exportDetails}`); - return response - .header('Content-Disposition', `attachment; filename="${request.query.file_name}"`) - .header('Content-Type', 'application/ndjson'); + return response + .header('Content-Disposition', `attachment; filename="${request.query.file_name}"`) + .header('Content-Type', 'application/ndjson'); + } catch { + return Boom.badRequest(`Sorry, something went wrong to export rules`); + } }, }; }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts index 71fdef3623bc7..0d57f5739fc15 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts @@ -6,8 +6,9 @@ import Boom from 'boom'; import Hapi from 'hapi'; +import { chunk, isEmpty, isFunction } from 'lodash/fp'; import { extname } from 'path'; -import { isFunction } from 'lodash/fp'; +import uuid from 'uuid'; import { createPromiseFromStreams } from '../../../../../../../../../src/legacy/utils/streams'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { createRules } from '../../rules/create_rules'; @@ -18,17 +19,25 @@ import { getIndexExists } from '../../index/get_index_exists'; import { callWithRequestFactory, getIndex, - createImportErrorObject, - transformImportError, - ImportSuccessError, + createBulkErrorObject, + ImportRuleResponse, } from '../utils'; import { createRulesStreamFromNdJson } from '../../rules/create_rules_stream_from_ndjson'; import { ImportRuleAlertRest } from '../../types'; -import { transformOrImportError } from './utils'; import { updateRules } from '../../rules/update_rules'; import { importRulesQuerySchema, importRulesPayloadSchema } from '../schemas/import_rules_schema'; import { KibanaRequest } from '../../../../../../../../../src/core/server'; +type PromiseFromStreams = ImportRuleAlertRest | Error; + +/* + * We were getting some error like that possible EventEmitter memory leak detected + * So we decide to batch the update by 10 to avoid any complication in the node side + * https://nodejs.org/docs/latest/api/events.html#events_emitter_setmaxlisteners_n + * + */ +const CHUNK_PARSED_OBJECT_SIZE = 10; + export const createImportRulesRoute = (server: ServerFacade): Hapi.ServerRoute => { return { method: 'POST', @@ -67,145 +76,189 @@ export const createImportRulesRoute = (server: ServerFacade): Hapi.ServerRoute = const objectLimit = server.config().get('savedObjects.maxImportExportSize'); const readStream = createRulesStreamFromNdJson(request.payload.file, objectLimit); - const parsedObjects = await createPromiseFromStreams<[ImportRuleAlertRest | Error]>([ - readStream, - ]); - - const reduced = await parsedObjects.reduce>( - async (accum, parsedRule) => { - const existingImportSuccessError = await accum; - if (parsedRule instanceof Error) { - // If the JSON object had a validation or parse error then we return - // early with the error and an (unknown) for the ruleId - return createImportErrorObject({ - ruleId: '(unknown)', // TODO: Better handling where we know which ruleId is having issues with imports - statusCode: 400, - message: parsedRule.message, - existingImportSuccessError, - }); - } + const parsedObjects = await createPromiseFromStreams([readStream]); - const { - description, - enabled, - false_positives: falsePositives, - from, - immutable, - query, - language, - output_index: outputIndex, - saved_id: savedId, - meta, - filters, - rule_id: ruleId, - index, - interval, - max_signals: maxSignals, - risk_score: riskScore, - name, - severity, - tags, - threat, - to, - type, - references, - timeline_id: timelineId, - timeline_title: timelineTitle, - version, - } = parsedRule; - try { - const finalIndex = outputIndex != null ? outputIndex : getIndex(request, server); - const callWithRequest = callWithRequestFactory(request, server); - const indexExists = await getIndexExists(callWithRequest, finalIndex); - if (!indexExists) { - return createImportErrorObject({ - ruleId, - statusCode: 409, - message: `To create a rule, the index must exist first. Index ${finalIndex} does not exist`, - existingImportSuccessError, - }); - } - const rule = await readRules({ alertsClient, ruleId }); - if (rule == null) { - const createdRule = await createRules({ - alertsClient, - actionsClient, - description, - enabled, - falsePositives, - from, - immutable, - query, - language, - outputIndex: finalIndex, - savedId, - timelineId, - timelineTitle, - meta, - filters, - ruleId, - index, - interval, - maxSignals, - riskScore, - name, - severity, - tags, - to, - type, - threat, - references, - version, - }); - return transformOrImportError(ruleId, createdRule, existingImportSuccessError); - } else if (rule != null && request.query.overwrite) { - const updatedRule = await updateRules({ - alertsClient, - actionsClient, - savedObjectsClient, - description, - enabled, - falsePositives, - from, - immutable, - query, - language, - outputIndex, - savedId, - timelineId, - timelineTitle, - meta, - filters, - id: undefined, - ruleId, - index, - interval, - maxSignals, - riskScore, - name, - severity, - tags, - to, - type, - threat, - references, - version, - }); - return transformOrImportError(ruleId, updatedRule, existingImportSuccessError); - } else { - return existingImportSuccessError; - } - } catch (err) { - return transformImportError(ruleId, err, existingImportSuccessError); - } - }, - Promise.resolve({ - success: true, - success_count: 0, - errors: [], - }) + const uniqueParsedObjects = Array.from( + parsedObjects + .reduce( + (acc, parsedRule) => { + if (parsedRule instanceof Error) { + acc.set(uuid.v4(), parsedRule); + } else { + const { rule_id: ruleId } = parsedRule; + if (ruleId != null) { + acc.set(ruleId, parsedRule); + } else { + acc.set(uuid.v4(), parsedRule); + } + } + return acc; + }, // using map (preserves ordering) + new Map() + ) + .values() ); - return reduced; + + const chunkParseObjects = chunk(CHUNK_PARSED_OBJECT_SIZE, uniqueParsedObjects); + let importRuleResponse: ImportRuleResponse[] = []; + + while (chunkParseObjects.length) { + const batchParseObjects = chunkParseObjects.shift() ?? []; + const newImportRuleResponse = await Promise.all( + batchParseObjects.reduce>>((accum, parsedRule) => { + const importsWorkerPromise = new Promise( + async (resolve, reject) => { + if (parsedRule instanceof Error) { + // If the JSON object had a validation or parse error then we return + // early with the error and an (unknown) for the ruleId + resolve( + createBulkErrorObject({ + ruleId: '(unknown)', + statusCode: 400, + message: parsedRule.message, + }) + ); + return null; + } + const { + description, + false_positives: falsePositives, + from, + immutable, + query, + language, + output_index: outputIndex, + saved_id: savedId, + meta, + filters, + rule_id: ruleId, + index, + interval, + max_signals: maxSignals, + risk_score: riskScore, + name, + severity, + tags, + threat, + to, + type, + references, + timeline_id: timelineId, + timeline_title: timelineTitle, + version, + } = parsedRule; + try { + const finalIndex = getIndex(request, server); + const callWithRequest = callWithRequestFactory(request, server); + const indexExists = await getIndexExists(callWithRequest, finalIndex); + if (!indexExists) { + resolve( + createBulkErrorObject({ + ruleId, + statusCode: 409, + message: `To create a rule, the index must exist first. Index ${finalIndex} does not exist`, + }) + ); + } + const rule = await readRules({ alertsClient, ruleId }); + if (rule == null) { + await createRules({ + alertsClient, + actionsClient, + description, + enabled: false, + falsePositives, + from, + immutable, + query, + language, + outputIndex: finalIndex, + savedId, + timelineId, + timelineTitle, + meta, + filters, + ruleId, + index, + interval, + maxSignals, + riskScore, + name, + severity, + tags, + to, + type, + threat, + references, + version, + }); + resolve({ rule_id: ruleId, status_code: 200 }); + } else if (rule != null && request.query.overwrite) { + await updateRules({ + alertsClient, + actionsClient, + savedObjectsClient, + description, + enabled: false, + falsePositives, + from, + immutable, + query, + language, + outputIndex, + savedId, + timelineId, + timelineTitle, + meta, + filters, + id: undefined, + ruleId, + index, + interval, + maxSignals, + riskScore, + name, + severity, + tags, + to, + type, + threat, + references, + version, + }); + resolve({ rule_id: ruleId, status_code: 200 }); + } else if (rule != null) { + resolve( + createBulkErrorObject({ + ruleId, + statusCode: 409, + message: `This Rule "${rule.name}" already exists`, + }) + ); + } + } catch (err) { + resolve( + createBulkErrorObject({ + ruleId, + statusCode: 400, + message: err.message, + }) + ); + } + } + ); + return [...accum, importsWorkerPromise]; + }, []) + ); + importRuleResponse = [...importRuleResponse, ...newImportRuleResponse]; + } + + const errorsResp = importRuleResponse.filter(resp => !isEmpty(resp.error)); + return { + success: errorsResp.length === 0, + success_count: importRuleResponse.filter(resp => resp.status_code === 200).length, + errors: errorsResp, + }; }, }; }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts index f51cea0753f1a..590307e06a26a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts @@ -39,8 +39,8 @@ export const createUpdateRulesRoute = (server: ServerFacade): Hapi.ServerRoute = language, output_index: outputIndex, saved_id: savedId, - timeline_id: timelineId, - timeline_title: timelineTitle, + timeline_id: timelineId = null, + timeline_title: timelineTitle = null, meta, filters, rule_id: ruleId, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts index 19cd972b60e1a..416c76b5d4eb5 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts @@ -52,6 +52,16 @@ export const createBulkErrorObject = ({ }; }; +export interface ImportRuleResponse { + rule_id: string; + status_code?: number; + message?: string; + error?: { + status_code: number; + message: string; + }; +} + export interface ImportSuccessError { success: boolean; success_count: number; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts index 05e455efb3f22..236d04acc782b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts @@ -66,6 +66,7 @@ describe('get_export_by_object_ids', () => { const objects = [{ rule_id: 'rule-1' }]; const exports = await getRulesFromObjects(unsafeCast, objects); const expected: RulesErrors = { + exportedCount: 1, missingRules: [], rules: [ { @@ -141,6 +142,7 @@ describe('get_export_by_object_ids', () => { const objects = [{ rule_id: 'rule-1' }]; const exports = await getRulesFromObjects(unsafeCast, objects); const expected: RulesErrors = { + exportedCount: 0, missingRules: [{ rule_id: 'rule-1' }], rules: [], }; @@ -164,6 +166,7 @@ describe('get_export_by_object_ids', () => { const objects = [{ rule_id: 'rule-1' }]; const exports = await getRulesFromObjects(unsafeCast, objects); const expected: RulesErrors = { + exportedCount: 0, missingRules: [{ rule_id: 'rule-1' }], rules: [], }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.ts index a5cf1bbfb7858..7e0d61d040617 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.ts @@ -11,7 +11,20 @@ import { readRules } from './read_rules'; import { transformRulesToNdjson, transformAlertToRule } from '../routes/rules/utils'; import { OutputRuleAlertRest } from '../types'; +interface ExportSuccesRule { + statusCode: 200; + rule: Partial; +} + +interface ExportFailedRule { + statusCode: 404; + missingRuleId: { rule_id: string }; +} + +type ExportRules = ExportSuccesRule | ExportFailedRule; + export interface RulesErrors { + exportedCount: number; missingRules: Array<{ rule_id: string }>; rules: Array>; } @@ -33,28 +46,44 @@ export const getRulesFromObjects = async ( alertsClient: AlertsClient, objects: Array<{ rule_id: string }> ): Promise => { - const alertsAndErrors = await objects.reduce>( - async (accumPromise, object) => { - const accum = await accumPromise; - const rule = await readRules({ alertsClient, ruleId: object.rule_id }); - if (rule != null && isAlertType(rule) && rule.params.immutable !== true) { - const transformedRule = transformAlertToRule(rule); - return { - missingRules: accum.missingRules, - rules: [...accum.rules, transformedRule], - }; - } else { - return { - missingRules: [...accum.missingRules, { rule_id: object.rule_id }], - rules: accum.rules, - }; - } - }, - Promise.resolve({ - exportedCount: 0, - missingRules: [], - rules: [], - }) + const alertsAndErrors = await Promise.all( + objects.reduce>>((accumPromise, object) => { + const exportWorkerPromise = new Promise(async resolve => { + try { + const rule = await readRules({ alertsClient, ruleId: object.rule_id }); + if (rule != null && isAlertType(rule) && rule.params.immutable !== true) { + const transformedRule = transformAlertToRule(rule); + resolve({ + statusCode: 200, + rule: transformedRule, + }); + } else { + resolve({ + statusCode: 404, + missingRuleId: { rule_id: object.rule_id }, + }); + } + } catch { + resolve({ + statusCode: 404, + missingRuleId: { rule_id: object.rule_id }, + }); + } + }); + return [...accumPromise, exportWorkerPromise]; + }, []) ); - return alertsAndErrors; + + const missingRules = alertsAndErrors.filter( + resp => resp.statusCode === 404 + ) as ExportFailedRule[]; + const exportedRules = alertsAndErrors.filter( + resp => resp.statusCode === 200 + ) as ExportSuccesRule[]; + + return { + exportedCount: exportedRules.length, + missingRules: missingRules.map(mr => mr.missingRuleId), + rules: exportedRules.map(er => er.rule), + }; }; diff --git a/x-pack/legacy/plugins/siem/server/types.ts b/x-pack/legacy/plugins/siem/server/types.ts index 3fa2268afe92c..7c07e63404eaa 100644 --- a/x-pack/legacy/plugins/siem/server/types.ts +++ b/x-pack/legacy/plugins/siem/server/types.ts @@ -8,6 +8,7 @@ import { Legacy } from 'kibana'; export interface ServerFacade { config: Legacy.Server['config']; + usingEphemeralEncryptionKey: boolean; plugins: { // eslint-disable-next-line @typescript-eslint/no-explicit-any actions: any; // We have to do this at the moment because the types are not compatible diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/get_supported_url_params.ts b/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/get_supported_url_params.ts index c1382d455313f..f01448d9e37ac 100644 --- a/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/get_supported_url_params.ts +++ b/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/get_supported_url_params.ts @@ -84,7 +84,8 @@ export const getSupportedUrlParams = (params: { ), absoluteDateRangeEnd: parseAbsoluteDate( dateRangeEnd || DATE_RANGE_END, - ABSOLUTE_DATE_RANGE_END + ABSOLUTE_DATE_RANGE_END, + { roundUp: true } ), autorefreshInterval: parseUrlInt(autorefreshInterval, AUTOREFRESH_INTERVAL), autorefreshIsPaused: parseIsPaused(autorefreshIsPaused, AUTOREFRESH_IS_PAUSED), diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/parse_absolute_date.ts b/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/parse_absolute_date.ts index eaf720a8d2f7e..2b0921f07abc9 100644 --- a/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/parse_absolute_date.ts +++ b/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/parse_absolute_date.ts @@ -6,8 +6,8 @@ import DateMath from '@elastic/datemath'; -export const parseAbsoluteDate = (date: string, defaultValue: number): number => { - const momentWrapper = DateMath.parse(date); +export const parseAbsoluteDate = (date: string, defaultValue: number, options = {}): number => { + const momentWrapper = DateMath.parse(date, options); if (momentWrapper) { return momentWrapper.valueOf(); } diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/find_potential_matches.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/find_potential_matches.ts index e34bc6ab805c0..634d6369531d8 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/find_potential_matches.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/find_potential_matches.ts @@ -25,7 +25,6 @@ export const findPotentialMatches = async ( size: number ) => { const queryResult = await query(queryContext, searchAfter, size); - const checkGroups = new Set(); const monitorIds: string[] = []; get(queryResult, 'aggregations.monitors.buckets', []).forEach((b: any) => { diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/query_context.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/query_context.ts index d97b7653402a3..961cc94dcea19 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/query_context.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/query_context.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import DateMath from '@elastic/datemath'; import { APICaller } from 'kibana/server'; import { CursorPagination } from '../adapter_types'; import { INDEX_NAMES } from '../../../../../common/constants'; +import { parseRelativeDate } from '../../../helper/get_histogram_interval'; export class QueryContext { callES: APICaller; @@ -95,8 +95,9 @@ export class QueryContext { // latencies and slowdowns that's dangerous. Making this value larger makes things // only slower, but only marginally so, and prevents people from seeing weird // behavior. - const tsStart = DateMath.parse(this.dateRangeEnd)!.subtract(5, 'minutes'); - const tsEnd = DateMath.parse(this.dateRangeEnd)!; + + const tsEnd = parseRelativeDate(this.dateRangeEnd, { roundUp: true })!; + const tsStart = tsEnd.subtract(5, 'minutes'); return { range: { diff --git a/x-pack/legacy/plugins/uptime/server/lib/helper/__test__/parse_relative_date.test.ts b/x-pack/legacy/plugins/uptime/server/lib/helper/__test__/parse_relative_date.test.ts new file mode 100644 index 0000000000000..ec6e48c62c62e --- /dev/null +++ b/x-pack/legacy/plugins/uptime/server/lib/helper/__test__/parse_relative_date.test.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { parseRelativeDate } from '../get_histogram_interval'; +import { Moment } from 'moment'; + +describe('Parsing a relative end date properly', () => { + it('converts the upper range of relative end dates to now', async () => { + const thisWeekEndDate = 'now/w'; + + let endDate = parseRelativeDate(thisWeekEndDate, { roundUp: true }); + expect(Date.now() - (endDate as Moment).valueOf()).toBeLessThan(1000); + + const todayEndDate = 'now/d'; + + endDate = parseRelativeDate(todayEndDate, { roundUp: true }); + + expect(Date.now() - (endDate as Moment).valueOf()).toBeLessThan(1000); + }); +}); diff --git a/x-pack/legacy/plugins/uptime/server/lib/helper/get_histogram_interval.ts b/x-pack/legacy/plugins/uptime/server/lib/helper/get_histogram_interval.ts index 0dedc3e456f51..26515fb4b4c63 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/helper/get_histogram_interval.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/helper/get_histogram_interval.ts @@ -7,18 +7,39 @@ import DateMath from '@elastic/datemath'; import { QUERY } from '../../../common/constants'; +export const parseRelativeDate = (dateStr: string, options = {}) => { + // We need this this parsing because if user selects This week or this date + // That represents end date in future, if week or day is still in the middle + // Uptime data can never be collected in future, so we will reset date to now + // in That case. Example case we select this week range will be to='now/w' and from = 'now/w'; + + const parsedDate = DateMath.parse(dateStr, options); + const dateTimestamp = parsedDate?.valueOf() ?? 0; + if (dateTimestamp > Date.now()) { + return DateMath.parse('now'); + } + return parsedDate; +}; + export const getHistogramInterval = ( dateRangeStart: string, dateRangeEnd: string, bucketCount?: number ): number => { - const from = DateMath.parse(dateRangeStart); - const to = DateMath.parse(dateRangeEnd); + const from = parseRelativeDate(dateRangeStart); + + // roundUp is required for relative date like now/w to get the end of the week + const to = parseRelativeDate(dateRangeEnd, { roundUp: true }); if (from === undefined) { throw Error('Invalid dateRangeStart value'); } if (to === undefined) { throw Error('Invalid dateRangeEnd value'); } - return Math.round((to.valueOf() - from.valueOf()) / (bucketCount || QUERY.DEFAULT_BUCKET_COUNT)); + const interval = Math.round( + (to.valueOf() - from.valueOf()) / (bucketCount || QUERY.DEFAULT_BUCKET_COUNT) + ); + + // Interval can never be zero, if it's 0 we return at least 1ms interval + return interval > 0 ? interval : 1; }; diff --git a/x-pack/plugins/encrypted_saved_objects/server/config.test.ts b/x-pack/plugins/encrypted_saved_objects/server/config.test.ts index 7d6632aa56cb1..e05d8d687d05a 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/config.test.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/config.test.ts @@ -49,7 +49,7 @@ describe('config schema', () => { }); describe('createConfig$()', () => { - it('should log a warning and set xpack.encryptedSavedObjects.encryptionKey if not set', async () => { + it('should log a warning, set xpack.encryptedSavedObjects.encryptionKey and usingEphemeralEncryptionKey=true when encryptionKey is not set', async () => { const mockRandomBytes = jest.requireMock('crypto').randomBytes; mockRandomBytes.mockReturnValue('ab'.repeat(16)); @@ -57,7 +57,10 @@ describe('createConfig$()', () => { const config = await createConfig$(contextMock) .pipe(first()) .toPromise(); - expect(config).toEqual({ encryptionKey: 'ab'.repeat(16) }); + expect(config).toEqual({ + config: { encryptionKey: 'ab'.repeat(16) }, + usingEphemeralEncryptionKey: true, + }); expect(loggingServiceMock.collect(contextMock.logger).warn).toMatchInlineSnapshot(` Array [ @@ -67,4 +70,19 @@ describe('createConfig$()', () => { ] `); }); + + it('should not log a warning and set usingEphemeralEncryptionKey=false when encryptionKey is set', async () => { + const contextMock = coreMock.createPluginInitializerContext({ + encryptionKey: 'supersecret', + }); + const config = await createConfig$(contextMock) + .pipe(first()) + .toPromise(); + expect(config).toEqual({ + config: { encryptionKey: 'supersecret' }, + usingEphemeralEncryptionKey: false, + }); + + expect(loggingServiceMock.collect(contextMock.logger).warn).toEqual([]); + }); }); diff --git a/x-pack/plugins/encrypted_saved_objects/server/config.ts b/x-pack/plugins/encrypted_saved_objects/server/config.ts index c755b7dd9f205..2f01850520724 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/config.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/config.ts @@ -5,15 +5,10 @@ */ import crypto from 'crypto'; -import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import { schema, TypeOf } from '@kbn/config-schema'; import { PluginInitializerContext } from 'src/core/server'; -export type ConfigType = ReturnType extends Observable - ? P - : ReturnType; - export const ConfigSchema = schema.object({ enabled: schema.boolean({ defaultValue: true }), encryptionKey: schema.conditional( @@ -30,6 +25,7 @@ export function createConfig$(context: PluginInitializerContext) { const logger = context.logger.get('config'); let encryptionKey = config.encryptionKey; + const usingEphemeralEncryptionKey = encryptionKey === undefined; if (encryptionKey === undefined) { logger.warn( 'Generating a random key for xpack.encryptedSavedObjects.encryptionKey. ' + @@ -40,7 +36,10 @@ export function createConfig$(context: PluginInitializerContext) { encryptionKey = crypto.randomBytes(16).toString('hex'); } - return { ...config, encryptionKey }; + return { + config: { ...config, encryptionKey }, + usingEphemeralEncryptionKey, + }; }) ); } diff --git a/x-pack/plugins/encrypted_saved_objects/server/mocks.ts b/x-pack/plugins/encrypted_saved_objects/server/mocks.ts index 87c36381a841a..7f53f47760f12 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/mocks.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/mocks.ts @@ -10,6 +10,7 @@ function createEncryptedSavedObjectsSetupMock() { return { registerType: jest.fn(), __legacyCompat: { registerLegacyAPI: jest.fn() }, + usingEphemeralEncryptionKey: true, } as jest.Mocked; } diff --git a/x-pack/plugins/encrypted_saved_objects/server/plugin.test.ts b/x-pack/plugins/encrypted_saved_objects/server/plugin.test.ts index 534ed13ba0acb..5228734e4a773 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/plugin.test.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/plugin.test.ts @@ -18,6 +18,7 @@ describe('EncryptedSavedObjects Plugin', () => { "registerLegacyAPI": [Function], }, "registerType": [Function], + "usingEphemeralEncryptionKey": true, } `); }); diff --git a/x-pack/plugins/encrypted_saved_objects/server/plugin.ts b/x-pack/plugins/encrypted_saved_objects/server/plugin.ts index ecd917ff90d00..d9185251ca466 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/plugin.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/plugin.ts @@ -23,6 +23,7 @@ import { SavedObjectsSetup, setupSavedObjects } from './saved_objects'; export interface PluginSetupContract { registerType: (typeRegistration: EncryptedSavedObjectTypeRegistration) => void; __legacyCompat: { registerLegacyAPI: (legacyAPI: LegacyAPI) => void }; + usingEphemeralEncryptionKey: boolean; } export interface PluginStartContract extends SavedObjectsSetup { @@ -59,7 +60,7 @@ export class Plugin { } public async setup(core: CoreSetup): Promise { - const config = await createConfig$(this.initializerContext) + const { config, usingEphemeralEncryptionKey } = await createConfig$(this.initializerContext) .pipe(first()) .toPromise(); @@ -81,6 +82,7 @@ export class Plugin { registerType: (typeRegistration: EncryptedSavedObjectTypeRegistration) => service.registerType(typeRegistration), __legacyCompat: { registerLegacyAPI: (legacyAPI: LegacyAPI) => (this.legacyAPI = legacyAPI) }, + usingEphemeralEncryptionKey, }; }