From 3fc54e7c552998367171d76e0119eab6d4f2ff1c Mon Sep 17 00:00:00 2001 From: Madison Caldwell Date: Fri, 10 Jul 2020 15:53:34 -0400 Subject: [PATCH 1/4] [Security Solution][Endpoint] Policy creation callback fixes + Improved error handling in user manifest loop (#71269) * Clean up matcher types * Rework promise and error-handling in ManifestManager * Write tests for ingest callback and ensure policy is returned when errors occur * More tests for ingest callback * Update tests * Fix tests Co-authored-by: Elastic Machine --- x-pack/plugins/ingest_manager/common/mocks.ts | 43 ++++ .../endpoint/ingest_integration.test.ts | 91 ++++++++ .../server/endpoint/ingest_integration.ts | 105 +++++---- .../server/endpoint/lib/artifacts/common.ts | 7 + .../server/endpoint/lib/artifacts/task.ts | 26 ++- .../server/endpoint/mocks.ts | 4 +- .../endpoint/schemas/artifacts/lists.ts | 26 +-- .../manifest_manager/manifest_manager.mock.ts | 61 ++---- .../manifest_manager/manifest_manager.test.ts | 11 +- .../manifest_manager/manifest_manager.ts | 205 +++++++++++------- .../apps/endpoint/policy_details.ts | 42 ++++ 11 files changed, 430 insertions(+), 191 deletions(-) create mode 100644 x-pack/plugins/ingest_manager/common/mocks.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/ingest_integration.test.ts diff --git a/x-pack/plugins/ingest_manager/common/mocks.ts b/x-pack/plugins/ingest_manager/common/mocks.ts new file mode 100644 index 00000000000000..131917af445952 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/mocks.ts @@ -0,0 +1,43 @@ +/* + * 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 { NewPackageConfig, PackageConfig } from './types/models/package_config'; + +export const createNewPackageConfigMock = () => { + return { + name: 'endpoint-1', + description: '', + namespace: 'default', + enabled: true, + config_id: '93c46720-c217-11ea-9906-b5b8a21b268e', + output_id: '', + package: { + name: 'endpoint', + title: 'Elastic Endpoint', + version: '0.9.0', + }, + inputs: [], + } as NewPackageConfig; +}; + +export const createPackageConfigMock = () => { + const newPackageConfig = createNewPackageConfigMock(); + return { + ...newPackageConfig, + id: 'c6d16e42-c32d-4dce-8a88-113cfe276ad1', + version: 'abcd', + revision: 1, + updated_at: '2020-06-25T16:03:38.159292', + updated_by: 'kibana', + created_at: '2020-06-25T16:03:38.159292', + created_by: 'kibana', + inputs: [ + { + config: {}, + }, + ], + } as PackageConfig; +}; 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 new file mode 100644 index 00000000000000..bb035a19f33d60 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/ingest_integration.test.ts @@ -0,0 +1,91 @@ +/* + * 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. + */ + +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { loggerMock } from 'src/core/server/logging/logger.mock'; +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 { getPackageConfigCreateCallback } from './ingest_integration'; + +describe('ingest_integration tests ', () => { + describe('ingest_integration sanity checks', () => { + test('policy is updated with manifest', async () => { + const logger = loggerMock.create(); + const manifestManager = getManifestManagerMock(); + 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({ + artifacts: { + 'endpoint-exceptionlist-linux-v1': { + compression_algorithm: 'zlib', + decoded_sha256: '1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', + decoded_size: 287, + encoded_sha256: 'c3dec543df1177561ab2aa74a37997ea3c1d748d532a597884f5a5c16670d56c', + encoded_size: 133, + encryption_algorithm: 'none', + relative_url: + '/api/endpoint/artifacts/download/endpoint-exceptionlist-linux-v1/1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', + }, + }, + manifest_version: 'WzAsMF0=', + schema_version: 'v1', + }); + }); + + test('policy is returned even if error is encountered during artifact sync', async () => { + const logger = loggerMock.create(); + const manifestManager = getManifestManagerMock(); + manifestManager.syncArtifacts = jest.fn().mockRejectedValue([new Error('error updating')]); + const lastDispatched = await manifestManager.getLastDispatchedManifest(); + 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('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); + 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 manifestManager = getManifestManagerMock(); + const snapshot = await manifestManager.getSnapshot(); + manifestManager.getLastDispatchedManifest = jest.fn().mockResolvedValue(snapshot!.manifest); + manifestManager.getSnapshot = jest.fn().mockResolvedValue({ + manifest: snapshot!.manifest, + 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() + ); + }); + }); +}); 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 1acec1e7c53ac7..e2522ac4af7786 100644 --- a/x-pack/plugins/security_solution/server/endpoint/ingest_integration.ts +++ b/x-pack/plugins/security_solution/server/endpoint/ingest_integration.ts @@ -8,7 +8,9 @@ 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 } from './services/artifacts'; +import { ManifestManager, ManifestSnapshot } from './services/artifacts'; +import { reportErrors, ManifestConstants } from './lib/artifacts/common'; +import { ManifestSchemaVersion } from '../../common/endpoint/schema/common'; /** * Callback to handle creation of PackageConfigs in Ingest Manager @@ -29,58 +31,83 @@ export const getPackageConfigCreateCallback = ( // follow the types/schema expected let updatedPackageConfig = newPackageConfig as NewPolicyData; - // get snapshot based on exception-list-agnostic SOs - // with diffs from last dispatched manifest, if it exists - const snapshot = await manifestManager.getSnapshot({ initialize: true }); + // 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: {}, + }; - if (snapshot === null) { - logger.warn('No manifest snapshot available.'); - return updatedPackageConfig; + // 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(), + }, + }, + }, + ], + }; } - if (snapshot.diffs.length > 0) { - // create new artifacts - await manifestManager.syncArtifacts(snapshot, 'add'); + let snapshot: ManifestSnapshot | null = null; + let success = true; + try { + // Try to get most up-to-date manifest data. - // Until we get the Default Policy Configuration in the Endpoint package, - // we will add it here manually at creation time. - // @ts-ignore - if (newPackageConfig.inputs.length === 0) { - updatedPackageConfig = { - ...newPackageConfig, - inputs: [ - { - type: 'endpoint', - enabled: true, - streams: [], - config: { - artifact_manifest: { - value: snapshot.manifest.toEndpointFormat(), - }, - policy: { - value: policyConfigFactory(), - }, - }, - }, - ], + // 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(), }; } - } - try { + return updatedPackageConfig; + } catch (err) { + success = false; + logger.error(err); return updatedPackageConfig; } finally { - 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 + if (success && snapshot !== null) { try { - await manifestManager.commit(snapshot.manifest); + 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'); + // clean up old artifacts + await manifestManager.syncArtifacts(snapshot, 'delete'); + } } catch (err) { logger.error(err); } + } else if (snapshot === null) { + logger.error('No manifest snapshot available.'); } } }; 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 9ad4554b302036..71d14eb1226d57 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 @@ -3,6 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { Logger } from 'src/core/server'; export const ArtifactConstants = { GLOBAL_ALLOWLIST_NAME: 'endpoint-exceptionlist', @@ -16,3 +17,9 @@ export const ManifestConstants = { SCHEMA_VERSION: 'v1', INITIAL_VERSION: 'WzAsMF0=', }; + +export const reportErrors = (logger: Logger, errors: Error[]) => { + errors.forEach((err) => { + logger.error(err); + }); +}; 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 aa7f56e815d58f..583f4499f591b5 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,6 +11,7 @@ import { TaskManagerStartContract, } from '../../../../../task_manager/server'; import { EndpointAppContext } from '../../types'; +import { reportErrors } from './common'; export const ManifestTaskConstants = { TIMEOUT: '1m', @@ -88,19 +89,36 @@ 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 - await manifestManager.syncArtifacts(snapshot, 'add'); + 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 - await manifestManager.dispatch(snapshot.manifest); + errors = await manifestManager.dispatch(snapshot.manifest); + if (errors.length) { + reportErrors(this.logger, errors); + throw new Error('Error dispatching manifest.'); + } // commit latest manifest state to user-artifact-manifest SO - await manifestManager.commit(snapshot.manifest); + const error = await manifestManager.commit(snapshot.manifest); + if (error) { + reportErrors(this.logger, [error]); + throw new Error('Error committing manifest.'); + } // clean up old artifacts - await manifestManager.syncArtifacts(snapshot, 'delete'); + errors = await manifestManager.syncArtifacts(snapshot, 'delete'); + if (errors.length) { + reportErrors(this.logger, errors); + throw new Error('Error cleaning up outdated artifacts.'); + } } } 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 55d7baec36dc6d..6a8c26e08d9dd9 100644 --- a/x-pack/plugins/security_solution/server/endpoint/mocks.ts +++ b/x-pack/plugins/security_solution/server/endpoint/mocks.ts @@ -6,6 +6,8 @@ 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, @@ -63,8 +65,8 @@ export const createMockEndpointAppContextServiceStartContract = (): jest.Mocked< > => { return { agentService: createMockAgentService(), + logger: loggerMock.create(), savedObjectsStart: savedObjectsServiceMock.createStartContract(), - // @ts-ignore manifestManager: getManifestManagerMock(), registerIngestCallback: jest.fn< ReturnType, diff --git a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/lists.ts b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/lists.ts index b7f99fe6fe297c..ed97d04eecee65 100644 --- a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/lists.ts +++ b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/lists.ts @@ -7,38 +7,38 @@ import * as t from 'io-ts'; import { operator } from '../../../../../lists/common/schemas'; +export const translatedEntryMatchAnyMatcher = t.keyof({ + exact_cased_any: null, + exact_caseless_any: null, +}); +export type TranslatedEntryMatchAnyMatcher = t.TypeOf; + export const translatedEntryMatchAny = t.exact( t.type({ field: t.string, operator, - type: t.keyof({ - exact_cased_any: null, - exact_caseless_any: null, - }), + type: translatedEntryMatchAnyMatcher, value: t.array(t.string), }) ); export type TranslatedEntryMatchAny = t.TypeOf; -export const translatedEntryMatchAnyMatcher = translatedEntryMatchAny.type.props.type; -export type TranslatedEntryMatchAnyMatcher = t.TypeOf; +export const translatedEntryMatchMatcher = t.keyof({ + exact_cased: null, + exact_caseless: null, +}); +export type TranslatedEntryMatchMatcher = t.TypeOf; export const translatedEntryMatch = t.exact( t.type({ field: t.string, operator, - type: t.keyof({ - exact_cased: null, - exact_caseless: null, - }), + type: translatedEntryMatchMatcher, value: t.string, }) ); export type TranslatedEntryMatch = t.TypeOf; -export const translatedEntryMatchMatcher = translatedEntryMatch.type.props.type; -export type TranslatedEntryMatchMatcher = t.TypeOf; - export const translatedEntryMatcher = t.union([ translatedEntryMatchMatcher, translatedEntryMatchAnyMatcher, 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 dfbe2572076d00..3bdc5dfbcbd45b 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 @@ -4,9 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -// eslint-disable-next-line max-classes-per-file import { savedObjectsClientMock, loggingSystemMock } from 'src/core/server/mocks'; import { Logger } from 'src/core/server'; +import { createPackageConfigMock } 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 { @@ -21,40 +23,6 @@ import { getArtifactClientMock } from '../artifact_client.mock'; import { getManifestClientMock } from '../manifest_client.mock'; import { ManifestManager } from './manifest_manager'; -function getMockPackageConfig() { - return { - id: 'c6d16e42-c32d-4dce-8a88-113cfe276ad1', - inputs: [ - { - config: {}, - }, - ], - revision: 1, - version: 'abcd', // TODO: not yet implemented in ingest_manager (https://github.com/elastic/kibana/issues/69992) - updated_at: '2020-06-25T16:03:38.159292', - updated_by: 'kibana', - created_at: '2020-06-25T16:03:38.159292', - created_by: 'kibana', - }; -} - -class PackageConfigServiceMock { - public create = jest.fn().mockResolvedValue(getMockPackageConfig()); - public get = jest.fn().mockResolvedValue(getMockPackageConfig()); - public getByIds = jest.fn().mockResolvedValue([getMockPackageConfig()]); - public list = jest.fn().mockResolvedValue({ - items: [getMockPackageConfig()], - total: 1, - page: 1, - perPage: 20, - }); - public update = jest.fn().mockResolvedValue(getMockPackageConfig()); -} - -export function getPackageConfigServiceMock() { - return new PackageConfigServiceMock(); -} - async function mockBuildExceptionListArtifacts( os: string, schemaVersion: string @@ -66,27 +34,23 @@ async function mockBuildExceptionListArtifacts( return [await buildArtifact(exceptions, os, schemaVersion)]; } -// @ts-ignore export class ManifestManagerMock extends ManifestManager { - // @ts-ignore - private buildExceptionListArtifacts = async () => { - return mockBuildExceptionListArtifacts('linux', 'v1'); - }; + protected buildExceptionListArtifacts = jest + .fn() + .mockResolvedValue(mockBuildExceptionListArtifacts('linux', 'v1')); - // @ts-ignore - private getLastDispatchedManifest = jest + public getLastDispatchedManifest = jest .fn() .mockResolvedValue(new Manifest(new Date(), 'v1', ManifestConstants.INITIAL_VERSION)); - // @ts-ignore - private getManifestClient = jest + protected getManifestClient = jest .fn() .mockReturnValue(getManifestClientMock(this.savedObjectsClient)); } export const getManifestManagerMock = (opts?: { cache?: ExceptionsCache; - packageConfigService?: PackageConfigServiceMock; + packageConfigService?: jest.Mocked; savedObjectsClient?: ReturnType; }): ManifestManagerMock => { let cache = new ExceptionsCache(5); @@ -94,10 +58,14 @@ export const getManifestManagerMock = (opts?: { cache = opts.cache; } - let packageConfigService = getPackageConfigServiceMock(); + let packageConfigService = createPackageConfigServiceMock(); if (opts?.packageConfigService !== undefined) { packageConfigService = opts.packageConfigService; } + packageConfigService.list = jest.fn().mockResolvedValue({ + total: 1, + items: [createPackageConfigMock()], + }); let savedObjectsClient = savedObjectsClientMock.create(); if (opts?.savedObjectsClient !== undefined) { @@ -107,7 +75,6 @@ export const getManifestManagerMock = (opts?: { const manifestManager = new ManifestManagerMock({ artifactClient: getArtifactClientMock(savedObjectsClient), cache, - // @ts-ignore packageConfigService, exceptionListClient: listMock.getExceptionListClient(), logger: loggingSystemMock.create().get() as jest.Mocked, 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 b1cbc41459f15f..d092e7060f8aa9 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 @@ -6,13 +6,14 @@ import { inflateSync } from 'zlib'; import { savedObjectsClientMock } from 'src/core/server/mocks'; +import { createPackageConfigServiceMock } from '../../../../../../ingest_manager/server/mocks'; import { ArtifactConstants, ManifestConstants, Manifest, ExceptionsCache, } from '../../../lib/artifacts'; -import { getPackageConfigServiceMock, getManifestManagerMock } from './manifest_manager.mock'; +import { getManifestManagerMock } from './manifest_manager.mock'; describe('manifest_manager', () => { describe('ManifestManager sanity checks', () => { @@ -73,15 +74,15 @@ describe('manifest_manager', () => { }); test('ManifestManager can dispatch manifest', async () => { - const packageConfigService = getPackageConfigServiceMock(); + const packageConfigService = createPackageConfigServiceMock(); const manifestManager = getManifestManagerMock({ packageConfigService }); const snapshot = await manifestManager.getSnapshot(); const dispatched = await manifestManager.dispatch(snapshot!.manifest); - expect(dispatched).toEqual(true); + expect(dispatched).toEqual([]); 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 + packageConfigService.update.mock.calls[0][2].inputs[0].config!.artifact_manifest.value ).toEqual({ manifest_version: ManifestConstants.INITIAL_VERSION, schema_version: 'v1', @@ -115,7 +116,7 @@ describe('manifest_manager', () => { snapshot!.diffs.push(diff); const dispatched = await manifestManager.dispatch(snapshot!.manifest); - expect(dispatched).toEqual(true); + expect(dispatched).toEqual([]); await manifestManager.commit(snapshot!.manifest); 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 b9e289cee62af1..c8cad32ab746e2 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 @@ -61,19 +61,25 @@ export class ManifestManager { /** * Gets a ManifestClient for the provided schemaVersion. * - * @param schemaVersion + * @param schemaVersion The schema version of the manifest. + * @returns {ManifestClient} A ManifestClient scoped to the provided schemaVersion. */ - private getManifestClient(schemaVersion: string) { + protected getManifestClient(schemaVersion: string): ManifestClient { return new ManifestClient(this.savedObjectsClient, schemaVersion as ManifestSchemaVersion); } /** * Builds an array of artifacts (one per supported OS) based on the current - * state of exception-list-agnostic SO's. + * state of exception-list-agnostic SOs. * - * @param schemaVersion + * @param schemaVersion The schema version of the artifact + * @returns {Promise} An array of uncompressed artifacts built from exception-list-agnostic SOs. + * @throws Throws/rejects if there are errors building the list. */ - private async buildExceptionListArtifacts(schemaVersion: string) { + 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( @@ -90,13 +96,75 @@ export class ManifestManager { ); } + /** + * Writes new artifact SOs based on provided snapshot. + * + * @param snapshot A ManifestSnapshot to use for writing the artifacts. + * @returns {Promise} Any errors encountered. + */ + 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.` + ); + } + + 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'); + + 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? + errors.push(err); + } + } + } + return errors; + } + + /** + * Deletes old artifact SOs based on provided snapshot. + * + * @param snapshot A ManifestSnapshot to use for deleting the artifacts. + * @returns {Promise} Any errors encountered. + */ + private async deleteArtifacts(snapshot: ManifestSnapshot): Promise { + const errors: Error[] = []; + for (const diff of snapshot.diffs) { + 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}`); + } catch (err) { + errors.push(err); + } + } + return errors; + } + /** * Returns the last dispatched manifest based on the current state of the * user-artifact-manifest SO. * - * @param schemaVersion + * @param schemaVersion The schema version of the manifest. + * @returns {Promise} The last dispatched manifest, or null if does not exist. + * @throws Throws/rejects if there is an unexpected error retrieving the manifest. */ - private async getLastDispatchedManifest(schemaVersion: string) { + public async getLastDispatchedManifest(schemaVersion: string): Promise { try { const manifestClient = this.getManifestClient(schemaVersion); const manifestSo = await manifestClient.getManifest(); @@ -127,9 +195,11 @@ export class ManifestManager { /** * Snapshots a manifest based on current state of exception-list-agnostic SOs. * - * @param opts TODO + * @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. */ - public async getSnapshot(opts?: ManifestSnapshotOpts) { + public async getSnapshot(opts?: ManifestSnapshotOpts): Promise { try { let oldManifest: Manifest | null; @@ -176,71 +246,39 @@ export class ManifestManager { * Creates artifacts that do not yet exist and cleans up old artifacts that have been * superceded by this snapshot. * - * Can be filtered to apply one or both operations. - * - * @param snapshot - * @param diffType + * @param snapshot A ManifestSnapshot to use for sync. + * @returns {Promise} Any errors encountered. */ - public async syncArtifacts(snapshot: ManifestSnapshot, diffType?: 'add' | 'delete') { - const filteredDiffs = snapshot.diffs.reduce((diffs: ManifestDiff[], diff) => { - if (diff.type === diffType || diffType === undefined) { - diffs.push(diff); - } else if (!['add', 'delete'].includes(diff.type)) { - // TODO: replace with io-ts schema - throw new Error(`Unsupported diff type: ${diff.type}`); - } - return diffs; - }, []); - - const adds = filteredDiffs.filter((diff) => { - return diff.type === 'add'; + public async syncArtifacts( + snapshot: ManifestSnapshot, + diffType: 'add' | 'delete' + ): Promise { + const filteredDiffs = snapshot.diffs.filter((diff) => { + return diff.type === diffType; }); - const deletes = filteredDiffs.filter((diff) => { - return diff.type === 'delete'; - }); + const tmpSnapshot = { ...snapshot }; + tmpSnapshot.diffs = filteredDiffs; - for (const diff of adds) { - const artifact = snapshot.manifest.getArtifact(diff.id); - if (artifact === undefined) { - throw new Error( - `Corrupted manifest detected. Diff contained artifact ${diff.id} not in manifest.` - ); - } - 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'); - - try { - await this.artifactClient.createArtifact(artifact); - } catch (err) { - if (err.status === 409) { - this.logger.debug(`Tried to create artifact ${diff.id}, but it already exists.`); - } else { - throw err; - } - } - // Cache the body of the artifact - this.cache.set(diff.id, Buffer.from(artifact.body, 'base64')); + if (diffType === 'add') { + return this.writeArtifacts(tmpSnapshot); + } else if (diffType === 'delete') { + return this.deleteArtifacts(tmpSnapshot); } - for (const diff of deletes) { - await this.artifactClient.deleteArtifact(diff.id); - // TODO: should we delete the cache entry here? - this.logger.info(`Cleaned up artifact ${diff.id}`); - } + 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) { + public async dispatch(manifest: Manifest): Promise { let paging = true; let page = 1; - let success = true; + const errors: Error[] = []; while (paging) { const { items, total } = await this.packageConfigService.list(this.savedObjectsClient, { @@ -264,13 +302,10 @@ export class ManifestManager { `Updated package config ${id} with manifest version ${manifest.getVersion()}` ); } catch (err) { - success = false; - this.logger.debug(`Error updating package config ${id}`); - this.logger.error(err); + errors.push(err); } } else { - success = false; - this.logger.debug(`Package config ${id} has no config.`); + errors.push(new Error(`Package config ${id} has no config.`)); } } @@ -278,32 +313,38 @@ export class ManifestManager { page++; } - // TODO: revisit success logic - return success; + return errors; } /** * Commits a manifest to indicate that it has been dispatched. * - * @param manifest + * @param manifest The Manifest to commit. + * @returns {Promise} An error if encountered, or null if successful. */ - public async commit(manifest: Manifest) { - const manifestClient = this.getManifestClient(manifest.getSchemaVersion()); - - // Commit the new manifest - if (manifest.getVersion() === ManifestConstants.INITIAL_VERSION) { - await manifestClient.createManifest(manifest.toSavedObject()); - } else { - const version = manifest.getVersion(); - if (version === ManifestConstants.INITIAL_VERSION) { - throw new Error('Updating existing manifest with baseline version. Bad state.'); + 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()); + } 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(), { + version, + }); } - await manifestClient.updateManifest(manifest.toSavedObject(), { - version, - }); + + this.logger.info(`Committed manifest ${manifest.getVersion()}`); + } catch (err) { + return err; } - this.logger.info(`Committed manifest ${manifest.getVersion()}`); + return null; } /** diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts index db33775abeb0a7..7207bb3fc37b37 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts @@ -116,6 +116,48 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { version: policyInfo.packageInfo.version, }, }, + artifact_manifest: { + artifacts: { + 'endpoint-exceptionlist-linux-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/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-windows-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + }, + }, + manifest_version: 'WzEwNSwxXQ==', + schema_version: 'v1', + }, policy: { linux: { events: { file: false, network: true, process: true }, From 66c531d90366cfd08fc671c9bf2208dc102b87e2 Mon Sep 17 00:00:00 2001 From: Zacqary Adam Xeper Date: Fri, 10 Jul 2020 16:26:57 -0500 Subject: [PATCH 2/4] [Metrics UI] Round metric threshold time buckets to nearest unit (#71172) Co-authored-by: Elastic Machine --- .../metric_threshold/lib/evaluate_alert.ts | 25 +++++++++++-------- .../metric_threshold/lib/metric_query.test.ts | 22 ++++++++++++++++ .../metric_threshold/lib/metric_query.ts | 9 +++++-- .../metric_threshold_executor.test.ts | 2 ++ .../metric_threshold_executor.ts | 4 ++- .../alerting/metric_threshold/test_mocks.ts | 1 + .../infra/server/utils/round_timestamp.ts | 15 +++++++++++ 7 files changed, 65 insertions(+), 13 deletions(-) create mode 100644 x-pack/plugins/infra/server/utils/round_timestamp.ts diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts index de5eda4a1f2c38..7f6bf9551e2c1b 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts @@ -23,6 +23,7 @@ interface Aggregation { buckets: Array<{ aggregatedValue: { value: number; values?: Array<{ key: number; value: number }> }; doc_count: number; + key_as_string: string; }>; }; } @@ -57,17 +58,18 @@ export const evaluateAlert = ( ); const { threshold, comparator } = criterion; const comparisonFunction = comparatorMap[comparator]; - return mapValues(currentValues, (values: number | number[] | null) => { - if (isTooManyBucketsPreviewException(values)) throw values; + return mapValues(currentValues, (points: any[] | typeof NaN | null) => { + if (isTooManyBucketsPreviewException(points)) throw points; return { ...criterion, metric: criterion.metric ?? DOCUMENT_COUNT_I18N, - currentValue: Array.isArray(values) ? last(values) : NaN, - shouldFire: Array.isArray(values) - ? values.map((value) => comparisonFunction(value, threshold)) + currentValue: Array.isArray(points) ? last(points)?.value : NaN, + timestamp: Array.isArray(points) ? last(points)?.key : NaN, + shouldFire: Array.isArray(points) + ? points.map((point) => comparisonFunction(point.value, threshold)) : [false], - isNoData: values === null, - isError: isNaN(values), + isNoData: points === null, + isError: isNaN(points), }; }); }) @@ -157,17 +159,20 @@ const getValuesFromAggregations = ( const { buckets } = aggregations.aggregatedIntervals; if (!buckets.length) return null; // No Data state if (aggType === Aggregators.COUNT) { - return buckets.map((bucket) => bucket.doc_count); + return buckets.map((bucket) => ({ key: bucket.key_as_string, value: bucket.doc_count })); } if (aggType === Aggregators.P95 || aggType === Aggregators.P99) { return buckets.map((bucket) => { const values = bucket.aggregatedValue?.values || []; const firstValue = first(values); if (!firstValue) return null; - return firstValue.value; + return { key: bucket.key_as_string, value: firstValue.value }; }); } - return buckets.map((bucket) => bucket.aggregatedValue.value); + return buckets.map((bucket) => ({ + key: bucket.key_as_string, + value: bucket.aggregatedValue.value, + })); } catch (e) { return NaN; // Error state } diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.test.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.test.ts index 3ad1031f574e25..b4fe8f053a44a8 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.test.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.test.ts @@ -56,4 +56,26 @@ describe("The Metric Threshold Alert's getElasticsearchMetricQuery", () => { ); }); }); + + describe('handles time', () => { + const end = new Date('2020-07-08T22:07:27.235Z').valueOf(); + const timerange = { + end, + start: end - 5 * 60 * 1000, + }; + const searchBody = getElasticsearchMetricQuery( + expressionParams, + timefield, + undefined, + undefined, + timerange + ); + test('by rounding timestamps to the nearest timeUnit', () => { + const rangeFilter = searchBody.query.bool.filter.find((filter) => + filter.hasOwnProperty('range') + )?.range[timefield]; + expect(rangeFilter?.lte).toBe(1594246020000); + expect(rangeFilter?.gte).toBe(1594245720000); + }); + }); }); diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.ts index 15506a30529c4e..078ca46d42e60d 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.ts @@ -3,9 +3,11 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + import { networkTraffic } from '../../../../../common/inventory_models/shared/metrics/snapshot/network_traffic'; import { MetricExpressionParams, Aggregators } from '../types'; import { getIntervalInSeconds } from '../../../../utils/get_interval_in_seconds'; +import { roundTimestamp } from '../../../../utils/round_timestamp'; import { getDateHistogramOffset } from '../../../snapshot/query_helpers'; import { createPercentileAggregation } from './create_percentile_aggregation'; @@ -34,12 +36,15 @@ export const getElasticsearchMetricQuery = ( const interval = `${timeSize}${timeUnit}`; const intervalAsSeconds = getIntervalInSeconds(interval); - const to = timeframe ? timeframe.end : Date.now(); + const to = roundTimestamp(timeframe ? timeframe.end : Date.now(), timeUnit); // We need enough data for 5 buckets worth of data. We also need // to convert the intervalAsSeconds to milliseconds. const minimumFrom = to - intervalAsSeconds * 1000 * MINIMUM_BUCKETS; - const from = timeframe && timeframe.start <= minimumFrom ? timeframe.start : minimumFrom; + const from = roundTimestamp( + timeframe && timeframe.start <= minimumFrom ? timeframe.start : minimumFrom, + timeUnit + ); const offset = getDateHistogramOffset(from, interval); diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts index 24f4bc2c678b41..003a6c3c20e986 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts @@ -94,12 +94,14 @@ describe('The metric threshold alert type', () => { expect(getState(instanceID).alertState).toBe(AlertStates.OK); }); test('reports expected values to the action context', async () => { + const now = 1577858400000; await execute(Comparator.GT, [0.75]); const { action } = mostRecentAction(instanceID); expect(action.group).toBe('*'); expect(action.reason).toContain('current value is 1'); expect(action.reason).toContain('threshold of 0.75'); expect(action.reason).toContain('test.metric.1'); + expect(action.timestamp).toBe(new Date(now).toISOString()); }); }); diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts index 4c02593dd00954..bc1cc24f65eebf 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts @@ -76,11 +76,13 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs, alertId: s } } if (reason) { + const firstResult = first(alertResults); + const timestamp = (firstResult && firstResult[group].timestamp) ?? moment().toISOString(); alertInstance.scheduleActions(FIRED_ACTIONS.id, { group, alertState: stateToAlertMessage[nextState], reason, - timestamp: moment().toISOString(), + timestamp, value: mapToConditionsLookup(alertResults, (result) => result[group].currentValue), threshold: mapToConditionsLookup(criteria, (c) => c.threshold), metric: mapToConditionsLookup(criteria, (c) => c.metric), diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts index ee2cf94a2fd627..c7e53eb2008f54 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts @@ -12,6 +12,7 @@ const bucketsA = [ { doc_count: 3, aggregatedValue: { value: 1.0, values: [{ key: 95.0, value: 1.0 }] }, + key_as_string: new Date(1577858400000).toISOString(), }, ]; diff --git a/x-pack/plugins/infra/server/utils/round_timestamp.ts b/x-pack/plugins/infra/server/utils/round_timestamp.ts new file mode 100644 index 00000000000000..9b5ae2ac401972 --- /dev/null +++ b/x-pack/plugins/infra/server/utils/round_timestamp.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Unit } from '@elastic/datemath'; +import moment from 'moment'; + +export const roundTimestamp = (timestamp: number, unit: Unit) => { + const floor = moment(timestamp).startOf(unit).valueOf(); + const ceil = moment(timestamp).add(1, unit).startOf(unit).valueOf(); + if (Math.abs(timestamp - floor) <= Math.abs(timestamp - ceil)) return floor; + return ceil; +}; From e1253ed0478b5c0276d5520d9f54a167a325fe0e Mon Sep 17 00:00:00 2001 From: Madison Caldwell Date: Fri, 10 Jul 2020 18:23:51 -0400 Subject: [PATCH 3/4] [Security Solution][Ingest Manager][Endpoint] Optional ingest manager (#71198) --- x-pack/plugins/security_solution/kibana.json | 5 +++-- .../plugins/security_solution/public/plugin.tsx | 10 ++++++---- x-pack/plugins/security_solution/public/types.ts | 2 +- .../endpoint_app_context_services.test.ts | 16 ++++++++-------- 4 files changed, 18 insertions(+), 15 deletions(-) diff --git a/x-pack/plugins/security_solution/kibana.json b/x-pack/plugins/security_solution/kibana.json index 40d3402378895c..29d0ab58e8b554 100644 --- a/x-pack/plugins/security_solution/kibana.json +++ b/x-pack/plugins/security_solution/kibana.json @@ -11,7 +11,6 @@ "embeddable", "features", "home", - "ingestManager", "taskManager", "inspector", "licensing", @@ -21,6 +20,7 @@ ], "optionalPlugins": [ "encryptedSavedObjects", + "ingestManager", "ml", "newsfeed", "security", @@ -33,6 +33,7 @@ "requiredBundles": [ "kibanaUtils", "esUiShared", - "kibanaReact" + "kibanaReact", + "ingestManager" ] } diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index 7bb4be6b508797..62328bd7677488 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -324,10 +324,12 @@ export class Plugin implements IPlugin { - // it('should return undefined on getAgentService if dependencies are not enabled', async () => { - // const endpointAppContextService = new EndpointAppContextService(); - // expect(endpointAppContextService.getAgentService()).toEqual(undefined); - // }); - // it('should return undefined on getManifestManager if dependencies are not enabled', async () => { - // const endpointAppContextService = new EndpointAppContextService(); - // expect(endpointAppContextService.getManifestManager()).toEqual(undefined); - // }); + it('should return undefined on getAgentService if dependencies are not enabled', async () => { + const endpointAppContextService = new EndpointAppContextService(); + expect(endpointAppContextService.getAgentService()).toEqual(undefined); + }); + it('should return undefined on getManifestManager if dependencies are not enabled', async () => { + const endpointAppContextService = new EndpointAppContextService(); + expect(endpointAppContextService.getManifestManager()).toEqual(undefined); + }); it('should throw error on getScopedSavedObjectsClient if start is not called', async () => { const endpointAppContextService = new EndpointAppContextService(); expect(() => From 79df8e01ad78d0b4ecb24edad14203c116990128 Mon Sep 17 00:00:00 2001 From: Liza Katz Date: Mon, 13 Jul 2020 09:26:34 +0300 Subject: [PATCH 4/4] improve bugfix 7198 test stability (#71250) * improve test stability * reenabled test Co-authored-by: Elastic Machine --- .../apps/discover/_doc_navigation.js | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/test/functional/apps/discover/_doc_navigation.js b/test/functional/apps/discover/_doc_navigation.js index 9bcf7fd2d73b57..5ae799f8756c09 100644 --- a/test/functional/apps/discover/_doc_navigation.js +++ b/test/functional/apps/discover/_doc_navigation.js @@ -29,7 +29,7 @@ export default function ({ getService, getPageObjects }) { const retry = getService('retry'); // Flaky: https://github.com/elastic/kibana/issues/71216 - describe.skip('doc link in discover', function contextSize() { + describe('doc link in discover', function contextSize() { beforeEach(async function () { log.debug('load kibana index with default index pattern'); await esArchiver.loadIfNeeded('discover'); @@ -63,20 +63,28 @@ export default function ({ getService, getPageObjects }) { await filterBar.addFilter('agent', 'is', 'Missing/Fields'); await PageObjects.discover.waitUntilSearchingHasFinished(); - // navigate to the doc view - await docTable.clickRowToggle({ rowIndex: 0 }); + await retry.try(async () => { + // navigate to the doc view + await docTable.clickRowToggle({ rowIndex: 0 }); - const details = await docTable.getDetailsRow(); - await docTable.addInclusiveFilter(details, 'referer'); - await PageObjects.discover.waitUntilSearchingHasFinished(); + const details = await docTable.getDetailsRow(); + await docTable.addInclusiveFilter(details, 'referer'); + await PageObjects.discover.waitUntilSearchingHasFinished(); - const hasInclusiveFilter = await filterBar.hasFilter('referer', 'exists', true, false, true); - expect(hasInclusiveFilter).to.be(true); + const hasInclusiveFilter = await filterBar.hasFilter( + 'referer', + 'exists', + true, + false, + true + ); + expect(hasInclusiveFilter).to.be(true); - await docTable.removeInclusiveFilter(details, 'referer'); - await PageObjects.discover.waitUntilSearchingHasFinished(); - const hasExcludeFilter = await filterBar.hasFilter('referer', 'exists', true, false, false); - expect(hasExcludeFilter).to.be(true); + await docTable.removeInclusiveFilter(details, 'referer'); + await PageObjects.discover.waitUntilSearchingHasFinished(); + const hasExcludeFilter = await filterBar.hasFilter('referer', 'exists', true, false, false); + expect(hasExcludeFilter).to.be(true); + }); }); }); }