From 3d1d2a5d43416aecd06744cca1a0c1242362248b Mon Sep 17 00:00:00 2001 From: Konrad Szwarc Date: Fri, 17 May 2024 16:57:12 +0200 Subject: [PATCH 1/6] [EDR Workflows] Unified Manifest - Manifest Manager (#179698) This pull request introduces a new SO used for keeping relations between artifactId and policyId. It addresses an issue where some users found the single SO structure containing too many nested entries. Originally, we planned to rewrite the existing Manifest Manager. However, during the POC implementation, it became clear that the effort required to refactor and retest the existing solution would be substantial. Therefore, this pull request can be considered as the first step in transitioning our approach from one SO to this new, distributed one. The main idea behind these changes is to modify the structure of the SO, rather than the logic of the Manifest Manager. To accomplish this, we need to retrieve the SO from the new source, translate it into the existing SO format (many SOs to one), execute the unchanged operations of the Manifest Manager on artifacts, translate the resulting SO into multiple SOs, and save them. This change is expected to be deployed with a Feature Flag, and we need to ensure that everything continues to function correctly in both cases. Therefore, I've introduced a new FTR suite with the Feature Flag enabled, which should be run alongside tests with the Feature Flag disabled. This suite contains duplicated test files that depend on SO logic. When we activate the Feature Flag, these tests should replace the existing ones, as the Unified Manifest SO will become the default approach. It appears that there is no need to introduce any kind of migrations, as the Manifest Manager is capable of recreating missing SOs (which has been tested). --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .buildkite/ftr_configs.yml | 2 + .../src/constants.ts | 1 + .../current_fields.json | 5 + .../current_mappings.json | 14 + .../check_registered_types.test.ts | 1 + .../group3/type_registrations.test.ts | 1 + .../group5/dot_kibana_split.test.ts | 1 + .../common/experimental_features.ts | 5 + .../server/endpoint/lib/artifacts/task.ts | 1 - .../schemas/artifacts/saved_objects.ts | 1 + .../manifest_manager/manifest_manager.mock.ts | 4 +- .../manifest_manager/manifest_manager.test.ts | 1209 ++++++++++++----- .../manifest_manager/manifest_manager.ts | 283 +++- .../artifacts/unified_manifes_client.test.ts | 27 +- .../artifacts/unified_manifest_client.ts | 19 +- .../endpoint/services/artifacts/utils.ts | 2 + .../security_solution/server/saved_objects.ts | 3 +- .../integrations/artifact_entries_list.ts | 4 +- .../artifact_entries_list.ts | 376 +++++ .../endpoint_exceptions.ts | 245 ++++ .../apps/integrations_feature_flag/index.ts | 48 + .../apps/integrations_feature_flag/mocks.ts | 807 +++++++++++ .../integrations_feature_flag.config.ts | 34 + ...erless.integrations_feature_flag.config.ts | 35 + .../services/endpoint_artifacts.ts | 27 +- 25 files changed, 2795 insertions(+), 360 deletions(-) create mode 100644 x-pack/test/security_solution_endpoint/apps/integrations_feature_flag/artifact_entries_list.ts create mode 100644 x-pack/test/security_solution_endpoint/apps/integrations_feature_flag/endpoint_exceptions.ts create mode 100644 x-pack/test/security_solution_endpoint/apps/integrations_feature_flag/index.ts create mode 100644 x-pack/test/security_solution_endpoint/apps/integrations_feature_flag/mocks.ts create mode 100644 x-pack/test/security_solution_endpoint/integrations_feature_flag.config.ts create mode 100644 x-pack/test/security_solution_endpoint/serverless.integrations_feature_flag.config.ts diff --git a/.buildkite/ftr_configs.yml b/.buildkite/ftr_configs.yml index ac53265b1c979..5b1734613c0f6 100644 --- a/.buildkite/ftr_configs.yml +++ b/.buildkite/ftr_configs.yml @@ -403,7 +403,9 @@ enabled: - x-pack/test/security_solution_endpoint/endpoint.config.ts - x-pack/test/security_solution_endpoint/serverless.endpoint.config.ts - x-pack/test/security_solution_endpoint/integrations.config.ts + - x-pack/test/security_solution_endpoint/integrations_feature_flag.config.ts - x-pack/test/security_solution_endpoint/serverless.integrations.config.ts + - x-pack/test/security_solution_endpoint/serverless.integrations_feature_flag.config.ts - x-pack/test/session_view/basic/config.ts - x-pack/test/spaces_api_integration/security_and_spaces/config_basic.ts - x-pack/test/spaces_api_integration/security_and_spaces/copy_to_space_config_basic.ts diff --git a/packages/core/saved-objects/core-saved-objects-base-server-internal/src/constants.ts b/packages/core/saved-objects/core-saved-objects-base-server-internal/src/constants.ts index a04e762eaf67d..4244a5fed91aa 100644 --- a/packages/core/saved-objects/core-saved-objects-base-server-internal/src/constants.ts +++ b/packages/core/saved-objects/core-saved-objects-base-server-internal/src/constants.ts @@ -157,6 +157,7 @@ export const HASH_TO_VERSION_MAP = { 'core-usage-stats|3d1b76c39bfb2cc8296b024d73854724': '7.14.1', 'csp-rule-template|6ee70dc06c0ca3ddffc18222f202ab25': '10.0.0', 'dashboard|b8aa800aa5e0d975c5e8dc57f03d41f8': '10.2.0', + 'endpoint:unified-user-artifact-manifest|393c6e4f5f16288c24ef9057e4d76a4c': '10.0.0', 'endpoint:user-artifact-manifest|7502b5c5bc923abe8aa5ccfd636e8c3d': '10.0.0', 'enterprise_search_telemetry|3d1b76c39bfb2cc8296b024d73854724': '10.0.0', 'epm-packages-assets|44621b2f6052ef966da47b7c3a00f33b': '10.0.0', diff --git a/packages/kbn-check-mappings-update-cli/current_fields.json b/packages/kbn-check-mappings-update-cli/current_fields.json index 65fd6af2ce919..04343968ef958 100644 --- a/packages/kbn-check-mappings-update-cli/current_fields.json +++ b/packages/kbn-check-mappings-update-cli/current_fields.json @@ -280,6 +280,11 @@ "title", "version" ], + "endpoint:unified-user-artifact-manifest": [ + "artifactIds", + "policyId", + "semanticVersion" + ], "endpoint:user-artifact-manifest": [ "artifacts", "schemaVersion" diff --git a/packages/kbn-check-mappings-update-cli/current_mappings.json b/packages/kbn-check-mappings-update-cli/current_mappings.json index 03a189b1549be..6ee1f6d13521f 100644 --- a/packages/kbn-check-mappings-update-cli/current_mappings.json +++ b/packages/kbn-check-mappings-update-cli/current_mappings.json @@ -964,6 +964,20 @@ } } }, + "endpoint:unified-user-artifact-manifest": { + "dynamic": false, + "properties": { + "artifactIds": { + "type": "keyword" + }, + "policyId": { + "type": "keyword" + }, + "semanticVersion": { + "type": "keyword" + } + } + }, "endpoint:user-artifact-manifest": { "dynamic": false, "properties": { diff --git a/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts b/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts index 2fa22873190e5..6b8a29c70227b 100644 --- a/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts +++ b/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts @@ -85,6 +85,7 @@ describe('checking migration metadata changes on all registered SO types', () => "core-usage-stats": "b3c04da317c957741ebcdedfea4524049fdc79ff", "csp-rule-template": "c151324d5f85178169395eecb12bac6b96064654", "dashboard": "211e9ca30f5a95d5f3c27b1bf2b58e6cfa0c9ae9", + "endpoint:unified-user-artifact-manifest": "71c7fcb52c658b21ea2800a6b6a76972ae1c776e", "endpoint:user-artifact-manifest": "1c3533161811a58772e30cdc77bac4631da3ef2b", "enterprise_search_telemetry": "9ac912e1417fc8681e0cd383775382117c9e3d3d", "epm-packages": "f8ee125b57df31fd035dc04ad81aef475fd2f5bd", diff --git a/src/core/server/integration_tests/saved_objects/migrations/group3/type_registrations.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group3/type_registrations.test.ts index 43f0d03b86552..712ddd4bca932 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/group3/type_registrations.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/group3/type_registrations.test.ts @@ -49,6 +49,7 @@ const previouslyRegisteredTypes = [ 'event-annotation-group', 'endpoint:user-artifact', 'endpoint:user-artifact-manifest', + 'endpoint:unified-user-artifact-manifest', 'enterprise_search_telemetry', 'epm-packages', 'epm-packages-assets', diff --git a/src/core/server/integration_tests/saved_objects/migrations/group5/dot_kibana_split.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group5/dot_kibana_split.test.ts index 22a3b9858599a..dcb40a3b07621 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/group5/dot_kibana_split.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/group5/dot_kibana_split.test.ts @@ -205,6 +205,7 @@ describe('split .kibana index into multiple system indices', () => { "connector_token", "core-usage-stats", "csp-rule-template", + "endpoint:unified-user-artifact-manifest", "endpoint:user-artifact-manifest", "enterprise_search_telemetry", "epm-packages", diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts index 3a1fd7ce8b734..da993a18debe3 100644 --- a/x-pack/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/plugins/security_solution/common/experimental_features.ts @@ -252,6 +252,11 @@ export const allowedExperimentalValues = Object.freeze({ */ malwareOnWriteScanOptionAvailable: true, + /** + * Enables unified manifest that replaces existing user artifacts manifest SO with a new approach of creating a SO per package policy. + */ + unifiedManifestEnabled: false, + /** * Enables Security AI Assistant's Flyout mode */ 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 8066e6094b568..d0c4d825d57d6 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 @@ -170,7 +170,6 @@ export class ManifestTask { this.logger.error( `unable to recover from error while attempting to retrieve last computed manifest` ); - return; } } diff --git a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/saved_objects.ts b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/saved_objects.ts index f0bc3a80d3406..4ca6d67031662 100644 --- a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/saved_objects.ts +++ b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/saved_objects.ts @@ -109,6 +109,7 @@ export const internalUnifiedManifestSchema = t.intersection([ t.type({ id: identifier, created: t.union([t.string, t.undefined]), + version: t.union([t.string, t.undefined]), }) ), ]); 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 a0cbc058ba6a8..31ac67b2368d6 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 @@ -72,6 +72,7 @@ export interface ManifestManagerMockOptions { packagePolicyService: jest.Mocked; savedObjectsClient: ReturnType; productFeaturesService: ProductFeaturesService; + experimentalFeatures?: string[]; } export const buildManifestManagerMockOptions = ( @@ -98,7 +99,8 @@ export const buildManifestManagerContextMock = ( ...fullOpts, artifactClient: createEndpointArtifactClientMock(), logger: loggingSystemMock.create().get() as jest.Mocked, - experimentalFeatures: parseExperimentalConfigValue([]).features, + experimentalFeatures: parseExperimentalConfigValue([...(opts.experimentalFeatures ?? [])]) + .features, packagerTaskPackagePolicyUpdateBatchSize: 10, esClient: elasticsearchServiceMock.createElasticsearchClient(), }; 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 7b996c7737c89..d6e82322ffa9c 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 @@ -14,6 +14,7 @@ import type { InternalArtifactCompleteSchema, InternalArtifactSchema, InternalManifestSchema, + InternalUnifiedManifestSchema, } from '../../../schemas/artifacts'; import { createPackagePolicyWithConfigMock, @@ -32,6 +33,7 @@ import { mockFindExceptionListItemResponses, } from './manifest_manager.mock'; +import type { ManifestManagerContext } from './manifest_manager'; import { ManifestManager } from './manifest_manager'; import type { EndpointArtifactClientInterface } from '../artifact_client'; import { InvalidInternalManifestError } from '../errors'; @@ -107,6 +109,168 @@ describe('ManifestManager', () => { ARTIFACT_TRUSTED_APPS_WINDOWS = ARTIFACTS[4]; }); + describe('getLastComputedManifest from Unified Manifest SO', () => { + const mockGetAllUnifiedManifestsSOFromCache = jest.fn().mockImplementation(() => [ + { + policyId: '.global', + semanticVersion: '1.0.0', + artifactIds: [ + ARTIFACT_ID_EXCEPTIONS_MACOS, + ARTIFACT_ID_EXCEPTIONS_WINDOWS, + ARTIFACT_ID_EXCEPTIONS_LINUX, + ], + created: '20-01-2020 10:00:00.000Z', + id: '3', + }, + { + policyId: TEST_POLICY_ID_1, + semanticVersion: '1.0.0', + artifactIds: [ + ARTIFACT_ID_EXCEPTIONS_WINDOWS, + ARTIFACT_ID_TRUSTED_APPS_MACOS, + ARTIFACT_ID_TRUSTED_APPS_WINDOWS, + ], + created: '20-01-2020 10:00:00.000Z', + id: '1', + }, + { + policyId: TEST_POLICY_ID_2, + semanticVersion: '1.0.0', + artifactIds: [ARTIFACT_ID_TRUSTED_APPS_WINDOWS], + created: '20-01-2020 10:00:00.000Z', + id: '2', + }, + ]); + + test('Retrieves empty unified manifest successfully', async () => { + const savedObjectsClient = savedObjectsClientMock.create(); + const manifestManager = new ManifestManager( + buildManifestManagerContextMock({ + savedObjectsClient, + experimentalFeatures: ['unifiedManifestEnabled'], + }) + ); + + manifestManager.getAllUnifiedManifestsSO = jest.fn().mockImplementation(() => []); + + const manifest = await manifestManager.getLastComputedManifest(); + + expect(manifest?.getSchemaVersion()).toStrictEqual('v1'); + expect(manifest?.getSemanticVersion()).toStrictEqual('1.0.0'); + expect(manifest?.getSavedObjectVersion()).toStrictEqual('WzQ3NzAsMV0='); + expect(manifest?.getAllArtifacts()).toStrictEqual([]); + }); + + test('Retrieves empty unified manifest successfully but uses semanticVersion from existing legacy SO manifest', async () => { + const semanticVersion = '1.14.0'; + const savedObjectsClient = savedObjectsClientMock.create(); + const manifestManager = new ManifestManager( + buildManifestManagerContextMock({ + savedObjectsClient, + experimentalFeatures: ['unifiedManifestEnabled'], + }) + ); + + savedObjectsClient.get = jest.fn().mockImplementation(async (objectType: string) => { + if (objectType === ManifestConstants.SAVED_OBJECT_TYPE) { + return { + attributes: { + artifacts: [ + { artifactId: ARTIFACT_ID_EXCEPTIONS_MACOS, policyId: undefined }, + { artifactId: ARTIFACT_ID_EXCEPTIONS_WINDOWS, policyId: undefined }, + ], + semanticVersion, + }, + }; + } else { + return null; + } + }); + + manifestManager.getAllUnifiedManifestsSO = jest.fn().mockImplementation(() => []); + + const manifest = await manifestManager.getLastComputedManifest(); + + expect(manifest?.getSchemaVersion()).toStrictEqual('v1'); + expect(manifest?.getSemanticVersion()).toStrictEqual(semanticVersion); + expect(manifest?.getSavedObjectVersion()).toStrictEqual('WzQ3NzAsMV0='); + expect(manifest?.getAllArtifacts()).toStrictEqual([]); + }); + + test('Retrieves non empty manifest succesfully from Unified Saved Object', async () => { + const savedObjectsClient = savedObjectsClientMock.create(); + const manifestManagerContext = buildManifestManagerContextMock({ + savedObjectsClient, + experimentalFeatures: ['unifiedManifestEnabled'], + }); + const manifestManager = new ManifestManager(manifestManagerContext); + + ( + manifestManagerContext.artifactClient as jest.Mocked + ).fetchAll.mockReturnValue(createFetchAllArtifactsIterableMock([ARTIFACTS as Artifact[]])); + + manifestManager.getAllUnifiedManifestsSO = mockGetAllUnifiedManifestsSOFromCache; + + const manifest = await manifestManager.getLastComputedManifest(); + + expect(manifest?.getSchemaVersion()).toStrictEqual('v1'); + expect(manifest?.getSemanticVersion()).toStrictEqual('1.0.0'); + expect(manifest?.getSavedObjectVersion()).toStrictEqual('WzQ3NzAsMV0='); + expect(manifest?.getAllArtifacts()).toStrictEqual(ARTIFACTS.slice(0, 5)); + expect(manifest?.isDefaultArtifact(ARTIFACT_EXCEPTIONS_MACOS)).toBe(true); + expect(manifest?.getArtifactTargetPolicies(ARTIFACT_EXCEPTIONS_MACOS)).toStrictEqual( + new Set() + ); + expect(manifest?.isDefaultArtifact(ARTIFACT_EXCEPTIONS_WINDOWS)).toBe(true); + expect(manifest?.getArtifactTargetPolicies(ARTIFACT_EXCEPTIONS_WINDOWS)).toStrictEqual( + new Set([TEST_POLICY_ID_1]) + ); + expect(manifest?.isDefaultArtifact(ARTIFACT_TRUSTED_APPS_MACOS)).toBe(false); + expect(manifest?.getArtifactTargetPolicies(ARTIFACT_TRUSTED_APPS_MACOS)).toStrictEqual( + new Set([TEST_POLICY_ID_1]) + ); + expect(manifest?.isDefaultArtifact(ARTIFACT_TRUSTED_APPS_WINDOWS)).toBe(false); + expect(manifest?.getArtifactTargetPolicies(ARTIFACT_TRUSTED_APPS_WINDOWS)).toStrictEqual( + new Set([TEST_POLICY_ID_1, TEST_POLICY_ID_2]) + ); + }); + + test("Retrieve non empty unified manifest and skips over artifacts that can't be found", async () => { + const savedObjectsClient = savedObjectsClientMock.create(); + const manifestManagerContext = buildManifestManagerContextMock({ + savedObjectsClient, + experimentalFeatures: ['unifiedManifestEnabled'], + }); + const manifestManager = new ManifestManager(manifestManagerContext); + + manifestManager.getAllUnifiedManifestsSO = mockGetAllUnifiedManifestsSOFromCache; + + ( + manifestManagerContext.artifactClient as jest.Mocked + ).fetchAll.mockReturnValue( + createFetchAllArtifactsIterableMock([ + // report the MACOS Exceptions artifact as not found + [ + ARTIFACT_TRUSTED_APPS_MACOS, + ARTIFACT_EXCEPTIONS_WINDOWS, + ARTIFACT_TRUSTED_APPS_WINDOWS, + ARTIFACTS_BY_ID[ARTIFACT_ID_EXCEPTIONS_LINUX], + ] as Artifact[], + ]) + ); + + const manifest = await manifestManager.getLastComputedManifest(); + + expect(manifest?.getAllArtifacts()).toStrictEqual(ARTIFACTS.slice(1, 5)); + + expect(manifestManagerContext.logger.warn).toHaveBeenCalledWith( + "Missing artifacts detected! Internal artifact manifest (SavedObject version [WzQ3NzAsMV0=]) references [1] artifact IDs that don't exist.\n" + + "First 10 below (run with logging set to 'debug' to see all):\n" + + 'endpoint-exceptionlist-macos-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3' + ); + }); + }); + describe('getLastComputedManifest', () => { test('Returns null when saved object not found', async () => { const savedObjectsClient = savedObjectsClientMock.create(); @@ -282,7 +446,197 @@ describe('ManifestManager', () => { }); }); - describe('buildNewManifest', () => { + describe('commit unified manifest', () => { + test('Correctly updates, creates and deletes unified manifest so', async () => { + const context = buildManifestManagerContextMock({ + experimentalFeatures: ['unifiedManifestEnabled'], + }); + const manifestManager = new ManifestManager(context); + const manifest = ManifestManager.createDefaultManifest(); + + manifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS); + manifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS, TEST_POLICY_ID_1); + manifest.addEntry(ARTIFACT_TRUSTED_APPS_WINDOWS, TEST_POLICY_ID_1); + manifest.addEntry(ARTIFACT_EXCEPTIONS_WINDOWS, TEST_POLICY_ID_2); + manifest.addEntry(ARTIFACT_TRUSTED_APPS_MACOS, TEST_POLICY_ID_1); + manifest.addEntry(ARTIFACT_TRUSTED_APPS_MACOS, TEST_POLICY_ID_2); + + manifestManager.getAllUnifiedManifestsSO = jest.fn().mockImplementation(() => [ + { + policyId: '.global', + semanticVersion: '1.0.0', + artifactIds: [ARTIFACT_ID_EXCEPTIONS_MACOS], + created: '20-01-2020 10:00:00.000Z', + id: '2', + }, + { + policyId: TEST_POLICY_ID_1, + semanticVersion: '1.0.0', + artifactIds: [ARTIFACT_ID_EXCEPTIONS_MACOS, ARTIFACT_ID_TRUSTED_APPS_MACOS], + created: '20-01-2020 10:00:00.000Z', + id: '3', + }, + { + policyId: 'non-existent-policy', + semanticVersion: '1.0.0', + artifactIds: [ARTIFACT_ID_EXCEPTIONS_WINDOWS], + created: '20-01-2020 10:00:00.000Z', + id: '4', + }, + ]); + + context.savedObjectsClient.bulkCreate = jest.fn(); + context.savedObjectsClient.bulkUpdate = jest.fn(); + context.savedObjectsClient.bulkDelete = jest.fn(); + manifestManager.bumpGlobalUnifiedManifestVersion = jest.fn(); + + await expect(manifestManager.commit(manifest)).resolves.toBeUndefined(); + expect(context.savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); + // TEST_POLICY_ID_1 and .global exists, shouldn't be created + expect(context.savedObjectsClient.bulkCreate).toHaveBeenCalledWith( + [ + { + attributes: { + artifactIds: [ARTIFACT_ID_EXCEPTIONS_WINDOWS, ARTIFACT_ID_TRUSTED_APPS_MACOS], + id: undefined, + policyId: TEST_POLICY_ID_2, + semanticVersion: '1.0.0', + }, + type: ManifestConstants.UNIFIED_SAVED_OBJECT_TYPE, + }, + ], + { initialNamespaces: ['*'] } + ); + + expect(context.savedObjectsClient.bulkUpdate).toHaveBeenCalledTimes(1); + // TEST_POLICY_ID_1 is updated, global is not due to no changes + expect(context.savedObjectsClient.bulkUpdate).toHaveBeenCalledWith([ + { + attributes: { + artifactIds: [ + ARTIFACT_ID_EXCEPTIONS_MACOS, + ARTIFACT_ID_TRUSTED_APPS_WINDOWS, + ARTIFACT_ID_TRUSTED_APPS_MACOS, + ], + policyId: TEST_POLICY_ID_1, + semanticVersion: '1.0.1', + }, + id: '3', + type: ManifestConstants.UNIFIED_SAVED_OBJECT_TYPE, + }, + ]); + + // non-existent-policy should be deleted for not being in the manifest + expect(context.savedObjectsClient.bulkDelete).toHaveBeenCalledTimes(1); + expect(context.savedObjectsClient.bulkDelete).toHaveBeenCalledWith([ + { id: '4', type: ManifestConstants.UNIFIED_SAVED_OBJECT_TYPE }, + ]); + // Global manifest wasn't updated, manual bump is required + expect(manifestManager.bumpGlobalUnifiedManifestVersion).toHaveBeenCalledTimes(1); + }); + }); + + describe('commit', () => { + test('Creates new saved object if no saved object version', async () => { + const context = buildManifestManagerContextMock({}); + const manifestManager = new ManifestManager(context); + const manifest = ManifestManager.createDefaultManifest(); + + manifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS); + manifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS, TEST_POLICY_ID_1); + manifest.addEntry(ARTIFACT_EXCEPTIONS_WINDOWS, TEST_POLICY_ID_2); + manifest.addEntry(ARTIFACT_TRUSTED_APPS_MACOS, TEST_POLICY_ID_1); + manifest.addEntry(ARTIFACT_TRUSTED_APPS_MACOS, TEST_POLICY_ID_2); + + context.savedObjectsClient.create = jest + .fn() + .mockImplementation((_type: string, object: InternalManifestSchema) => object); + + await expect(manifestManager.commit(manifest)).resolves.toBeUndefined(); + + expect(context.savedObjectsClient.create).toHaveBeenCalledTimes(1); + expect(context.savedObjectsClient.create).toHaveBeenNthCalledWith( + 1, + ManifestConstants.SAVED_OBJECT_TYPE, + { + artifacts: [ + { artifactId: ARTIFACT_ID_EXCEPTIONS_MACOS, policyId: undefined }, + { artifactId: ARTIFACT_ID_EXCEPTIONS_MACOS, policyId: TEST_POLICY_ID_1 }, + { artifactId: ARTIFACT_ID_TRUSTED_APPS_MACOS, policyId: TEST_POLICY_ID_1 }, + { artifactId: ARTIFACT_ID_EXCEPTIONS_WINDOWS, policyId: TEST_POLICY_ID_2 }, + { artifactId: ARTIFACT_ID_TRUSTED_APPS_MACOS, policyId: TEST_POLICY_ID_2 }, + ], + schemaVersion: 'v1', + semanticVersion: '1.0.0', + created: expect.anything(), + }, + { id: 'endpoint-manifest-v1' } + ); + }); + + test('Updates existing saved object if has saved object version', async () => { + const context = buildManifestManagerContextMock({}); + const manifestManager = new ManifestManager(context); + const manifest = new Manifest({ soVersion: '1.0.0' }); + + manifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS); + manifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS, TEST_POLICY_ID_1); + manifest.addEntry(ARTIFACT_EXCEPTIONS_WINDOWS, TEST_POLICY_ID_2); + manifest.addEntry(ARTIFACT_TRUSTED_APPS_MACOS, TEST_POLICY_ID_1); + manifest.addEntry(ARTIFACT_TRUSTED_APPS_MACOS, TEST_POLICY_ID_2); + + context.savedObjectsClient.update = jest + .fn() + .mockImplementation((_type: string, _id: string, object: InternalManifestSchema) => object); + + await expect(manifestManager.commit(manifest)).resolves.toBeUndefined(); + + expect(context.savedObjectsClient.update).toHaveBeenCalledTimes(1); + expect(context.savedObjectsClient.update).toHaveBeenNthCalledWith( + 1, + ManifestConstants.SAVED_OBJECT_TYPE, + 'endpoint-manifest-v1', + { + artifacts: [ + { artifactId: ARTIFACT_ID_EXCEPTIONS_MACOS, policyId: undefined }, + { artifactId: ARTIFACT_ID_EXCEPTIONS_MACOS, policyId: TEST_POLICY_ID_1 }, + { artifactId: ARTIFACT_ID_TRUSTED_APPS_MACOS, policyId: TEST_POLICY_ID_1 }, + { artifactId: ARTIFACT_ID_EXCEPTIONS_WINDOWS, policyId: TEST_POLICY_ID_2 }, + { artifactId: ARTIFACT_ID_TRUSTED_APPS_MACOS, policyId: TEST_POLICY_ID_2 }, + ], + schemaVersion: 'v1', + semanticVersion: '1.0.0', + }, + { version: '1.0.0' } + ); + }); + + test('Throws error when saved objects client fails', async () => { + const context = buildManifestManagerContextMock({}); + const manifestManager = new ManifestManager(context); + const manifest = new Manifest({ soVersion: '1.0.0' }); + const error = new Error(); + + context.savedObjectsClient.update = jest.fn().mockRejectedValue(error); + + await expect(manifestManager.commit(manifest)).rejects.toBe(error); + + expect(context.savedObjectsClient.update).toHaveBeenCalledTimes(1); + expect(context.savedObjectsClient.update).toHaveBeenNthCalledWith( + 1, + ManifestConstants.SAVED_OBJECT_TYPE, + 'endpoint-manifest-v1', + { + artifacts: [], + schemaVersion: 'v1', + semanticVersion: '1.0.0', + }, + { version: '1.0.0' } + ); + }); + }); + + describe.each([true, false])('buildNewManifest', (unifiedManifestSO) => { const SUPPORTED_ARTIFACT_NAMES = [ ARTIFACT_NAME_EXCEPTIONS_MACOS, ARTIFACT_NAME_EXCEPTIONS_WINDOWS, @@ -305,8 +659,10 @@ describe('ManifestManager', () => { ...new Set(artifacts.map((artifact) => artifact.identifier)).values(), ]; - test('Fails when exception list client fails', async () => { - const context = buildManifestManagerContextMock({}); + test(`Fails when exception list client fails when unifiedManifestEnabled feature flag is set to: ${unifiedManifestSO}`, async () => { + const context = buildManifestManagerContextMock({ + ...(unifiedManifestSO ? { experimentalFeatures: ['unifiedManifestEnabled'] } : {}), + }); const manifestManager = new ManifestManager(context); context.exceptionListClient.findExceptionListItem = jest.fn().mockRejectedValue(new Error()); @@ -314,8 +670,10 @@ describe('ManifestManager', () => { await expect(manifestManager.buildNewManifest()).rejects.toThrow(); }); - test('Builds fully new manifest if no baseline parameter passed and no exception list items', async () => { - const context = buildManifestManagerContextMock({}); + test(`Builds fully new manifest if no baseline parameter passed and no exception list items when unifiedManifestEnabled feature flag is set to: ${unifiedManifestSO}`, async () => { + const context = buildManifestManagerContextMock({ + ...(unifiedManifestSO ? { experimentalFeatures: ['unifiedManifestEnabled'] } : {}), + }); const manifestManager = new ManifestManager(context); context.exceptionListClient.findExceptionListItem = mockFindExceptionListItemResponses({}); @@ -323,11 +681,6 @@ describe('ManifestManager', () => { TEST_POLICY_ID_1, ]); - context.savedObjectsClient.create = jest - .fn() - .mockImplementation((_type: string, object: InternalManifestSchema) => ({ - attributes: object, - })); const manifest = await manifestManager.buildNewManifest(); expect(manifest?.getSchemaVersion()).toStrictEqual('v1'); @@ -348,7 +701,7 @@ describe('ManifestManager', () => { } }); - test('Builds fully new manifest if no baseline parameter passed and present exception list items', async () => { + test(`Builds fully new manifest if no baseline parameter passed and present exception list items when unifiedManifestEnabled feature flag is set to: ${unifiedManifestSO}`, async () => { const exceptionListItem = getExceptionListItemSchemaMock({ os_types: ['macos'] }); const trustedAppListItem = getExceptionListItemSchemaMock({ os_types: ['linux'], @@ -366,7 +719,9 @@ describe('ManifestManager', () => { os_types: ['macos'], tags: ['policy:all'], }); - const context = buildManifestManagerContextMock({}); + const context = buildManifestManagerContextMock({ + ...(unifiedManifestSO ? { experimentalFeatures: ['unifiedManifestEnabled'] } : {}), + }); const manifestManager = new ManifestManager(context); context.exceptionListClient.findExceptionListItem = mockFindExceptionListItemResponses({ @@ -378,11 +733,7 @@ describe('ManifestManager', () => { }, [ENDPOINT_ARTIFACT_LISTS.blocklists.id]: { linux: [blocklistsListItem] }, }); - context.savedObjectsClient.create = jest - .fn() - .mockImplementation((_type: string, object: InternalManifestSchema) => ({ - attributes: object, - })); + context.packagePolicyService.fetchAllItemIds = getMockPolicyFetchAllItemIds([ TEST_POLICY_ID_1, ]); @@ -432,7 +783,7 @@ describe('ManifestManager', () => { } }); - test('Reuses artifacts when baseline parameter passed and present exception list items', async () => { + test(`Reuses artifacts when baseline parameter passed and present exception list items when unifiedManifestEnabled feature flag is set to: ${unifiedManifestSO}`, async () => { const exceptionListItem = getExceptionListItemSchemaMock({ os_types: ['macos'] }); const trustedAppListItem = getExceptionListItemSchemaMock({ os_types: ['linux'], @@ -450,7 +801,9 @@ describe('ManifestManager', () => { os_types: ['macos'], tags: ['policy:all'], }); - const context = buildManifestManagerContextMock({}); + const context = buildManifestManagerContextMock({ + ...(unifiedManifestSO ? { experimentalFeatures: ['unifiedManifestEnabled'] } : {}), + }); const manifestManager = new ManifestManager(context); context.exceptionListClient.findExceptionListItem = mockFindExceptionListItemResponses({ @@ -459,11 +812,7 @@ describe('ManifestManager', () => { context.packagePolicyService.fetchAllItemIds = getMockPolicyFetchAllItemIds([ TEST_POLICY_ID_1, ]); - context.savedObjectsClient.create = jest - .fn() - .mockImplementation((_type: string, object: InternalManifestSchema) => ({ - attributes: object, - })); + const oldManifest = await manifestManager.buildNewManifest(); context.exceptionListClient.findExceptionListItem = mockFindExceptionListItemResponses({ @@ -519,7 +868,7 @@ describe('ManifestManager', () => { } }); - test('Builds fully new manifest with single entries when they are duplicated', async () => { + test(`Builds fully new manifest with single entries when they are duplicated when unifiedManifestEnabled feature flag is set to: ${unifiedManifestSO}`, async () => { const exceptionListItem = getExceptionListItemSchemaMock({ os_types: ['macos'] }); const trustedAppListItem = getExceptionListItemSchemaMock({ os_types: ['linux'], @@ -537,7 +886,9 @@ describe('ManifestManager', () => { os_types: ['macos'], tags: ['policy:all'], }); - const context = buildManifestManagerContextMock({}); + const context = buildManifestManagerContextMock({ + ...(unifiedManifestSO ? { experimentalFeatures: ['unifiedManifestEnabled'] } : {}), + }); const manifestManager = new ManifestManager(context); const duplicatedEventFilterInDifferentPolicy = { @@ -569,11 +920,7 @@ describe('ManifestManager', () => { macos: [blocklistsListItem, blocklistsListItem], }, }); - context.savedObjectsClient.create = jest - .fn() - .mockImplementation((_type: string, object: InternalManifestSchema) => ({ - attributes: object, - })); + context.packagePolicyService.fetchAllItemIds = getMockPolicyFetchAllItemIds([ TEST_POLICY_ID_1, TEST_POLICY_ID_2, @@ -655,7 +1002,7 @@ describe('ManifestManager', () => { } }); - test('Builds manifest with policy specific exception list items for trusted apps', async () => { + test(`Builds manifest with policy specific exception list items for trusted apps when unifiedManifestEnabled feature flag is set to: ${unifiedManifestSO}`, async () => { const exceptionListItem = getExceptionListItemSchemaMock({ os_types: ['macos'] }); const trustedAppListItem = getExceptionListItemSchemaMock({ os_types: ['linux'], @@ -668,7 +1015,9 @@ describe('ManifestManager', () => { ], tags: [`policy:${TEST_POLICY_ID_2}`], }); - const context = buildManifestManagerContextMock({}); + const context = buildManifestManagerContextMock({ + ...(unifiedManifestSO ? { experimentalFeatures: ['unifiedManifestEnabled'] } : {}), + }); const manifestManager = new ManifestManager(context); context.exceptionListClient.findExceptionListItem = mockFindExceptionListItemResponses({ @@ -682,12 +1031,6 @@ describe('ManifestManager', () => { TEST_POLICY_ID_2, ]); - context.savedObjectsClient.create = jest - .fn() - .mockImplementation((_type: string, object: InternalManifestSchema) => ({ - attributes: object, - })); - const manifest = await manifestManager.buildNewManifest(); expect(manifest?.getSchemaVersion()).toStrictEqual('v1'); @@ -733,7 +1076,7 @@ describe('ManifestManager', () => { }); }); - describe('buildNewManifest when using app features', () => { + describe.each([true, false])('buildNewManifest when using app features', (unifiedManifestSO) => { const SUPPORTED_ARTIFACT_NAMES = [ ARTIFACT_NAME_EXCEPTIONS_MACOS, ARTIFACT_NAME_EXCEPTIONS_WINDOWS, @@ -756,7 +1099,7 @@ describe('ManifestManager', () => { ...new Set(artifacts.map((artifact) => artifact.identifier)).values(), ]; - test('when it has endpoint artifact management app feature it should not generate host isolation exceptions', async () => { + test(`when it has endpoint artifact management app feature it should not generate host isolation exceptions when unifiedManifestEnabled feature flag is set to: ${unifiedManifestSO}`, async () => { const exceptionListItem = getExceptionListItemSchemaMock({ os_types: ['macos'] }); const trustedAppListItem = getExceptionListItemSchemaMock({ os_types: ['linux'], @@ -774,9 +1117,10 @@ describe('ManifestManager', () => { os_types: ['macos'], tags: ['policy:all'], }); - const context = buildManifestManagerContextMock({}, [ - ProductFeatureSecurityKey.endpointArtifactManagement, - ]); + const context = buildManifestManagerContextMock( + { ...(unifiedManifestSO ? { experimentalFeatures: ['unifiedManifestEnabled'] } : {}) }, + [ProductFeatureSecurityKey.endpointArtifactManagement] + ); const manifestManager = new ManifestManager(context); context.exceptionListClient.findExceptionListItem = mockFindExceptionListItemResponses({ @@ -788,11 +1132,7 @@ describe('ManifestManager', () => { }, [ENDPOINT_ARTIFACT_LISTS.blocklists.id]: { linux: [blocklistsListItem] }, }); - context.savedObjectsClient.create = jest - .fn() - .mockImplementation((_type: string, object: InternalManifestSchema) => ({ - attributes: object, - })); + context.packagePolicyService.fetchAllItemIds = getMockPolicyFetchAllItemIds([ TEST_POLICY_ID_1, ]); @@ -840,7 +1180,7 @@ describe('ManifestManager', () => { } }); - test('when it has endpoint artifact management and response actions app features it should generate all exceptions', async () => { + test(`when it has endpoint artifact management and response actions app features it should generate all exceptions when unifiedManifestEnabled feature flag is set to: ${unifiedManifestSO}`, async () => { const exceptionListItem = getExceptionListItemSchemaMock({ os_types: ['macos'] }); const trustedAppListItem = getExceptionListItemSchemaMock({ os_types: ['linux'], @@ -858,10 +1198,13 @@ describe('ManifestManager', () => { os_types: ['macos'], tags: ['policy:all'], }); - const context = buildManifestManagerContextMock({}, [ - ProductFeatureSecurityKey.endpointArtifactManagement, - ProductFeatureSecurityKey.endpointResponseActions, - ]); + const context = buildManifestManagerContextMock( + { ...(unifiedManifestSO ? { experimentalFeatures: ['unifiedManifestEnabled'] } : {}) }, + [ + ProductFeatureSecurityKey.endpointArtifactManagement, + ProductFeatureSecurityKey.endpointResponseActions, + ] + ); const manifestManager = new ManifestManager(context); context.exceptionListClient.findExceptionListItem = mockFindExceptionListItemResponses({ @@ -873,11 +1216,7 @@ describe('ManifestManager', () => { }, [ENDPOINT_ARTIFACT_LISTS.blocklists.id]: { linux: [blocklistsListItem] }, }); - context.savedObjectsClient.create = jest - .fn() - .mockImplementation((_type: string, object: InternalManifestSchema) => ({ - attributes: object, - })); + context.packagePolicyService.fetchAllItemIds = getMockPolicyFetchAllItemIds([ TEST_POLICY_ID_1, ]); @@ -927,7 +1266,7 @@ describe('ManifestManager', () => { } }); - test('when does not have right app features, should not generate any exception', async () => { + test(`when does not have right app features, should not generate any exception when unifiedManifestEnabled feature flag is set to: ${unifiedManifestSO}`, async () => { const exceptionListItem = getExceptionListItemSchemaMock({ os_types: ['macos'] }); const trustedAppListItem = getExceptionListItemSchemaMock({ os_types: ['linux'], @@ -945,7 +1284,10 @@ describe('ManifestManager', () => { os_types: ['macos'], tags: ['policy:all'], }); - const context = buildManifestManagerContextMock({}, []); + const context = buildManifestManagerContextMock( + { ...(unifiedManifestSO ? { experimentalFeatures: ['unifiedManifestEnabled'] } : {}) }, + [] + ); const manifestManager = new ManifestManager(context); context.exceptionListClient.findExceptionListItem = mockFindExceptionListItemResponses({ @@ -957,123 +1299,7 @@ describe('ManifestManager', () => { }, [ENDPOINT_ARTIFACT_LISTS.blocklists.id]: { linux: [blocklistsListItem] }, }); - context.savedObjectsClient.create = jest - .fn() - .mockImplementation((_type: string, object: InternalManifestSchema) => ({ - attributes: object, - })); - context.packagePolicyService.fetchAllItemIds = getMockPolicyFetchAllItemIds([ - TEST_POLICY_ID_1, - ]); - - const manifest = await manifestManager.buildNewManifest(); - - expect(manifest?.getSchemaVersion()).toStrictEqual('v1'); - expect(manifest?.getSemanticVersion()).toStrictEqual('1.0.0'); - expect(manifest?.getSavedObjectVersion()).toBeUndefined(); - - const artifacts = manifest.getAllArtifacts(); - - expect(artifacts.length).toBe(15); - expect(getArtifactIds(artifacts)).toStrictEqual(SUPPORTED_ARTIFACT_NAMES); - - expect(getArtifactObject(artifacts[0])).toStrictEqual({ entries: [] }); - expect(getArtifactObject(artifacts[1])).toStrictEqual({ entries: [] }); - expect(getArtifactObject(artifacts[2])).toStrictEqual({ entries: [] }); - expect(getArtifactObject(artifacts[3])).toStrictEqual({ entries: [] }); - expect(getArtifactObject(artifacts[4])).toStrictEqual({ entries: [] }); - expect(getArtifactObject(artifacts[5])).toStrictEqual({ entries: [] }); - expect(getArtifactObject(artifacts[6])).toStrictEqual({ entries: [] }); - expect(getArtifactObject(artifacts[7])).toStrictEqual({ entries: [] }); - expect(getArtifactObject(artifacts[8])).toStrictEqual({ entries: [] }); - expect(getArtifactObject(artifacts[9])).toStrictEqual({ entries: [] }); - expect(getArtifactObject(artifacts[10])).toStrictEqual({ entries: [] }); - expect(getArtifactObject(artifacts[11])).toStrictEqual({ entries: [] }); - expect(getArtifactObject(artifacts[12])).toStrictEqual({ entries: [] }); - expect(getArtifactObject(artifacts[13])).toStrictEqual({ entries: [] }); - expect(getArtifactObject(artifacts[14])).toStrictEqual({ entries: [] }); - - for (const artifact of artifacts) { - expect(manifest.isDefaultArtifact(artifact)).toBe(true); - expect(manifest.getArtifactTargetPolicies(artifact)).toStrictEqual( - new Set([TEST_POLICY_ID_1]) - ); - } - }); - }); - - describe('buildNewManifest when Endpoint Exceptions contain `matches`', () => { - test('when contains only `wildcard`, `event.module=endpoint` is added', async () => { - const exceptionListItem = getExceptionListItemSchemaMock({ - os_types: ['macos'], - entries: [ - { type: 'wildcard', operator: 'included', field: 'path', value: '*match_me*' }, - { type: 'wildcard', operator: 'excluded', field: 'not_path', value: '*dont_match_me*' }, - ], - }); - const expectedExceptionListItem = getExceptionListItemSchemaMock({ - os_types: ['macos'], - entries: [ - ...exceptionListItem.entries, - { type: 'match', operator: 'included', field: 'event.module', value: 'endpoint' }, - ], - }); - - const context = buildManifestManagerContextMock({}); - const manifestManager = new ManifestManager(context); - - context.exceptionListClient.findExceptionListItem = mockFindExceptionListItemResponses({ - [ENDPOINT_LIST_ID]: { macos: [exceptionListItem] }, - }); - context.savedObjectsClient.create = jest - .fn() - .mockImplementation((_type: string, object: InternalManifestSchema) => ({ - attributes: object, - })); - context.packagePolicyService.fetchAllItemIds = getMockPolicyFetchAllItemIds([ - TEST_POLICY_ID_1, - ]); - - const manifest = await manifestManager.buildNewManifest(); - - expect(manifest?.getSchemaVersion()).toStrictEqual('v1'); - expect(manifest?.getSemanticVersion()).toStrictEqual('1.0.0'); - expect(manifest?.getSavedObjectVersion()).toBeUndefined(); - - const artifacts = manifest.getAllArtifacts(); - - expect(artifacts.length).toBe(15); - - expect(getArtifactObject(artifacts[0])).toStrictEqual({ - entries: translateToEndpointExceptions([expectedExceptionListItem], 'v1'), - }); - }); - - test('when contains anything next to `wildcard`, nothing is added', async () => { - const exceptionListItem = getExceptionListItemSchemaMock({ - os_types: ['macos'], - entries: [ - { type: 'wildcard', operator: 'included', field: 'path', value: '*match_me*' }, - { type: 'wildcard', operator: 'excluded', field: 'path', value: '*dont_match_me*' }, - { type: 'match', operator: 'included', field: 'path', value: 'something' }, - ], - }); - const expectedExceptionListItem = getExceptionListItemSchemaMock({ - os_types: ['macos'], - entries: [...exceptionListItem.entries], - }); - const context = buildManifestManagerContextMock({}); - const manifestManager = new ManifestManager(context); - - context.exceptionListClient.findExceptionListItem = mockFindExceptionListItemResponses({ - [ENDPOINT_LIST_ID]: { macos: [exceptionListItem] }, - }); - context.savedObjectsClient.create = jest - .fn() - .mockImplementation((_type: string, object: InternalManifestSchema) => ({ - attributes: object, - })); context.packagePolicyService.fetchAllItemIds = getMockPolicyFetchAllItemIds([ TEST_POLICY_ID_1, ]); @@ -1087,16 +1313,129 @@ describe('ManifestManager', () => { const artifacts = manifest.getAllArtifacts(); expect(artifacts.length).toBe(15); + expect(getArtifactIds(artifacts)).toStrictEqual(SUPPORTED_ARTIFACT_NAMES); - expect(getArtifactObject(artifacts[0])).toStrictEqual({ - entries: translateToEndpointExceptions([expectedExceptionListItem], 'v1'), - }); + expect(getArtifactObject(artifacts[0])).toStrictEqual({ entries: [] }); + expect(getArtifactObject(artifacts[1])).toStrictEqual({ entries: [] }); + expect(getArtifactObject(artifacts[2])).toStrictEqual({ entries: [] }); + expect(getArtifactObject(artifacts[3])).toStrictEqual({ entries: [] }); + expect(getArtifactObject(artifacts[4])).toStrictEqual({ entries: [] }); + expect(getArtifactObject(artifacts[5])).toStrictEqual({ entries: [] }); + expect(getArtifactObject(artifacts[6])).toStrictEqual({ entries: [] }); + expect(getArtifactObject(artifacts[7])).toStrictEqual({ entries: [] }); + expect(getArtifactObject(artifacts[8])).toStrictEqual({ entries: [] }); + expect(getArtifactObject(artifacts[9])).toStrictEqual({ entries: [] }); + expect(getArtifactObject(artifacts[10])).toStrictEqual({ entries: [] }); + expect(getArtifactObject(artifacts[11])).toStrictEqual({ entries: [] }); + expect(getArtifactObject(artifacts[12])).toStrictEqual({ entries: [] }); + expect(getArtifactObject(artifacts[13])).toStrictEqual({ entries: [] }); + expect(getArtifactObject(artifacts[14])).toStrictEqual({ entries: [] }); + + for (const artifact of artifacts) { + expect(manifest.isDefaultArtifact(artifact)).toBe(true); + expect(manifest.getArtifactTargetPolicies(artifact)).toStrictEqual( + new Set([TEST_POLICY_ID_1]) + ); + } }); }); - describe('deleteArtifacts', () => { - test('Successfully invokes saved objects client', async () => { - const context = buildManifestManagerContextMock({}); + describe.each([true, false])( + 'buildNewManifest when Endpoint Exceptions contain `matches`', + (unifiedManifestSO) => { + test(`when contains only \`wildcard\`, \`event.module=endpoint\` is added when unifiedManifestEnabled feature flag is set to: ${unifiedManifestSO}`, async () => { + const exceptionListItem = getExceptionListItemSchemaMock({ + os_types: ['macos'], + entries: [ + { type: 'wildcard', operator: 'included', field: 'path', value: '*match_me*' }, + { type: 'wildcard', operator: 'excluded', field: 'not_path', value: '*dont_match_me*' }, + ], + }); + const expectedExceptionListItem = getExceptionListItemSchemaMock({ + os_types: ['macos'], + entries: [ + ...exceptionListItem.entries, + { type: 'match', operator: 'included', field: 'event.module', value: 'endpoint' }, + ], + }); + + const context = buildManifestManagerContextMock({ + ...(unifiedManifestSO ? { experimentalFeatures: ['unifiedManifestEnabled'] } : {}), + }); + const manifestManager = new ManifestManager(context); + + context.exceptionListClient.findExceptionListItem = mockFindExceptionListItemResponses({ + [ENDPOINT_LIST_ID]: { macos: [exceptionListItem] }, + }); + + context.packagePolicyService.fetchAllItemIds = getMockPolicyFetchAllItemIds([ + TEST_POLICY_ID_1, + ]); + + const manifest = await manifestManager.buildNewManifest(); + + expect(manifest?.getSchemaVersion()).toStrictEqual('v1'); + expect(manifest?.getSemanticVersion()).toStrictEqual('1.0.0'); + expect(manifest?.getSavedObjectVersion()).toBeUndefined(); + + const artifacts = manifest.getAllArtifacts(); + + expect(artifacts.length).toBe(15); + + expect(getArtifactObject(artifacts[0])).toStrictEqual({ + entries: translateToEndpointExceptions([expectedExceptionListItem], 'v1'), + }); + }); + + test(`when contains anything next to \`wildcard\`, nothing is added when unifiedManifestEnabled feature flag is set to: ${unifiedManifestSO}`, async () => { + const exceptionListItem = getExceptionListItemSchemaMock({ + os_types: ['macos'], + entries: [ + { type: 'wildcard', operator: 'included', field: 'path', value: '*match_me*' }, + { type: 'wildcard', operator: 'excluded', field: 'path', value: '*dont_match_me*' }, + { type: 'match', operator: 'included', field: 'path', value: 'something' }, + ], + }); + const expectedExceptionListItem = getExceptionListItemSchemaMock({ + os_types: ['macos'], + entries: [...exceptionListItem.entries], + }); + + const context = buildManifestManagerContextMock({ + ...(unifiedManifestSO ? { experimentalFeatures: ['unifiedManifestEnabled'] } : {}), + }); + const manifestManager = new ManifestManager(context); + + context.exceptionListClient.findExceptionListItem = mockFindExceptionListItemResponses({ + [ENDPOINT_LIST_ID]: { macos: [exceptionListItem] }, + }); + + context.packagePolicyService.fetchAllItemIds = getMockPolicyFetchAllItemIds([ + TEST_POLICY_ID_1, + ]); + + const manifest = await manifestManager.buildNewManifest(); + + expect(manifest?.getSchemaVersion()).toStrictEqual('v1'); + expect(manifest?.getSemanticVersion()).toStrictEqual('1.0.0'); + expect(manifest?.getSavedObjectVersion()).toBeUndefined(); + + const artifacts = manifest.getAllArtifacts(); + + expect(artifacts.length).toBe(15); + + expect(getArtifactObject(artifacts[0])).toStrictEqual({ + entries: translateToEndpointExceptions([expectedExceptionListItem], 'v1'), + }); + }); + } + ); + + describe.each([true, false])('deleteArtifacts', (unifiedManifestSO) => { + test(`Successfully invokes saved objects client when unifiedManifestEnabled feature flag is set to: ${unifiedManifestSO}`, async () => { + const context = buildManifestManagerContextMock({ + ...(unifiedManifestSO ? { experimentalFeatures: ['unifiedManifestEnabled'] } : {}), + }); const manifestManager = new ManifestManager(context); await expect( @@ -1112,8 +1451,10 @@ describe('ManifestManager', () => { ]); }); - test('Returns errors for partial failures', async () => { - const context = buildManifestManagerContextMock({}); + test(`Returns errors for partial failures when unifiedManifestEnabled feature flag is set to: ${unifiedManifestSO}`, async () => { + const context = buildManifestManagerContextMock({ + ...(unifiedManifestSO ? { experimentalFeatures: ['unifiedManifestEnabled'] } : {}), + }); const artifactClient = context.artifactClient as jest.Mocked; const manifestManager = new ManifestManager(context); const error = new Error(); @@ -1140,9 +1481,11 @@ describe('ManifestManager', () => { }); }); - describe('pushArtifacts', () => { - test('Successfully invokes artifactClient', async () => { - const context = buildManifestManagerContextMock({}); + describe.each([true, false])('pushArtifacts', (unifiedManifestSO) => { + test(`Successfully invokes artifactClient when unifiedManifestEnabled feature flag is set to: ${unifiedManifestSO}`, async () => { + const context = buildManifestManagerContextMock({ + ...(unifiedManifestSO ? { experimentalFeatures: ['unifiedManifestEnabled'] } : {}), + }); const artifactClient = context.artifactClient as jest.Mocked; const manifestManager = new ManifestManager(context); const newManifest = ManifestManager.createDefaultManifest(); @@ -1164,8 +1507,10 @@ describe('ManifestManager', () => { ]); }); - test('Returns errors for partial failures', async () => { - const context = buildManifestManagerContextMock({}); + test(`Returns errors for partial failures when unifiedManifestEnabled feature flag is set to: ${unifiedManifestSO}`, async () => { + const context = buildManifestManagerContextMock({ + ...(unifiedManifestSO ? { experimentalFeatures: ['unifiedManifestEnabled'] } : {}), + }); const artifactClient = context.artifactClient as jest.Mocked; const manifestManager = new ManifestManager(context); const newManifest = ManifestManager.createDefaultManifest(); @@ -1206,114 +1551,16 @@ describe('ManifestManager', () => { }); }); - describe('commit', () => { - test('Creates new saved object if no saved object version', async () => { - const context = buildManifestManagerContextMock({}); - const manifestManager = new ManifestManager(context); - const manifest = ManifestManager.createDefaultManifest(); - - manifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS); - manifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS, TEST_POLICY_ID_1); - manifest.addEntry(ARTIFACT_EXCEPTIONS_WINDOWS, TEST_POLICY_ID_2); - manifest.addEntry(ARTIFACT_TRUSTED_APPS_MACOS, TEST_POLICY_ID_1); - manifest.addEntry(ARTIFACT_TRUSTED_APPS_MACOS, TEST_POLICY_ID_2); - - context.savedObjectsClient.create = jest - .fn() - .mockImplementation((_type: string, object: InternalManifestSchema) => object); - - await expect(manifestManager.commit(manifest)).resolves.toBeUndefined(); - - expect(context.savedObjectsClient.create).toHaveBeenCalledTimes(1); - expect(context.savedObjectsClient.create).toHaveBeenNthCalledWith( - 1, - ManifestConstants.SAVED_OBJECT_TYPE, - { - artifacts: [ - { artifactId: ARTIFACT_ID_EXCEPTIONS_MACOS, policyId: undefined }, - { artifactId: ARTIFACT_ID_EXCEPTIONS_MACOS, policyId: TEST_POLICY_ID_1 }, - { artifactId: ARTIFACT_ID_TRUSTED_APPS_MACOS, policyId: TEST_POLICY_ID_1 }, - { artifactId: ARTIFACT_ID_EXCEPTIONS_WINDOWS, policyId: TEST_POLICY_ID_2 }, - { artifactId: ARTIFACT_ID_TRUSTED_APPS_MACOS, policyId: TEST_POLICY_ID_2 }, - ], - schemaVersion: 'v1', - semanticVersion: '1.0.0', - created: expect.anything(), - }, - { id: 'endpoint-manifest-v1' } - ); - }); - - test('Updates existing saved object if has saved object version', async () => { - const context = buildManifestManagerContextMock({}); - const manifestManager = new ManifestManager(context); - const manifest = new Manifest({ soVersion: '1.0.0' }); - - manifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS); - manifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS, TEST_POLICY_ID_1); - manifest.addEntry(ARTIFACT_EXCEPTIONS_WINDOWS, TEST_POLICY_ID_2); - manifest.addEntry(ARTIFACT_TRUSTED_APPS_MACOS, TEST_POLICY_ID_1); - manifest.addEntry(ARTIFACT_TRUSTED_APPS_MACOS, TEST_POLICY_ID_2); - - context.savedObjectsClient.update = jest - .fn() - .mockImplementation((_type: string, _id: string, object: InternalManifestSchema) => object); - - await expect(manifestManager.commit(manifest)).resolves.toBeUndefined(); - - expect(context.savedObjectsClient.update).toHaveBeenCalledTimes(1); - expect(context.savedObjectsClient.update).toHaveBeenNthCalledWith( - 1, - ManifestConstants.SAVED_OBJECT_TYPE, - 'endpoint-manifest-v1', - { - artifacts: [ - { artifactId: ARTIFACT_ID_EXCEPTIONS_MACOS, policyId: undefined }, - { artifactId: ARTIFACT_ID_EXCEPTIONS_MACOS, policyId: TEST_POLICY_ID_1 }, - { artifactId: ARTIFACT_ID_TRUSTED_APPS_MACOS, policyId: TEST_POLICY_ID_1 }, - { artifactId: ARTIFACT_ID_EXCEPTIONS_WINDOWS, policyId: TEST_POLICY_ID_2 }, - { artifactId: ARTIFACT_ID_TRUSTED_APPS_MACOS, policyId: TEST_POLICY_ID_2 }, - ], - schemaVersion: 'v1', - semanticVersion: '1.0.0', - }, - { version: '1.0.0' } - ); - }); - - test('Throws error when saved objects client fails', async () => { - const context = buildManifestManagerContextMock({}); - const manifestManager = new ManifestManager(context); - const manifest = new Manifest({ soVersion: '1.0.0' }); - const error = new Error(); - - context.savedObjectsClient.update = jest.fn().mockRejectedValue(error); - - await expect(manifestManager.commit(manifest)).rejects.toBe(error); - - expect(context.savedObjectsClient.update).toHaveBeenCalledTimes(1); - expect(context.savedObjectsClient.update).toHaveBeenNthCalledWith( - 1, - ManifestConstants.SAVED_OBJECT_TYPE, - 'endpoint-manifest-v1', - { - artifacts: [], - schemaVersion: 'v1', - semanticVersion: '1.0.0', - }, - { version: '1.0.0' } - ); - }); - }); - - describe('tryDispatch', () => { + describe.each([true, false])('tryDispatch', (unifiedSavedObject) => { const getMockPolicyFetchAllItems = (items: PackagePolicy[]) => jest.fn(async function* () { yield items; }); - test('Should not dispatch if no policies', async () => { - const context = buildManifestManagerContextMock({}); + test(`Should not dispatch if no policies when unifiedManifestEnabled feature flag is set to: ${unifiedSavedObject}`, async () => { + const context = buildManifestManagerContextMock({ + ...(unifiedSavedObject ? { experimentalFeatures: ['unifiedManifestEnabled'] } : {}), + }); const manifestManager = new ManifestManager(context); const manifest = new Manifest({ soVersion: '1.0.0' }); @@ -1325,8 +1572,10 @@ describe('ManifestManager', () => { expect(context.packagePolicyService.bulkUpdate).toHaveBeenCalledTimes(0); }); - test('Should return errors if invalid config for package policy', async () => { - const context = buildManifestManagerContextMock({}); + test(`Should return errors if invalid config for package policy when unifiedManifestEnabled feature flag is set to: ${unifiedSavedObject}`, async () => { + const context = buildManifestManagerContextMock({ + ...(unifiedSavedObject ? { experimentalFeatures: ['unifiedManifestEnabled'] } : {}), + }); const manifestManager = new ManifestManager(context); const manifest = new Manifest({ soVersion: '1.0.0' }); @@ -1343,8 +1592,10 @@ describe('ManifestManager', () => { expect(context.packagePolicyService.bulkUpdate).toHaveBeenCalledTimes(0); }); - test('Should not dispatch if semantic version has not changed', async () => { - const context = buildManifestManagerContextMock({}); + test(`Should not dispatch if semantic version has not changed when unifiedManifestEnabled feature flag is set to: ${unifiedSavedObject}`, async () => { + const context = buildManifestManagerContextMock({ + ...(unifiedSavedObject ? { experimentalFeatures: ['unifiedManifestEnabled'] } : {}), + }); const manifestManager = new ManifestManager(context); const manifest = new Manifest({ soVersion: '1.0.0' }); @@ -1372,8 +1623,10 @@ describe('ManifestManager', () => { expect(context.packagePolicyService.bulkUpdate).toHaveBeenCalledTimes(0); }); - test('Should dispatch to only policies where list of artifacts changed', async () => { - const context = buildManifestManagerContextMock({}); + test(`Should dispatch to only policies where list of artifacts changed when unifiedManifestEnabled feature flag is set to: ${unifiedSavedObject}`, async () => { + const context = buildManifestManagerContextMock({ + ...(unifiedSavedObject ? { experimentalFeatures: ['unifiedManifestEnabled'] } : {}), + }); const manifestManager = new ManifestManager(context); const manifest = new Manifest({ soVersion: '1.0.0', semanticVersion: '1.0.1' }); @@ -1441,8 +1694,10 @@ describe('ManifestManager', () => { ); }); - test('Should dispatch to only policies where artifact content changed', async () => { - const context = buildManifestManagerContextMock({}); + test(`Should dispatch to only policies where artifact content changed when unifiedManifestEnabled feature flag is set to: ${unifiedSavedObject}`, async () => { + const context = buildManifestManagerContextMock({ + ...(unifiedSavedObject ? { experimentalFeatures: ['unifiedManifestEnabled'] } : {}), + }); const manifestManager = new ManifestManager(context); const manifest = new Manifest({ soVersion: '1.0.0', semanticVersion: '1.0.1' }); @@ -1512,8 +1767,10 @@ describe('ManifestManager', () => { ); }); - test('Should return partial errors', async () => { - const context = buildManifestManagerContextMock({}); + test(`Should return partial errors when unifiedManifestEnabled feature flag is set to: ${unifiedSavedObject}`, async () => { + const context = buildManifestManagerContextMock({ + ...(unifiedSavedObject ? { experimentalFeatures: ['unifiedManifestEnabled'] } : {}), + }); const manifestManager = new ManifestManager(context); const error = new Error(); @@ -1556,9 +1813,11 @@ describe('ManifestManager', () => { }); }); - describe('cleanup artifacts', () => { - test('Successfully removes orphan artifacts', async () => { - const context = buildManifestManagerContextMock({}); + describe.each([true, false])('cleanup artifacts', (unifiedSavedObject) => { + test(`Successfully removes orphan artifacts when unifiedManifestEnabled feature flag is set to: ${unifiedSavedObject}`, async () => { + const context = buildManifestManagerContextMock({ + ...(unifiedSavedObject ? { experimentalFeatures: ['unifiedManifestEnabled'] } : {}), + }); const manifestManager = new ManifestManager(context); (context.artifactClient.fetchAll as jest.Mock).mockReturnValue( @@ -1586,8 +1845,10 @@ describe('ManifestManager', () => { ]); }); - test('When there is no artifact to be removed', async () => { - const context = buildManifestManagerContextMock({}); + test(`When there is no artifact to be removed when unifiedManifestEnabled feature flag is set to: ${unifiedSavedObject}`, async () => { + const context = buildManifestManagerContextMock({ + ...(unifiedSavedObject ? { experimentalFeatures: ['unifiedManifestEnabled'] } : {}), + }); const manifestManager = new ManifestManager(context); context.exceptionListClient.findExceptionListItem = mockFindExceptionListItemResponses({}); @@ -1625,4 +1886,336 @@ describe('ManifestManager', () => { expect(context.artifactClient.bulkDeleteArtifacts).toHaveBeenCalledTimes(0); }); }); + + describe('Unified Manifest Methods', () => { + let manifestManager: ManifestManager; + let context: ManifestManagerContext; + + beforeEach(() => { + context = buildManifestManagerContextMock({}); + manifestManager = new ManifestManager(context); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('transforms', () => { + const createLegacyManifestSO = ( + opts: { + semanticVersion?: string; + } = {} + ) => ({ + attributes: { + schemaVersion: 'v1' as const, + semanticVersion: opts.semanticVersion ?? '1.0.0', + artifacts: [ + { artifactId: ARTIFACT_ID_EXCEPTIONS_MACOS, policyId: undefined }, + { artifactId: ARTIFACT_ID_EXCEPTIONS_WINDOWS, policyId: undefined }, + { artifactId: ARTIFACT_ID_EXCEPTIONS_LINUX, policyId: undefined }, + { artifactId: ARTIFACT_ID_EXCEPTIONS_WINDOWS, policyId: TEST_POLICY_ID_1 }, + { artifactId: ARTIFACT_ID_TRUSTED_APPS_MACOS, policyId: TEST_POLICY_ID_1 }, + { artifactId: ARTIFACT_ID_TRUSTED_APPS_WINDOWS, policyId: TEST_POLICY_ID_1 }, + { artifactId: ARTIFACT_ID_TRUSTED_APPS_WINDOWS, policyId: TEST_POLICY_ID_2 }, + ], + }, + version: 'WzQ3NzAsMV0=', + }); + + const createUnifiedManifestSO = (globalSemanticVersion?: string) => [ + { + policyId: '.global', + semanticVersion: globalSemanticVersion ?? '1.0.0', + artifactIds: [ + ARTIFACT_ID_EXCEPTIONS_MACOS, + ARTIFACT_ID_EXCEPTIONS_WINDOWS, + ARTIFACT_ID_EXCEPTIONS_LINUX, + ], + ...(globalSemanticVersion ? { created: '20-01-2020 10:00:00.000Z' } : {}), + id: '3', + }, + { + policyId: TEST_POLICY_ID_1, + semanticVersion: '1.0.0', + artifactIds: [ + ARTIFACT_ID_EXCEPTIONS_WINDOWS, + ARTIFACT_ID_TRUSTED_APPS_MACOS, + ARTIFACT_ID_TRUSTED_APPS_WINDOWS, + ], + ...(globalSemanticVersion ? { created: '20-01-2020 10:00:00.000Z' } : {}), + id: '1', + }, + { + policyId: TEST_POLICY_ID_2, + semanticVersion: '1.0.0', + artifactIds: [ARTIFACT_ID_TRUSTED_APPS_WINDOWS], + ...(globalSemanticVersion ? { created: '20-01-2020 10:00:00.000Z' } : {}), + id: '2', + }, + ]; + + describe('transformUnifiedManifestSOtoLegacyManifestSO', () => { + const unifiedManifestSO = createUnifiedManifestSO('1.0.5'); + test('should transform unified manifest saved object to legacy manifest saved object', async () => { + const expectedLegacyManifestSO = createLegacyManifestSO({ semanticVersion: '1.0.5' }); + + expect( + manifestManager.transformUnifiedManifestSOtoLegacyManifestSO( + unifiedManifestSO as InternalUnifiedManifestSchema[] + ) + ).toEqual(expectedLegacyManifestSO); + }); + + test('should return empty artifacts array when unified manifest saved object is empty', async () => { + const emptyUnifiedManifestSO: InternalUnifiedManifestSchema[] = []; + const expectedEmptyLegacyManifestSO = createLegacyManifestSO(); + expect( + manifestManager.transformUnifiedManifestSOtoLegacyManifestSO(emptyUnifiedManifestSO) + ).toEqual({ + ...expectedEmptyLegacyManifestSO, + attributes: { ...expectedEmptyLegacyManifestSO.attributes, artifacts: [] }, + }); + }); + + test('should return empty artifacts array when unified manifest saved object is empty but semanticVersion was provided', async () => { + const semanticVersion = '1.14.0'; + const emptyUnifiedManifestSO: InternalUnifiedManifestSchema[] = []; + const expectedEmptyLegacyManifestSO = createLegacyManifestSO(); + expect( + manifestManager.transformUnifiedManifestSOtoLegacyManifestSO( + emptyUnifiedManifestSO, + semanticVersion + ) + ).toEqual({ + ...expectedEmptyLegacyManifestSO, + attributes: { + ...expectedEmptyLegacyManifestSO.attributes, + artifacts: [], + semanticVersion, + }, + }); + }); + }); + describe('transformLegacyManifestSOtoUnifiedManifestSO', () => { + const unifiedManifestSO = createUnifiedManifestSO(); + const expectedLegacyManifestSO = createLegacyManifestSO().attributes; + test('should properly transform legacy manifest to unified manifest saved object with empty exising unified manifest so', async () => { + expect( + manifestManager.transformLegacyManifestSOtoUnifiedManifestSO( + expectedLegacyManifestSO, + [] + ) + ).toEqual(unifiedManifestSO.map((item) => ({ ...item, id: undefined }))); + }); + + test('should properly transform legacy manifest to unified manifest saved object with empty exising unified manifest so and propagate semanticVersion from the manifest', async () => { + const semanticVersion = '1.14.0'; + const expectedLegacyManifestSOWithSemanticVersion = createLegacyManifestSO({ + semanticVersion, + }).attributes; + const unifiedManifestSOWithSemanticVersion = createUnifiedManifestSO(semanticVersion); + + expect( + manifestManager.transformLegacyManifestSOtoUnifiedManifestSO( + expectedLegacyManifestSOWithSemanticVersion, + [] + ) + ).toEqual( + unifiedManifestSOWithSemanticVersion.map((item) => ({ + ...item, + id: undefined, + created: undefined, + })) + ); + }); + + test('should properly transform legacy manifest to unified manifest saved object with existing unified manifest so', async () => { + const createUnifiedManifests = (empty = false) => [ + { + policyId: '.global', + semanticVersion: '1.0.2', + artifactIds: !empty + ? [ + ARTIFACT_ID_EXCEPTIONS_MACOS, + ARTIFACT_ID_EXCEPTIONS_WINDOWS, + ARTIFACT_ID_EXCEPTIONS_LINUX, + ] + : [], + id: '3', + ...(empty ? { created: '2000' } : {}), + }, + { + policyId: TEST_POLICY_ID_1, + semanticVersion: '1.0.5', + artifactIds: !empty + ? [ + ARTIFACT_ID_EXCEPTIONS_WINDOWS, + ARTIFACT_ID_TRUSTED_APPS_MACOS, + ARTIFACT_ID_TRUSTED_APPS_WINDOWS, + ] + : [], + id: '1', + ...(empty ? { created: '2000' } : {}), + }, + { + policyId: TEST_POLICY_ID_2, + semanticVersion: '1.0.1', + artifactIds: !empty ? [ARTIFACT_ID_TRUSTED_APPS_WINDOWS] : [], + id: '2', + ...(empty ? { created: '2000' } : {}), + }, + ]; + + const existingUnifiedManifest = createUnifiedManifests(true); + const output = createUnifiedManifests(); + + const legacyManifest = createLegacyManifestSO().attributes; + + expect( + manifestManager.transformLegacyManifestSOtoUnifiedManifestSO( + legacyManifest, + existingUnifiedManifest as InternalUnifiedManifestSchema[] + ) + ).toEqual(output); + }); + }); + }); + + describe('prepareUnifiedManifestsSOUpdates', () => { + const existingUnifiedManifests = ['.global', TEST_POLICY_ID_1, TEST_POLICY_ID_2].map( + (policyId, idx) => ({ + policyId, + semanticVersion: '1.0.0', + artifactIds: [ARTIFACT_ID_EXCEPTIONS_WINDOWS], + id: `${idx}`, + created: '1', + version: 'abc', + }) + ); + + const bumpSemanticVersion = ( + manifests: Array>, + semanticVersion = '1.0.1' + ) => + manifests.map((manifest) => ({ + ...manifest, + semanticVersion, + })); + + test('correctly selects manifests to create', () => { + const unifiedManifest = existingUnifiedManifests.map( + ({ id, created, ...manifest }) => manifest + ); + + const { unifiedManifestsToUpdate, unifiedManifestsToCreate, unifiedManifestsToDelete } = + manifestManager.prepareUnifiedManifestsSOUpdates(unifiedManifest, []); + + expect(unifiedManifestsToUpdate).toEqual([]); + expect(unifiedManifestsToCreate).toEqual(unifiedManifest); + expect(unifiedManifestsToDelete).toEqual([]); + }); + test('correctly selects manifests to delete', () => { + const newUnifiedManifests = existingUnifiedManifests.slice(0, 2); + + const { unifiedManifestsToUpdate, unifiedManifestsToCreate, unifiedManifestsToDelete } = + manifestManager.prepareUnifiedManifestsSOUpdates( + newUnifiedManifests, + existingUnifiedManifests + ); + + expect(unifiedManifestsToUpdate).toEqual([]); + expect(unifiedManifestsToCreate).toEqual([]); + expect(unifiedManifestsToDelete).toEqual([existingUnifiedManifests[2].id]); + }); + test('correctly selects manifests to update when artifactIds changed', () => { + const newUnifiedManifests = existingUnifiedManifests.map((manifest) => ({ + ...manifest, + artifactIds: [ARTIFACT_ID_EXCEPTIONS_WINDOWS, ARTIFACT_ID_EXCEPTIONS_WINDOWS], + })); + + const expectedUnifiedManifestsToUpdate = bumpSemanticVersion(newUnifiedManifests); + + const { unifiedManifestsToUpdate, unifiedManifestsToCreate, unifiedManifestsToDelete } = + manifestManager.prepareUnifiedManifestsSOUpdates( + newUnifiedManifests, + existingUnifiedManifests + ); + + expect(unifiedManifestsToUpdate).toEqual(expectedUnifiedManifestsToUpdate); + expect(unifiedManifestsToCreate).toEqual([]); + expect(unifiedManifestsToDelete).toEqual([]); + }); + + test('correctly combines all cases', () => { + const newUnifiedManifests = existingUnifiedManifests.slice(0, 2).map((manifest) => ({ + ...manifest, + artifactIds: [ARTIFACT_ID_EXCEPTIONS_WINDOWS, ARTIFACT_ID_EXCEPTIONS_WINDOWS], + })); + + const newUnifiedManifestsAddition = { + policyId: 'test', + semanticVersion: '1.0.0', + artifactIds: [], + }; + + const expectedUnifiedManifestsToUpdate = bumpSemanticVersion(newUnifiedManifests); + + const expectedUnifiedManifestsToCreate = [newUnifiedManifestsAddition]; + + const expectedUnifiedManifestsToDelete = [existingUnifiedManifests[2].id]; + + const { unifiedManifestsToUpdate, unifiedManifestsToCreate, unifiedManifestsToDelete } = + manifestManager.prepareUnifiedManifestsSOUpdates( + [...newUnifiedManifests, newUnifiedManifestsAddition], + existingUnifiedManifests + ); + + expect(unifiedManifestsToUpdate).toEqual(expectedUnifiedManifestsToUpdate); + expect(unifiedManifestsToCreate).toEqual(expectedUnifiedManifestsToCreate); + expect(unifiedManifestsToDelete).toEqual(expectedUnifiedManifestsToDelete); + }); + }); + describe('bumpGlobalUnifiedManifestVersion', () => { + const createSoFindMock = (savedObjects: Array>) => + jest.fn().mockImplementation(async (objectType: { type: string }) => { + if (objectType.type === ManifestConstants.UNIFIED_SAVED_OBJECT_TYPE) { + return { + saved_objects: savedObjects, + }; + } else { + return null; + } + }); + + test('should bump the semantic version of the global manifest', async () => { + context.savedObjectsClient.find = createSoFindMock([ + { + id: '1', + attributes: { + policyId: '.global', + semanticVersion: '1.0.1', + }, + }, + ]); + context.savedObjectsClient.bulkUpdate = jest.fn(); + await manifestManager.bumpGlobalUnifiedManifestVersion(); + expect(context.savedObjectsClient.bulkUpdate).toHaveBeenCalledWith([ + { + id: '1', + type: ManifestConstants.UNIFIED_SAVED_OBJECT_TYPE, + attributes: { + policyId: '.global', + semanticVersion: '1.0.2', + }, + }, + ]); + }); + test('should make a clean return when no global manifest is found', async () => { + context.savedObjectsClient.find = createSoFindMock([]); + context.savedObjectsClient.bulkUpdate = jest.fn(); + await manifestManager.bumpGlobalUnifiedManifestVersion(); + expect(context.savedObjectsClient.bulkUpdate).toHaveBeenCalledTimes(0); + }); + }); + }); }); 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 663318c7459a3..3a6cfc5be280c 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 @@ -6,15 +6,17 @@ */ import semver from 'semver'; -import { isEmpty, isEqual, keyBy } from 'lodash'; +import { chunk, isEmpty, isEqual, keyBy } from 'lodash'; import type { ElasticsearchClient } from '@kbn/core/server'; import { type Logger, type SavedObjectsClientContract } from '@kbn/core/server'; -import { ENDPOINT_LIST_ID, ENDPOINT_ARTIFACT_LISTS } from '@kbn/securitysolution-list-constants'; +import { ENDPOINT_ARTIFACT_LISTS, ENDPOINT_LIST_ID } from '@kbn/securitysolution-list-constants'; import type { PackagePolicy } from '@kbn/fleet-plugin/common'; import type { Artifact, PackagePolicyClient } from '@kbn/fleet-plugin/server'; import type { ExceptionListClient } from '@kbn/lists-plugin/server'; import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; import { ProductFeatureKey } from '@kbn/security-solution-features/keys'; +import { asyncForEach } from '@kbn/std'; +import { UnifiedManifestClient } from '../unified_manifest_client'; import { stringify } from '../../../utils/stringify'; import { QueueProcessor } from '../../../utils/queue_processor'; import type { ProductFeaturesService } from '../../../../lib/product_features_service/product_features_service'; @@ -35,9 +37,15 @@ import { Manifest, } from '../../../lib/artifacts'; +import type { + InternalUnifiedManifestBaseSchema, + InternalUnifiedManifestSchema, + InternalUnifiedManifestUpdateSchema, +} from '../../../schemas/artifacts'; import { internalArtifactCompleteSchema, type InternalArtifactCompleteSchema, + type InternalManifestSchema, type WrappedTranslatedExceptionList, } from '../../../schemas/artifacts'; import type { EndpointArtifactClientInterface } from '../artifact_client'; @@ -513,7 +521,25 @@ export class ManifestManager { */ public async getLastComputedManifest(): Promise { try { - const manifestSo = await this.getManifestClient().getManifest(); + let manifestSo; + if (this.experimentalFeatures.unifiedManifestEnabled) { + const unifiedManifestsSo = await this.getAllUnifiedManifestsSO(); + // On first run, there will be no existing Unified Manifests SO, so we need to copy the semanticVersion from the legacy manifest + // This is to ensure that the first Unified Manifest created has the same semanticVersion as the legacy manifest and is not too far + // behind for package policy to pick it up. + if (unifiedManifestsSo.length === 0) { + const legacyManifestSo = await this.getManifestClient().getManifest(); + const legacySemanticVersion = legacyManifestSo?.attributes?.semanticVersion; + manifestSo = this.transformUnifiedManifestSOtoLegacyManifestSO( + unifiedManifestsSo, + legacySemanticVersion + ); + } else { + manifestSo = this.transformUnifiedManifestSOtoLegacyManifestSO(unifiedManifestsSo); + } + } else { + manifestSo = await this.getManifestClient().getManifest(); + } if (manifestSo.version === undefined) { throw new InvalidInternalManifestError( @@ -721,21 +747,25 @@ export class ManifestManager { * @returns {Promise} An error, if encountered, or null. */ public async commit(manifest: Manifest) { - const manifestClient = this.getManifestClient(); - - // Commit the new manifest const manifestSo = manifest.toSavedObject(); - const version = manifest.getSavedObjectVersion(); - if (version == null) { - await manifestClient.createManifest(manifestSo); + if (this.experimentalFeatures.unifiedManifestEnabled) { + await this.commitUnified(manifestSo); } else { - await manifestClient.updateManifest(manifestSo, { - version, - }); - } + const manifestClient = this.getManifestClient(); - this.logger.debug(`Committed manifest ${manifest.getSemanticVersion()}`); + const version = manifest.getSavedObjectVersion(); + + if (version == null) { + await manifestClient.createManifest(manifestSo); + } else { + await manifestClient.updateManifest(manifestSo, { + version, + }); + } + + this.logger.debug(`Committed manifest ${manifest.getSemanticVersion()}`); + } } private fetchAllPolicies(): AsyncIterable { @@ -835,4 +865,229 @@ export class ManifestManager { ); } } + + /** + * Unified Manifest methods + */ + + private setNewSemanticVersion(semanticVersion: string): string | null { + const newSemanticVersion = semver.inc(semanticVersion, 'patch'); + if (!semver.valid(newSemanticVersion)) { + throw new Error(`Invalid semver: ${newSemanticVersion}`); + } + return newSemanticVersion; + } + + protected getUnifiedManifestClient(): UnifiedManifestClient { + return new UnifiedManifestClient(this.savedObjectsClient); + } + + public async getAllUnifiedManifestsSO(): Promise { + return this.getUnifiedManifestClient().getAllUnifiedManifests(); + } + + public transformUnifiedManifestSOtoLegacyManifestSO( + unifiedManifestsSo: InternalUnifiedManifestSchema[], + semanticVersion?: string + ): { + version: string; + attributes: { + artifacts: Array< + { artifactId: string; policyId: undefined } | { artifactId: string; policyId: string } + >; + semanticVersion: string; + schemaVersion: ManifestSchemaVersion; + }; + } { + const globalUnifiedManifest = unifiedManifestsSo.find((a) => a.policyId === '.global'); + return { + version: 'WzQ3NzAsMV0=', // version is hardcoded since it was used only to determine whether to create a new manifest or update an existing one + attributes: { + artifacts: [ + ...(globalUnifiedManifest?.artifactIds.map((artifactId) => ({ + artifactId, + policyId: undefined, + })) ?? []), + ...unifiedManifestsSo.reduce( + (acc: Array<{ artifactId: string; policyId: string }>, unifiedManifest) => { + if (unifiedManifest.policyId === '.global') { + return acc; + } + acc.push( + ...unifiedManifest.artifactIds.map((artifactId) => ({ + policyId: unifiedManifest.policyId, + artifactId, + })) + ); + + return acc; + }, + [] + ), + ], + semanticVersion: (semanticVersion || globalUnifiedManifest?.semanticVersion) ?? '1.0.0', + schemaVersion: this.schemaVersion, + }, + }; + } + + public transformLegacyManifestSOtoUnifiedManifestSO( + manifestSo: InternalManifestSchema, + unifiedManifestsSo: InternalUnifiedManifestSchema[] + ): Array { + const manifestObject = manifestSo.artifacts.reduce( + ( + acc: Record, + { artifactId, policyId = '.global' } + ) => { + const existingPolicy = acc[policyId]; + if (existingPolicy) { + existingPolicy.artifactIds.push(artifactId); + } else { + const existingUnifiedManifestSo = unifiedManifestsSo.find( + (item) => item.policyId === policyId + ); + + // On first run, there will be no existing Unified Manifests SO, so we need to copy the semanticVersion from the legacy manifest + // This is to ensure that the first Unified Manifest created has the same semanticVersion as the legacy manifest and is not too far + // behind for package policy to pick it up. + const semanticVersion = + (policyId === '.global' && !unifiedManifestsSo.length + ? manifestSo?.semanticVersion + : existingUnifiedManifestSo?.semanticVersion) ?? '1.0.0'; + + acc[policyId] = { + policyId, + artifactIds: [artifactId], + semanticVersion, + id: existingUnifiedManifestSo?.id, + }; + } + return acc; + }, + {} + ); + return Object.values(manifestObject); + } + + public prepareUnifiedManifestsSOUpdates( + unifiedManifestsSo: Array & { id?: string }>, + existingUnifiedManifestsSo: InternalUnifiedManifestSchema[] + ) { + const existingManifestsObj: Record = {}; + existingUnifiedManifestsSo.forEach((manifest) => { + existingManifestsObj[manifest.id] = manifest; + }); + + const { unifiedManifestsToUpdate, unifiedManifestsToCreate } = unifiedManifestsSo.reduce( + ( + acc: { + unifiedManifestsToUpdate: InternalUnifiedManifestUpdateSchema[]; + unifiedManifestsToCreate: InternalUnifiedManifestBaseSchema[]; + }, + unifiedManifest + ) => { + if (unifiedManifest.id !== undefined) { + // Manifest with id exists in SO, check if it needs to be updated + const existingUnifiedManifest = existingManifestsObj[unifiedManifest.id]; + // Update SO if the artifactIds changed. + if (!isEqual(existingUnifiedManifest.artifactIds, unifiedManifest.artifactIds)) { + acc.unifiedManifestsToUpdate.push({ + ...unifiedManifest, + semanticVersion: this.setNewSemanticVersion(unifiedManifest.semanticVersion), + version: existingUnifiedManifest.version, + } as InternalUnifiedManifestUpdateSchema); + } + } else { + // Manifest with id does not exist in SO, create new SO + acc.unifiedManifestsToCreate.push(unifiedManifest); + } + + return acc; + }, + { unifiedManifestsToUpdate: [], unifiedManifestsToCreate: [] } + ); + + const unifiedManifestsToDelete = existingUnifiedManifestsSo.reduce( + (acc: string[], { policyId, id }) => { + const existingPolicy = unifiedManifestsSo.find((item) => item.policyId === policyId); + if (!existingPolicy) { + acc.push(id); + } + return acc; + }, + [] + ); + + return { unifiedManifestsToUpdate, unifiedManifestsToCreate, unifiedManifestsToDelete }; + } + + public async bumpGlobalUnifiedManifestVersion(): Promise { + const globalUnifiedManifestSO = + await this.getUnifiedManifestClient().getUnifiedManifestByPolicyId('.global'); + if (!globalUnifiedManifestSO?.saved_objects?.length) { + this.logger.warn('No Global Unified Manifest found to bump version'); + return; + } + const globalUnifiedManifest = globalUnifiedManifestSO.saved_objects[0]; + + const newSemanticVersion = + this.setNewSemanticVersion(globalUnifiedManifest.attributes.semanticVersion) || '1.0.0'; + await this.getUnifiedManifestClient().updateUnifiedManifest({ + ...globalUnifiedManifest.attributes, + id: globalUnifiedManifest.id, + semanticVersion: newSemanticVersion, + }); + } + + public async commitUnified(manifestSo: InternalManifestSchema): Promise { + const existingUnifiedManifestsSo = await this.getAllUnifiedManifestsSO(); + + const unifiedManifestSO = this.transformLegacyManifestSOtoUnifiedManifestSO( + manifestSo, + existingUnifiedManifestsSo + ); + + const { unifiedManifestsToUpdate, unifiedManifestsToCreate, unifiedManifestsToDelete } = + this.prepareUnifiedManifestsSOUpdates(unifiedManifestSO, existingUnifiedManifestsSo); + + if (unifiedManifestsToCreate.length) { + await asyncForEach(chunk(unifiedManifestsToCreate, 100), async (unifiedManifestsBatch) => { + await this.getUnifiedManifestClient().createUnifiedManifests(unifiedManifestsBatch); + }); + this.logger.debug(`Created ${unifiedManifestsToCreate.length} unified manifests`); + } + + if (unifiedManifestsToUpdate.length) { + await asyncForEach(chunk(unifiedManifestsToUpdate, 100), async (unifiedManifestsBatch) => { + await this.getUnifiedManifestClient().updateUnifiedManifests(unifiedManifestsBatch); + }); + + this.logger.debug(`Updated ${unifiedManifestsToUpdate.length} unified manifests`); + } + + if (unifiedManifestsToDelete.length) { + await asyncForEach(chunk(unifiedManifestsToDelete, 100), async (unifiedManifestsBatch) => { + await this.getUnifiedManifestClient().deleteUnifiedManifestByIds(unifiedManifestsBatch); + }); + + this.logger.debug(`Deleted ${unifiedManifestsToDelete.length} unified manifests`); + } + + if ( + unifiedManifestsToCreate.length || + unifiedManifestsToUpdate.length || + unifiedManifestsToDelete.length + ) { + // If global manifest is not in the list of manifests to create or update, we need to bump its version + // We use it to set schemaVersion of the legacy manifest we are going to create so that its being picked up when populating agent policy + const hasGlobalManifest = [...unifiedManifestsToCreate, ...unifiedManifestsToUpdate].some( + (manifest) => manifest.policyId === '.global' + ); + + if (!hasGlobalManifest || unifiedManifestsToDelete.length) { + await this.bumpGlobalUnifiedManifestVersion(); + } + } + } } diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/unified_manifes_client.test.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/unified_manifes_client.test.ts index 83a37e617caf7..4fde69946ccff 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/unified_manifes_client.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/unified_manifes_client.test.ts @@ -73,8 +73,7 @@ describe('unified_manifest_client', () => { test('can get unified manifest by id', async () => { await unifiedManifestClient.getUnifiedManifestById('123'); expect(savedObjectsClient.bulkGet).toHaveBeenCalledWith( - expect.arrayContaining([mockSoClientCallParams({ id: '123' }, false)]), - { namespace: UNIFIED_MANIFEST_ALL_NAMESPACES } + expect.arrayContaining([mockSoClientCallParams({ id: '123' }, false)]) ); }); @@ -84,8 +83,7 @@ describe('unified_manifest_client', () => { expect.arrayContaining([ mockSoClientCallParams({ id: '123' }, false), mockSoClientCallParams({ id: '456' }, false), - ]), - { namespace: UNIFIED_MANIFEST_ALL_NAMESPACES } + ]) ); }); @@ -114,17 +112,14 @@ describe('unified_manifest_client', () => { ...mockUnifiedManifestAttributes({ policyId: `policy-${i}` }), id: `id-${i}`, created: '1', + version: '1', }; }) ); - const cbFunc = jest.fn(); - await unifiedManifestClient.getAllUnifiedManifests(cbFunc); + const result = await unifiedManifestClient.getAllUnifiedManifests(); - expect(cbFunc).toHaveBeenCalledTimes(3); - expect(cbFunc).toHaveBeenLastCalledWith([ - expect.objectContaining({ policyId: 'policy-2000', id: 'id-2000' }), - ]); + expect(result.length).toBe(2001); }); }); @@ -140,8 +135,7 @@ describe('unified_manifest_client', () => { expect(savedObjectsClient.bulkUpdate).toHaveBeenCalledWith( expect.arrayContaining([ mockSoClientCallParams({ id: '1234', version: 'abcd' }, true, false), - ]), - { namespace: UNIFIED_MANIFEST_ALL_NAMESPACES } + ]) ); }); test('can update unified manifests', async () => { @@ -153,8 +147,7 @@ describe('unified_manifest_client', () => { expect.arrayContaining([ mockSoClientCallParams({ id: '1234', version: 'abcd' }, true, false), mockSoClientCallParams({ id: '1234', version: 'abcd' }, true, false), - ]), - { namespace: UNIFIED_MANIFEST_ALL_NAMESPACES } + ]) ); }); }); @@ -162,8 +155,7 @@ describe('unified_manifest_client', () => { test('can delete unified manifest', async () => { await unifiedManifestClient.deleteUnifiedManifestById('123'); expect(savedObjectsClient.bulkDelete).toHaveBeenCalledWith( - expect.arrayContaining([{ id: '123', type: ManifestConstants.UNIFIED_SAVED_OBJECT_TYPE }]), - { namespace: UNIFIED_MANIFEST_ALL_NAMESPACES } + expect.arrayContaining([{ id: '123', type: ManifestConstants.UNIFIED_SAVED_OBJECT_TYPE }]) ); }); test('can delete unified manifests', async () => { @@ -172,8 +164,7 @@ describe('unified_manifest_client', () => { expect.arrayContaining([ mockSoClientCallParams({ id: '123' }, false), mockSoClientCallParams({ id: '456' }, false), - ]), - { namespace: UNIFIED_MANIFEST_ALL_NAMESPACES } + ]) ); }); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/unified_manifest_client.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/unified_manifest_client.ts index db47c43b68607..d84742ed65f10 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/unified_manifest_client.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/unified_manifest_client.ts @@ -86,20 +86,19 @@ export class UnifiedManifestClient { manifestIds: string[] ): Promise> { return this.savedObjectsClient.bulkGet( - manifestIds.map((id) => ({ id, type: ManifestConstants.UNIFIED_SAVED_OBJECT_TYPE })), - { namespace: UNIFIED_MANIFEST_ALL_NAMESPACES } + manifestIds.map((id) => ({ id, type: ManifestConstants.UNIFIED_SAVED_OBJECT_TYPE })) ); } public async getAllUnifiedManifests( - cb: (unifiedManifests: InternalUnifiedManifestSchema[]) => void | Promise, options?: FetchAllUnifiedManifestsOptions - ): Promise { + ): Promise { const unifiedManifestsFetcher = this.fetchAllUnifiedManifests(this.savedObjectsClient, options); - + const allUnifiedManifests: InternalUnifiedManifestSchema[] = []; for await (const unifiedManifests of unifiedManifestsFetcher) { - await cb(unifiedManifests); + allUnifiedManifests.push(...unifiedManifests); } + return allUnifiedManifests; } /** @@ -127,8 +126,7 @@ export class UnifiedManifestClient { attributes, ...(version ? { version } : {}), }; - }), - { namespace: UNIFIED_MANIFEST_ALL_NAMESPACES } + }) ); } @@ -144,8 +142,7 @@ export class UnifiedManifestClient { manifestIds: string[] ): Promise { return this.savedObjectsClient.bulkDelete( - manifestIds.map((id) => ({ id, type: ManifestConstants.UNIFIED_SAVED_OBJECT_TYPE })), - { namespace: UNIFIED_MANIFEST_ALL_NAMESPACES } + manifestIds.map((id) => ({ id, type: ManifestConstants.UNIFIED_SAVED_OBJECT_TYPE })) ); } @@ -160,7 +157,7 @@ export class UnifiedManifestClient { fields = [], kuery, sortOrder = 'asc', - sortField = 'created', + sortField = 'created_at', }: FetchAllUnifiedManifestsOptions = {} ): AsyncIterable { return createSoFindIterable({ diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/utils.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/utils.ts index f52eafbdc4529..de1484bf155a1 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/utils.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/utils.ts @@ -16,6 +16,7 @@ export const mapUnifiedManifestSavedObjectToUnifiedManifest = ({ attributes: { artifactIds, policyId, semanticVersion }, // eslint-disable-next-line @typescript-eslint/naming-convention created_at, + version, }: SavedObject): InternalUnifiedManifestSchema => { return { id, @@ -23,5 +24,6 @@ export const mapUnifiedManifestSavedObjectToUnifiedManifest = ({ semanticVersion, created: created_at, artifactIds, + version, }; }; diff --git a/x-pack/plugins/security_solution/server/saved_objects.ts b/x-pack/plugins/security_solution/server/saved_objects.ts index 0b1fec5677488..3659b15a04714 100644 --- a/x-pack/plugins/security_solution/server/saved_objects.ts +++ b/x-pack/plugins/security_solution/server/saved_objects.ts @@ -13,7 +13,7 @@ import { noteType, pinnedEventType, timelineType } from './lib/timeline/saved_ob import { legacyType as legacyRuleActionsType } from './lib/detection_engine/rule_actions_legacy'; import { prebuiltRuleAssetType } from './lib/detection_engine/prebuilt_rules'; import { type as signalsMigrationType } from './lib/detection_engine/migrations/saved_objects'; -import { manifestType } from './endpoint/lib/artifacts/saved_object_mappings'; +import { manifestType, unifiedManifestType } from './endpoint/lib/artifacts/saved_object_mappings'; import { riskEngineConfigurationType } from './lib/entity_analytics/risk_engine/saved_object'; const types = [ @@ -23,6 +23,7 @@ const types = [ prebuiltRuleAssetType, timelineType, manifestType, + unifiedManifestType, signalsMigrationType, riskEngineConfigurationType, protectionUpdatesNoteType, diff --git a/x-pack/test/security_solution_endpoint/apps/integrations/artifact_entries_list.ts b/x-pack/test/security_solution_endpoint/apps/integrations/artifact_entries_list.ts index 58ffde0d7611f..6c78dd995673f 100644 --- a/x-pack/test/security_solution_endpoint/apps/integrations/artifact_entries_list.ts +++ b/x-pack/test/security_solution_endpoint/apps/integrations/artifact_entries_list.ts @@ -30,7 +30,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const pageObjects = getPageObjects(['common', 'artifactEntriesList']); const testSubjects = getService('testSubjects'); const browser = getService('browser'); - const endpointArtifactsTestResources = getService('endpointArtifactTestResources'); + const endpointArtifactTestResources = getService('endpointArtifactTestResources'); const endpointTestResources = getService('endpointTestResources'); const retry = getService('retry'); const esClient = getService('es'); @@ -76,7 +76,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { // Check edited artifact is in the list with new values (wait for list to be updated) let updatedArtifact: ArtifactElasticsearchProperties | undefined; await retry.waitForWithTimeout('fleet artifact is updated', 120_000, async () => { - const artifacts = await endpointArtifactsTestResources.getArtifacts(); + const artifacts = await endpointArtifactTestResources.getArtifacts(); const manifestArtifact = artifacts.find((artifact) => { return ( diff --git a/x-pack/test/security_solution_endpoint/apps/integrations_feature_flag/artifact_entries_list.ts b/x-pack/test/security_solution_endpoint/apps/integrations_feature_flag/artifact_entries_list.ts new file mode 100644 index 0000000000000..46e333e10779d --- /dev/null +++ b/x-pack/test/security_solution_endpoint/apps/integrations_feature_flag/artifact_entries_list.ts @@ -0,0 +1,376 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { unzip } from 'zlib'; +import { promisify } from 'util'; +import expect from '@kbn/expect'; +import { IndexedHostsAndAlertsResponse } from '@kbn/security-solution-plugin/common/endpoint/index_data'; +import { + ENDPOINT_ARTIFACT_LIST_IDS, + EXCEPTION_LIST_URL, +} from '@kbn/securitysolution-list-constants'; +import { ArtifactElasticsearchProperties } from '@kbn/fleet-plugin/server/services'; +import { FtrProviderContext } from '../../ftr_provider_context'; +import { + ArtifactBodyType, + getArtifactsListTestsData, + ArtifactActionsType, + AgentPolicyResponseType, + getCreateMultipleData, + MultipleArtifactActionsType, +} from './mocks'; +import { PolicyTestResourceInfo } from '../../services/endpoint_policy'; +import { targetTags } from '../../target_tags'; + +export default ({ getPageObjects, getService }: FtrProviderContext) => { + const pageObjects = getPageObjects(['common', 'artifactEntriesList']); + const testSubjects = getService('testSubjects'); + const browser = getService('browser'); + const endpointArtifactsTestResources = getService('endpointArtifactTestResources'); + const endpointTestResources = getService('endpointTestResources'); + const retry = getService('retry'); + const esClient = getService('es'); + const supertest = getService('supertest'); + const find = getService('find'); + const toasts = getService('toasts'); + const policyTestResources = getService('policyTestResources'); + const unzipPromisify = promisify(unzip); + + const removeAllArtifacts = async () => { + for (const listId of ENDPOINT_ARTIFACT_LIST_IDS) { + await removeExceptionsList(listId); + } + }; + + const removeExceptionsList = async (listId: string) => { + await supertest + .delete(`${EXCEPTION_LIST_URL}?list_id=${listId}&namespace_type=agnostic`) + .set('kbn-xsrf', 'true'); + }; + + describe('For each artifact list under management', function () { + targetTags(this, ['@ess', '@serverless']); + + this.timeout(60_000 * 5); + let indexedData: IndexedHostsAndAlertsResponse; + let policyInfo: PolicyTestResourceInfo; + + before(async () => { + indexedData = await endpointTestResources.loadEndpointData(); + }); + after(async () => { + await endpointTestResources.unloadEndpointData(indexedData); + }); + + const checkFleetArtifacts = async ( + identifier: string, + expectedArtifact: ArtifactElasticsearchProperties, + expectedDecodedBodyArtifact: ArtifactBodyType, + policy?: PolicyTestResourceInfo + ) => { + // Check edited artifact is in the list with new values (wait for list to be updated) + let updatedArtifact: ArtifactElasticsearchProperties | undefined; + await retry.waitForWithTimeout('fleet artifact is updated', 120_000, async () => { + const artifacts = await endpointArtifactsTestResources.getArtifactsFromUnifiedManifestSO(); + + // This expects manifest artifact to come from unified so + const manifestArtifact = artifacts.find((artifact) => { + return ( + artifact.artifactIds.includes( + `${expectedArtifact.identifier}-${expectedArtifact.decoded_sha256}` + ) && artifact.policyId === policy?.packagePolicy.id + ); + }); + + if (!manifestArtifact) return false; + + // Get fleet artifact + const windowsArtifactResult = await esClient.get({ + index: '.fleet-artifacts-7', + id: `endpoint:${expectedArtifact.identifier}-${expectedArtifact.decoded_sha256}`, + }); + + const windowsArtifact = windowsArtifactResult._source as ArtifactElasticsearchProperties; + + // Get agent policy + const { + hits: { hits: policiesResults }, + } = await esClient.search({ + index: '.fleet-policies*', + query: { + bool: { + filter: [ + { + match: { + policy_id: policy?.agentPolicy.id, + }, + }, + ], + }, + }, + sort: [{ revision_idx: { order: 'desc' } }], + size: 1, + }); + + const agentPolicyResults = policiesResults[0] as AgentPolicyResponseType; + const policyArtifactManifest = agentPolicyResults._source.data.inputs[0] + ? agentPolicyResults._source.data.inputs[0].artifact_manifest + : undefined; + + let isUpdated: boolean = false; + if (policyArtifactManifest) { + // Compare artifacts from fleet artifacts and agent policy are the expecteds + isUpdated = + windowsArtifact.encoded_sha256 === expectedArtifact.encoded_sha256 && + policyArtifactManifest.artifacts[identifier].encoded_sha256 === + expectedArtifact.encoded_sha256; + } + + if (isUpdated) updatedArtifact = windowsArtifact; + return isUpdated; + }); + + updatedArtifact!.created = expectedArtifact.created; + const bodyFormBuffer = Buffer.from(updatedArtifact!.body, 'base64'); + const unzippedBody = await unzipPromisify(bodyFormBuffer); + + // Check decoded body first to detect possible body changes + expect(JSON.parse(unzippedBody.toString())).eql(expectedDecodedBodyArtifact); + expect(updatedArtifact).eql(expectedArtifact); + }; + + const performActions = async ( + actions: + | ArtifactActionsType['create']['formFields'] + | ArtifactActionsType['update']['formFields'], + suffix?: string + ) => { + for (const formAction of actions) { + if (formAction.type === 'customClick') { + await find.clickByCssSelector(formAction.selector, testSubjects.FIND_TIME); + } else if (formAction.type === 'click') { + await testSubjects.click(formAction.selector); + } else if (formAction.type === 'input') { + await testSubjects.setValue( + formAction.selector, + (formAction.value || '') + (suffix ? suffix : '') + ); + } else if (formAction.type === 'clear') { + await ( + await (await testSubjects.find(formAction.selector)).findByCssSelector('button') + ).click(); + } + } + }; + + const deleteArtifact = async (actions: ArtifactActionsType) => { + await pageObjects.artifactEntriesList.clickCardActionMenu(actions.pagePrefix); + await testSubjects.click(`${actions.pagePrefix}-card-cardDeleteAction`); + await testSubjects.click(`${actions.pagePrefix}-deleteModal-submitButton`); + await testSubjects.waitForDeleted(actions.delete.confirmSelector); + }; + + const createArtifact = async ( + actions: ArtifactActionsType | MultipleArtifactActionsType, + options?: { policyId?: string; suffix?: string; createButton?: string } + ) => { + // Opens add flyout + if (options?.createButton) { + await testSubjects.click(`${actions.pagePrefix}-${options.createButton}`); + } else { + await testSubjects.click(`${actions.pagePrefix}-emptyState-addButton`); + } + + await performActions(actions.create.formFields, options?.suffix); + + if (options?.policyId) { + await testSubjects.click(`${actions.pageObject}-form-effectedPolicies-perPolicy`); + await testSubjects.click(`policy-${options.policyId}-checkbox`); + } + + // Submit create artifact form + await testSubjects.click(`${actions.pagePrefix}-flyout-submitButton`); + }; + + const updateArtifact = async ( + actions: ArtifactActionsType, + options?: { policyId?: string; suffix?: string } + ) => { + // Opens edit flyout + await pageObjects.artifactEntriesList.clickCardActionMenu(actions.pagePrefix); + await testSubjects.click(`${actions.pagePrefix}-card-cardEditAction`); + + await performActions(actions.update.formFields); + + if (options?.policyId) { + await testSubjects.click(`${actions.pageObject}-form-effectedPolicies-perPolicy`); + await testSubjects.click(`policy-${options.policyId}-checkbox`); + } + + // Submit edit artifact form + await testSubjects.click(`${actions.pagePrefix}-flyout-submitButton`); + }; + + for (const testData of getArtifactsListTestsData()) { + describe(`When on the ${testData.title} entries list`, function () { + beforeEach(async () => { + policyInfo = await policyTestResources.createPolicy(); + await removeAllArtifacts(); + await browser.refresh(); + await pageObjects.artifactEntriesList.navigateToList(testData.urlPath); + }); + + afterEach(async () => { + await removeAllArtifacts(); + if (policyInfo) { + await policyInfo.cleanup(); + } + }); + + it(`should not show page title if there is no ${testData.title} entry`, async () => { + await testSubjects.missingOrFail('header-page-title'); + }); + + it(`should be able to add a new ${testData.title} entry`, async () => { + await createArtifact(testData, { policyId: policyInfo.packagePolicy.id }); + // Check new artifact is in the list + for (const checkResult of testData.create.checkResults) { + expect(await testSubjects.getVisibleText(checkResult.selector)).to.equal( + checkResult.value + ); + } + await toasts.dismiss(); + + // Title is shown after adding an item + expect(await testSubjects.getVisibleText('header-page-title')).to.equal(testData.title); + + // Checks if fleet artifact has been updated correctly + await checkFleetArtifacts( + testData.fleetArtifact.identifier, + testData.fleetArtifact.getExpectedUpdatedtArtifactWhenCreate(), + testData.fleetArtifact.getExpectedUpdatedArtifactBodyWhenCreate(), + policyInfo + ); + }); + + it(`should be able to update an existing ${testData.title} entry`, async () => { + await createArtifact(testData); + await updateArtifact(testData, { policyId: policyInfo.packagePolicy.id }); + + // Check edited artifact is in the list with new values (wait for list to be updated) + await retry.waitForWithTimeout('entry is updated in list', 20000, async () => { + const currentValue = await testSubjects.getVisibleText( + `${testData.pagePrefix}-card-criteriaConditions${ + testData.pagePrefix === 'EventFiltersListPage' ? '-condition' : '' + }` + ); + return currentValue === testData.update.waitForValue; + }); + + for (const checkResult of testData.update.checkResults) { + expect(await testSubjects.getVisibleText(checkResult.selector)).to.equal( + checkResult.value + ); + } + + await toasts.dismiss(); + + // Title still shown after editing an item + expect(await testSubjects.getVisibleText('header-page-title')).to.equal(testData.title); + + // Checks if fleet artifact has been updated correctly + await checkFleetArtifacts( + testData.fleetArtifact.identifier, + testData.fleetArtifact.getExpectedUpdatedArtifactWhenUpdate(), + testData.fleetArtifact.getExpectedUpdatedArtifactBodyWhenUpdate(), + policyInfo + ); + }); + + it(`should be able to delete the existing ${testData.title} entry`, async () => { + await createArtifact(testData); + await deleteArtifact(testData); + // We only expect one artifact to have been visible + await testSubjects.missingOrFail(testData.delete.card); + // Header has gone because there is no artifact + await testSubjects.missingOrFail('header-page-title'); + }); + }); + } + + describe('Should check artifacts are correctly generated when multiple entries', function () { + let firstPolicy: PolicyTestResourceInfo; + let secondPolicy: PolicyTestResourceInfo; + + const firstSuffix = 'first'; + const secondSuffix = 'second'; + const thirdSuffix = 'third'; + + beforeEach(async () => { + firstPolicy = await policyTestResources.createPolicy(); + secondPolicy = await policyTestResources.createPolicy(); + await removeAllArtifacts(); + await browser.refresh(); + await pageObjects.artifactEntriesList.navigateToList(testData.urlPath); + }); + + afterEach(async () => { + await removeAllArtifacts(); + if (firstPolicy) { + await firstPolicy.cleanup(); + } + if (secondPolicy) { + await secondPolicy.cleanup(); + } + }); + + const testData = getCreateMultipleData(); + it(`should get correct atifact when multiple entries are created`, async () => { + // Create first trusted app + await createArtifact(testData, { + policyId: firstPolicy.packagePolicy.id, + suffix: firstSuffix, + }); + await toasts.dismiss(); + + // Create second trusted app + await createArtifact(testData, { + policyId: secondPolicy.packagePolicy.id, + suffix: secondSuffix, + createButton: 'pageAddButton', + }); + await toasts.dismiss(); + + // Create third trusted app + await createArtifact(testData, { suffix: thirdSuffix, createButton: 'pageAddButton' }); + await toasts.dismiss(); + + // Checks if fleet artifact has been updated correctly + await checkFleetArtifacts( + testData.fleetArtifact.identifier, + testData.fleetArtifact.getExpectedUpdatedArtifactWhenCreateMultipleFirst(), + testData.fleetArtifact.getExpectedUpdatedArtifactBodyWhenCreateMultipleFirst( + thirdSuffix, + firstSuffix + ), + firstPolicy + ); + + // Checks if fleet artifact has been updated correctly + await checkFleetArtifacts( + testData.fleetArtifact.identifier, + testData.fleetArtifact.getExpectedUpdatedArtifactWhenCreateMultipleSecond(), + testData.fleetArtifact.getExpectedUpdatedArtifactBodyWhenCreateMultipleSecond( + thirdSuffix, + secondSuffix + ), + secondPolicy + ); + }); + }); + }); +}; diff --git a/x-pack/test/security_solution_endpoint/apps/integrations_feature_flag/endpoint_exceptions.ts b/x-pack/test/security_solution_endpoint/apps/integrations_feature_flag/endpoint_exceptions.ts new file mode 100644 index 0000000000000..ece54cecb47f1 --- /dev/null +++ b/x-pack/test/security_solution_endpoint/apps/integrations_feature_flag/endpoint_exceptions.ts @@ -0,0 +1,245 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { unzip } from 'zlib'; +import { promisify } from 'util'; +import expect from '@kbn/expect'; +import { IndexedHostsAndAlertsResponse } from '@kbn/security-solution-plugin/common/endpoint/index_data'; +import { EXCEPTION_LIST_ITEM_URL } from '@kbn/securitysolution-list-constants'; +import { ArtifactElasticsearchProperties } from '@kbn/fleet-plugin/server/services'; +import { FoundExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { WebElementWrapper } from '@kbn/ftr-common-functional-ui-services'; +import { FtrProviderContext } from '../../ftr_provider_context'; +import { targetTags } from '../../target_tags'; + +export default ({ getPageObjects, getService }: FtrProviderContext) => { + const pageObjects = getPageObjects(['common', 'header']); + const queryBar = getService('queryBar'); + const testSubjects = getService('testSubjects'); + const endpointTestResources = getService('endpointTestResources'); + const endpointArtifactTestResources = getService('endpointArtifactTestResources'); + const retry = getService('retry'); + const esClient = getService('es'); + const supertest = getService('supertest'); + const find = getService('find'); + const unzipPromisify = promisify(unzip); + const comboBox = getService('comboBox'); + const toasts = getService('toasts'); + + describe('Endpoint Exceptions', function () { + targetTags(this, ['@ess', '@serverless']); + + this.timeout(10 * 60_000); + + const clearPrefilledEntries = async () => { + const entriesContainer = await testSubjects.find('exceptionEntriesContainer'); + + let deleteButtons: WebElementWrapper[]; + do { + deleteButtons = await testSubjects.findAllDescendant( + 'builderItemEntryDeleteButton', + entriesContainer + ); + + await deleteButtons[0].click(); + } while (deleteButtons.length > 1); + }; + + const openNewEndpointExceptionFlyout = async () => { + await testSubjects.click('timeline-context-menu-button'); + await testSubjects.click('add-endpoint-exception-menu-item'); + await testSubjects.existOrFail('addExceptionFlyout'); + + await retry.waitFor('entries should be loaded', () => + testSubjects.exists('exceptionItemEntryContainer') + ); + }; + + const setLastFieldsValue = async ({ + testSubj, + value, + }: { + testSubj: string; + value: string; + optionSelector?: string; + }) => { + const fields = await find.allByCssSelector(`[data-test-subj="${testSubj}"]`); + + const lastField = fields[fields.length - 1]; + await lastField.click(); + + await retry.try( + async () => { + await comboBox.setElement(lastField, value); + }, + async () => { + // If the above fails due to an option not existing, create the value custom instead + await comboBox.setFilterValue(lastField, value); + await pageObjects.common.pressEnterKey(); + } + ); + }; + + const setLastEntry = async ({ + field, + operator, + value, + }: { + field: string; + operator: 'matches' | 'is'; + value: string; + }) => { + await setLastFieldsValue({ testSubj: 'fieldAutocompleteComboBox', value: field }); + await setLastFieldsValue({ testSubj: 'operatorAutocompleteComboBox', value: operator }); + await setLastFieldsValue({ + testSubj: operator === 'matches' ? 'valuesAutocompleteWildcard' : 'valuesAutocompleteMatch', + value, + }); + }; + + const checkArtifact = (expectedArtifact: object) => { + return retry.tryForTime(120_000, async () => { + const artifacts = await endpointArtifactTestResources.getArtifactsFromUnifiedManifestSO(); + + const foundArtifactId = artifacts + .flatMap((artifact) => artifact.artifactIds) + .find((artifactId) => artifactId.startsWith('endpoint-exceptionlist-macos-v1')); + + expect(foundArtifactId).to.not.be(undefined); + + // Get fleet artifact + const artifactResult = await esClient.get({ + index: '.fleet-artifacts-7', + id: `endpoint:${foundArtifactId!}`, + }); + + const artifact = artifactResult._source as ArtifactElasticsearchProperties; + + const zippedBody = Buffer.from(artifact.body, 'base64'); + const artifactBody = await unzipPromisify(zippedBody); + + expect(JSON.parse(artifactBody.toString())).to.eql(expectedArtifact); + }); + }; + + let indexedData: IndexedHostsAndAlertsResponse; + before(async () => { + indexedData = await endpointTestResources.loadEndpointData(); + + const waitForAlertsToAppear = async () => { + await pageObjects.common.navigateToUrlWithBrowserHistory('security', `/alerts`); + await pageObjects.header.waitUntilLoadingHasFinished(); + await retry.waitForWithTimeout('alerts to appear', 10 * 60_000, async () => { + await queryBar.clickQuerySubmitButton(); + return testSubjects.exists('timeline-context-menu-button'); + }); + }; + + await waitForAlertsToAppear(); + }); + + after(async () => { + await endpointTestResources.unloadEndpointData(indexedData); + }); + + beforeEach(async () => { + const deleteEndpointExceptions = async () => { + const { body } = await supertest + .get(`${EXCEPTION_LIST_ITEM_URL}/_find?list_id=endpoint_list&namespace_type=agnostic`) + .set('kbn-xsrf', 'true'); + + for (const exceptionListItem of (body as FoundExceptionListItemSchema).data) { + await supertest + .delete(`${EXCEPTION_LIST_ITEM_URL}?id=${exceptionListItem.id}&namespace_type=agnostic`) + .set('kbn-xsrf', 'true'); + } + }; + + await deleteEndpointExceptions(); + }); + + it('should add `event.module=endpoint` to entry if only wildcard operator is present', async () => { + await pageObjects.common.navigateToUrlWithBrowserHistory('security', `/alerts`); + + await openNewEndpointExceptionFlyout(); + await clearPrefilledEntries(); + + await testSubjects.setValue('exceptionFlyoutNameInput', 'test exception'); + await setLastEntry({ field: 'file.path', operator: 'matches', value: '*/cheese/*' }); + await testSubjects.click('exceptionsAndButton'); + await setLastEntry({ field: 'process.executable', operator: 'matches', value: 'ex*' }); + + await testSubjects.click('addExceptionConfirmButton'); + await toasts.dismiss(); + + await checkArtifact({ + entries: [ + { + type: 'simple', + entries: [ + { + field: 'file.path', + operator: 'included', + type: 'wildcard_cased', + value: '*/cheese/*', + }, + { + field: 'process.executable', + operator: 'included', + type: 'wildcard_cased', + value: 'ex*', + }, + { + // this additional entry should be added + field: 'event.module', + operator: 'included', + type: 'exact_cased', + value: 'endpoint', + }, + ], + }, + ], + }); + }); + + it('should NOT add `event.module=endpoint` to entry if there is another operator', async () => { + await pageObjects.common.navigateToUrlWithBrowserHistory('security', `/alerts`); + + await openNewEndpointExceptionFlyout(); + await clearPrefilledEntries(); + + await testSubjects.setValue('exceptionFlyoutNameInput', 'test exception'); + await setLastEntry({ field: 'file.path', operator: 'matches', value: '*/cheese/*' }); + await testSubjects.click('exceptionsAndButton'); + await setLastEntry({ field: 'process.executable', operator: 'is', value: 'something' }); + + await testSubjects.click('addExceptionConfirmButton'); + await toasts.dismiss(); + + await checkArtifact({ + entries: [ + { + type: 'simple', + entries: [ + { + field: 'file.path', + operator: 'included', + type: 'wildcard_cased', + value: '*/cheese/*', + }, + { + field: 'process.executable', + operator: 'included', + type: 'exact_cased', + value: 'something', + }, + ], + }, + ], + }); + }); + }); +}; diff --git a/x-pack/test/security_solution_endpoint/apps/integrations_feature_flag/index.ts b/x-pack/test/security_solution_endpoint/apps/integrations_feature_flag/index.ts new file mode 100644 index 0000000000000..5464cf07f02e3 --- /dev/null +++ b/x-pack/test/security_solution_endpoint/apps/integrations_feature_flag/index.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getRegistryUrl as getRegistryUrlFromIngest } from '@kbn/fleet-plugin/server'; +import { isServerlessKibanaFlavor } from '@kbn/security-solution-plugin/scripts/endpoint/common/stack_services'; +import { FtrProviderContext } from '../../ftr_provider_context'; +import { + getRegistryUrlFromTestEnv, + isRegistryEnabled, +} from '../../../security_solution_endpoint_api_int/registry'; + +export default function (providerContext: FtrProviderContext) { + const { loadTestFile, getService, getPageObjects } = providerContext; + + describe('endpoint', function () { + const ingestManager = getService('ingestManager'); + const log = getService('log'); + const endpointTestResources = getService('endpointTestResources'); + const kbnClient = getService('kibanaServer'); + + if (!isRegistryEnabled()) { + log.warning('These tests are being run with an external package registry'); + } + + const registryUrl = getRegistryUrlFromTestEnv() ?? getRegistryUrlFromIngest(); + log.info(`Package registry URL for tests: ${registryUrl}`); + + before(async () => { + log.info('calling Fleet setup'); + await ingestManager.setup(); + + log.info('installing/upgrading Endpoint fleet package'); + await endpointTestResources.installOrUpgradeEndpointFleetPackage(); + + if (await isServerlessKibanaFlavor(kbnClient)) { + log.info('login for serverless environment'); + const pageObjects = getPageObjects(['svlCommonPage']); + await pageObjects.svlCommonPage.login(); + } + }); + loadTestFile(require.resolve('./artifact_entries_list')); + loadTestFile(require.resolve('./endpoint_exceptions')); + }); +} diff --git a/x-pack/test/security_solution_endpoint/apps/integrations_feature_flag/mocks.ts b/x-pack/test/security_solution_endpoint/apps/integrations_feature_flag/mocks.ts new file mode 100644 index 0000000000000..47523694b4349 --- /dev/null +++ b/x-pack/test/security_solution_endpoint/apps/integrations_feature_flag/mocks.ts @@ -0,0 +1,807 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FullAgentPolicy } from '@kbn/fleet-plugin/common/types'; +import { ArtifactElasticsearchProperties } from '@kbn/fleet-plugin/server/services/artifacts/types'; +import { InternalUnifiedManifestBaseSchema } from '@kbn/security-solution-plugin/server/endpoint/schemas/artifacts'; +import { TranslatedExceptionListItem } from '@kbn/security-solution-plugin/server/endpoint/schemas/artifacts/lists'; + +export interface AgentPolicyResponseType { + _index: string; + _id: string; + _score: number; + _source: { data: FullAgentPolicy }; +} + +export interface InternalUnifiedManifestSchemaResponseType { + _index: string; + _id: string; + _score: number; + _source: { + 'endpoint:unified-user-artifact-manifest': InternalUnifiedManifestBaseSchema; + }; +} + +export interface ArtifactBodyType { + entries: TranslatedExceptionListItem[]; +} + +export type ArtifactActionsType = ReturnType[0]; +export type MultipleArtifactActionsType = ReturnType; + +export const getArtifactsListTestsData = () => [ + { + title: 'Trusted applications', + pagePrefix: 'trustedAppsListPage', + create: { + formFields: [ + { + type: 'input', + selector: 'trustedApps-form-descriptionField', + value: 'This is the trusted application description', + }, + { + type: 'input', + selector: 'trustedApps-form-nameTextField', + value: 'Trusted application name', + }, + { + type: 'click', + selector: 'trustedApps-form-conditionsBuilder-group1-entry0-field', + }, + { + type: 'click', + selector: 'trustedApps-form-conditionsBuilder-group1-entry0-field-type-Hash', + }, + { + type: 'input', + selector: 'trustedApps-form-conditionsBuilder-group1-entry0-value', + value: 'A4370C0CF81686C0B696FA6261c9d3e0d810ae704ab8301839dffd5d5112f476', + }, + ], + checkResults: [ + { + selector: 'trustedAppsListPage-card-criteriaConditions', + value: + 'OSIS Windows\nAND process.hash.*IS a4370c0cf81686c0b696fa6261c9d3e0d810ae704ab8301839dffd5d5112f476', + }, + ], + }, + update: { + formFields: [ + { + type: 'input', + selector: 'trustedApps-form-descriptionField', + value: 'This is the trusted application description edited', + }, + { + type: 'input', + selector: 'trustedApps-form-nameTextField', + value: 'Trusted application name edited', + }, + { + type: 'click', + selector: 'trustedApps-form-conditionsBuilder-group1-entry0-field', + }, + { + type: 'click', + selector: 'trustedApps-form-conditionsBuilder-group1-entry0-field-type-Path', + }, + { + type: 'input', + selector: 'trustedApps-form-conditionsBuilder-group1-entry0-value', + value: 'c:\\randomFolder\\randomFile.exe, c:\\randomFolder\\randomFile2.exe', + }, + ], + checkResults: [ + { + selector: 'trustedAppsListPage-card-criteriaConditions', + value: + 'OSIS Windows\nAND process.executable.caselessIS c:\\randomFolder\\randomFile.exe, c:\\randomFolder\\randomFile2.exe', + }, + { + selector: 'trustedAppsListPage-card-header-title', + value: 'Trusted application name edited', + }, + { + selector: 'trustedAppsListPage-card-description', + value: 'This is the trusted application description edited', + }, + ], + waitForValue: + 'OSIS Windows\nAND process.executable.caselessIS c:\\randomFolder\\randomFile.exe, c:\\randomFolder\\randomFile2.exe', + }, + delete: { + confirmSelector: 'trustedAppsListPage-deleteModal-submitButton', + card: 'trustedAppsListPage-card', + }, + urlPath: 'trusted_apps', + pageObject: 'trustedApps', + fleetArtifact: { + identifier: 'endpoint-trustlist-windows-v1', + type: 'trustedApplications', + getExpectedUpdatedtArtifactWhenCreate: (): ArtifactElasticsearchProperties => ({ + type: 'trustlist', + identifier: 'endpoint-trustlist-windows-v1', + body: 'eJxVzNEKgyAUgOF3OdcxNMvMVxkxTp4jCa5EbWxE7z422MVuvx/+A3itOXABez2gvhKDhRLuKTI0f80HjgQWUt4cl3JZsCyXsmDba2hgS5yxbhkshNXFnZig+f34ia7eHJYvPjDuH8VODcIJ543URjsx61F71K2WbiTFgowUyIPocDZKSKNG8p566qVsfTdoOKdzOt89hz0Q', + package_name: 'endpoint', + created: '2000-01-01T00:00:00.000Z', + relative_url: + '/api/fleet/artifacts/endpoint-trustlist-windows-v1/016bec11c5b1d6f8609fd3525202aa12baf0132484abf368d5011100d5ec1ec4', + compression_algorithm: 'zlib', + decoded_size: 193, + decoded_sha256: '016bec11c5b1d6f8609fd3525202aa12baf0132484abf368d5011100d5ec1ec4', + encryption_algorithm: 'none', + encoded_sha256: '814aabc04d674ccdeb7c1acfe74120cb52ad1392d6924a7d813e08f8b6cd0f0f', + encoded_size: 153, + }), + getExpectedUpdatedArtifactBodyWhenCreate: (): ArtifactBodyType => ({ + entries: [ + { + type: 'simple', + entries: [ + { + field: 'process.hash.sha256', + operator: 'included', + type: 'exact_cased', + value: 'a4370c0cf81686c0b696fa6261c9d3e0d810ae704ab8301839dffd5d5112f476', + }, + ], + }, + ], + }), + getExpectedUpdatedArtifactWhenUpdate: (): ArtifactElasticsearchProperties => ({ + type: 'trustlist', + identifier: 'endpoint-trustlist-windows-v1', + body: 'eJx9jEEKwjAUBa8ibx1cuMwBvIQtEpMnBH6TkJ9KpeTuEkHBjcthhtnB1Gqkwl52tGchLDQuRQjz4+6REmBRavZUPXKjX5u7vcNcWF3LFRYxeVkDA8xnx835dvVOKVSFwcPJOoS301RdCnk5ZwmsX4rC8TeHf8VpJOhzn/sLJpZG8A==', + package_name: 'endpoint', + created: '2000-01-01T00:00:00.000Z', + relative_url: + '/api/fleet/artifacts/endpoint-trustlist-windows-v1/ac2bf74a73885f9a5a1700c328bf1a5a8f6cb72f2465a575335ea99dac0d4c10', + compression_algorithm: 'zlib', + decoded_size: 198, + decoded_sha256: 'ac2bf74a73885f9a5a1700c328bf1a5a8f6cb72f2465a575335ea99dac0d4c10', + encryption_algorithm: 'none', + encoded_sha256: '28d81b2787cea23fcb88d02b1c09940858963a62c60cdfd7a2b7564cfc251708', + encoded_size: 130, + }), + getExpectedUpdatedArtifactBodyWhenUpdate: (): ArtifactBodyType => ({ + entries: [ + { + type: 'simple', + entries: [ + { + field: 'process.executable', + operator: 'included', + type: 'exact_caseless', + value: 'c:\\randomFolder\\randomFile.exe, c:\\randomFolder\\randomFile2.exe', + }, + ], + }, + ], + }), + }, + }, + { + title: 'Event Filters', + pagePrefix: 'EventFiltersListPage', + create: { + formFields: [ + { + type: 'input', + selector: 'eventFilters-form-name-input', + value: 'Event filter name', + }, + { + type: 'input', + selector: 'eventFilters-form-description-input', + value: 'This is the event filter description', + }, + { + type: 'click', + selector: 'fieldAutocompleteComboBox', + }, + { + type: 'customClick', + selector: 'button[title="agent.ephemeral_id"]', + }, + { + type: 'click', + selector: 'valuesAutocompleteMatch', + }, + { + type: 'input', + selector: 'valuesAutocompleteMatch', + value: 'endpoint', + }, + ], + checkResults: [ + { + selector: 'EventFiltersListPage-card-criteriaConditions-condition', + value: 'AND agent.ephemeral_idIS endpoint', + }, + ], + }, + update: { + formFields: [ + { + type: 'input', + selector: 'eventFilters-form-name-input', + value: 'Event filter name edited', + }, + { + type: 'input', + selector: 'eventFilters-form-description-input', + value: 'This is the event filter description edited', + }, + { + type: 'click', + selector: 'fieldAutocompleteComboBox', + }, + { + type: 'input', + selector: 'fieldAutocompleteComboBox', + value: 'agent.id', + }, + { + type: 'customClick', + selector: 'button[title="agent.id"]', + }, + { + type: 'input', + selector: 'valuesAutocompleteMatch', + value: 'test super large value', + }, + { + type: 'click', + selector: 'eventFilters-form-description-input', + }, + ], + checkResults: [ + { + selector: 'EventFiltersListPage-card-criteriaConditions-condition', + value: 'AND agent.idIS test super large value', + }, + { + selector: 'EventFiltersListPage-card-header-title', + value: 'Event filter name edited', + }, + { + selector: 'EventFiltersListPage-card-description', + value: 'This is the event filter description edited', + }, + ], + waitForValue: 'AND agent.idIS test super large value', + }, + delete: { + confirmSelector: 'EventFiltersListPage-deleteModal-submitButton', + card: 'EventFiltersListPage-card', + }, + urlPath: 'event_filters', + pageObject: 'eventFilters', + fleetArtifact: { + identifier: 'endpoint-eventfilterlist-windows-v1', + type: 'eventfilterlist', + getExpectedUpdatedtArtifactWhenCreate: (): ArtifactElasticsearchProperties => ({ + type: 'eventfilterlist', + identifier: 'endpoint-eventfilterlist-windows-v1', + body: 'eJxVzFEKwjAQRdG9vO/iArKVUsqQPHVgmoRkWpSSvYvFH3/PhXuC2ZuyI8wn/F2JgK5bNWL6a3elJQTIg9lvrE9ubGKrJkwolU28NARojrYnfvW340uir1H6hYfYfmlOtWh2jGUs4wOrCC+X', + package_name: 'endpoint', + created: '2000-01-01T00:00:00.000Z', + relative_url: + '/api/fleet/artifacts/endpoint-eventfilterlist-windows-v1/b3373c93ffc795d954f22c625c084dc5874a156ec0cb3d4af1c3dab0b965fa30', + compression_algorithm: 'zlib', + decoded_size: 136, + decoded_sha256: 'b3373c93ffc795d954f22c625c084dc5874a156ec0cb3d4af1c3dab0b965fa30', + encryption_algorithm: 'none', + encoded_sha256: 'cc9bc4e3cc2c2767c3f56b17ebf4901dbe7e82f15720d48c745370e028c5e887', + encoded_size: 108, + }), + getExpectedUpdatedArtifactBodyWhenCreate: (): ArtifactBodyType => ({ + entries: [ + { + type: 'simple', + entries: [ + { + field: 'agent.ephemeral_id', + operator: 'included', + type: 'exact_cased', + value: 'endpoint', + }, + ], + }, + ], + }), + getExpectedUpdatedArtifactWhenUpdate: (): ArtifactElasticsearchProperties => ({ + type: 'eventfilterlist', + identifier: 'endpoint-eventfilterlist-windows-v1', + body: 'eJxVzEEKwyAURdGtyBuHLsCtlFA++hoEa+T7LQnBvZc0nXR6LtwDLKaJDf5+wPZKeLT0qpmY/tozMUd4yMJitxQxYa1UsVXhkUrIPfLU34SbBHsEaV98S+6nGpu51ivVZdGF7gpjHvP4ADqUMJs=', + package_name: 'endpoint', + created: '2000-01-01T00:00:00.000Z', + relative_url: + '/api/fleet/artifacts/endpoint-eventfilterlist-windows-v1/e4f00c88380d2c429eeb2741ad19383b94d76f79744b098b095befc24003e158', + compression_algorithm: 'zlib', + decoded_size: 140, + decoded_sha256: 'e4f00c88380d2c429eeb2741ad19383b94d76f79744b098b095befc24003e158', + encryption_algorithm: 'none', + encoded_sha256: 'e371e2a28b59bd942ca7ef9665dae7c9b27409ad6f2ca3bff6357a98deb23c12', + encoded_size: 110, + }), + getExpectedUpdatedArtifactBodyWhenUpdate: (): ArtifactBodyType => ({ + entries: [ + { + type: 'simple', + entries: [ + { + field: 'agent.id', + operator: 'included', + type: 'exact_cased', + value: 'test super large value', + }, + ], + }, + ], + }), + }, + }, + { + title: 'Blocklist', + pagePrefix: 'blocklistPage', + create: { + formFields: [ + { + type: 'input', + selector: 'blocklist-form-name-input', + value: 'Blocklist name', + }, + { + type: 'input', + selector: 'blocklist-form-description-input', + value: 'This is the blocklist description', + }, + { + type: 'click', + selector: 'blocklist-form-field-select', + }, + { + type: 'click', + selector: 'blocklist-form-file.hash.*', + }, + { + type: 'input', + selector: 'blocklist-form-values-input', + value: + 'A4370C0CF81686C0B696FA6261c9d3e0d810ae704ab8301839dffd5d5112f476,aedb279e378BED6C2DB3C9DC9e12ba635e0b391c,741462ab431a22233C787BAAB9B653C7', + }, + { + type: 'click', + selector: 'blocklist-form-name-input', + }, + ], + checkResults: [ + { + selector: 'blocklistPage-card-criteriaConditions', + value: + 'OSIS Windows\nAND file.hash.*IS ONE OF\n741462ab431a22233c787baab9b653c7\naedb279e378bed6c2db3c9dc9e12ba635e0b391c\na4370c0cf81686c0b696fa6261c9d3e0d810ae704ab8301839dffd5d5112f476', + }, + ], + }, + update: { + formFields: [ + { + type: 'input', + selector: 'blocklist-form-name-input', + value: 'Blocklist name edited', + }, + { + type: 'input', + selector: 'blocklist-form-description-input', + value: 'This is the blocklist description edited', + }, + { + type: 'click', + selector: 'blocklist-form-field-select', + }, + { + type: 'click', + selector: 'blocklist-form-file.path.caseless', + }, + { + type: 'clear', + selector: + 'blocklist-form-values-input-a4370c0cf81686c0b696fa6261c9d3e0d810ae704ab8301839dffd5d5112f476', + }, + { + type: 'clear', + selector: 'blocklist-form-values-input-741462ab431a22233c787baab9b653c7', + }, + { + type: 'clear', + selector: 'blocklist-form-values-input-aedb279e378bed6c2db3c9dc9e12ba635e0b391c', + }, + { + type: 'input', + selector: 'blocklist-form-values-input', + value: 'c:\\randomFolder\\randomFile.exe, c:\\randomFolder\\randomFile2.exe', + }, + { + type: 'click', + selector: 'blocklist-form-name-input', + }, + ], + checkResults: [ + { + selector: 'blocklistPage-card-criteriaConditions', + value: + 'OSIS Windows\nAND file.path.caselessIS ONE OF\nc:\\randomFolder\\randomFile.exe\nc:\\randomFolder\\randomFile2.exe', + }, + { + selector: 'blocklistPage-card-header-title', + value: 'Blocklist name edited', + }, + { + selector: 'blocklistPage-card-description', + value: 'This is the blocklist description edited', + }, + ], + waitForValue: + 'OSIS Windows\nAND file.path.caselessIS ONE OF\nc:\\randomFolder\\randomFile.exe\nc:\\randomFolder\\randomFile2.exe', + }, + delete: { + confirmSelector: 'blocklistDeletionConfirm', + card: 'blocklistCard', + }, + pageObject: 'blocklist', + urlPath: 'blocklist', + fleetArtifact: { + identifier: 'endpoint-blocklist-windows-v1', + type: 'blocklist', + getExpectedUpdatedtArtifactWhenCreate: (): ArtifactElasticsearchProperties => ({ + type: 'blocklist', + identifier: 'endpoint-blocklist-windows-v1', + relative_url: + '/api/fleet/artifacts/endpoint-blocklist-windows-v1/637f1e8795406904980ae2ab4a69cea967756571507f6bd7fc94cde0add20df2', + body: 'eJylzk1qw0AMQOG7aG3C/GpmfJVggkbSYIPjmNgpDcF3LxS66LLN+sHje4Eu+33SDfrzC/bnqtDDNl3XWaH71dqks0APbZr1NNI2nq4SoYPbqnfab3foYVp4fogKdD8n/STeL0ybyoWWJ3TwQfNDoT9DCjagoxq8Jeec95xyqkS1VIyeEwzHcHR/NW0j2TdQpFJdKupTrirITqrnIlzUukroo5rqi+V/41zEd3jBJ8OGW7aYkU3Fgo3QoeUiXo1ka0iTCVSzNzb7Iq1JlGitayHhN3s4vgDTjqDt', + encryption_algorithm: 'none', + package_name: 'endpoint', + encoded_size: 219, + encoded_sha256: 'e803c1ee6aec0885092bfd6c288839f42b31107dd6d0bb2c8e2d2b9f8fc8b293', + decoded_size: 501, + decoded_sha256: '637f1e8795406904980ae2ab4a69cea967756571507f6bd7fc94cde0add20df2', + compression_algorithm: 'zlib', + created: '2000-01-01T00:00:00.000Z', + }), + getExpectedUpdatedArtifactBodyWhenCreate: (): ArtifactBodyType => ({ + entries: [ + { + type: 'simple', + entries: [ + { + field: 'file.hash.md5', + operator: 'included', + type: 'exact_cased_any', + value: ['741462ab431a22233c787baab9b653c7'], + }, + ], + }, + { + type: 'simple', + entries: [ + { + field: 'file.hash.sha1', + operator: 'included', + type: 'exact_cased_any', + value: ['aedb279e378bed6c2db3c9dc9e12ba635e0b391c'], + }, + ], + }, + { + type: 'simple', + entries: [ + { + field: 'file.hash.sha256', + operator: 'included', + type: 'exact_cased_any', + value: ['a4370c0cf81686c0b696fa6261c9d3e0d810ae704ab8301839dffd5d5112f476'], + }, + ], + }, + ], + }), + getExpectedUpdatedArtifactWhenUpdate: (): ArtifactElasticsearchProperties => ({ + type: 'blocklist', + identifier: 'endpoint-blocklist-windows-v1', + relative_url: + '/api/fleet/artifacts/endpoint-blocklist-windows-v1/3ead6ce4e34cb4411083a44bfe813d9442d296981ee8d56e727e6cff14dea0f0', + body: 'eJx9jUEKwjAURK8isw4uXOYAXqKV8kmmGPhNQpJKS/HuEkHBjcxqmMebA4ytBFbY4UDbM2FRw5KVMD/bHKgeFnNQnrO0OwxSZpGWCixCdLp6epiPhZu4NjmpVNY6Sdxh8BBdCTvA2XEsEn1arkk9y7d1Pbf+fvrHXN7Q7dnzAojqRb8=', + encryption_algorithm: 'none', + package_name: 'endpoint', + encoded_size: 131, + encoded_sha256: 'f0e2dc2aa8d968b704baa11bf3100db91a85991d5de431f8c389b7417335a701', + decoded_size: 197, + decoded_sha256: '3ead6ce4e34cb4411083a44bfe813d9442d296981ee8d56e727e6cff14dea0f0', + compression_algorithm: 'zlib', + created: '2000-01-01T00:00:00.000Z', + }), + getExpectedUpdatedArtifactBodyWhenUpdate: (): ArtifactBodyType => ({ + entries: [ + { + type: 'simple', + entries: [ + { + field: 'file.path', + operator: 'included', + type: 'exact_caseless_any', + value: ['c:\\randomFolder\\randomFile.exe', ' c:\\randomFolder\\randomFile2.exe'], + }, + ], + }, + ], + }), + }, + }, + { + title: 'Host isolation exceptions', + pagePrefix: 'hostIsolationExceptionsListPage', + create: { + formFields: [ + { + type: 'input', + selector: 'hostIsolationExceptions-form-name-input', + value: 'Host Isolation exception name', + }, + { + type: 'input', + selector: 'hostIsolationExceptions-form-description-input', + value: 'This is the host isolation exception description', + }, + { + type: 'input', + selector: 'hostIsolationExceptions-form-ip-input', + value: '1.1.1.1', + }, + ], + checkResults: [ + { + selector: 'hostIsolationExceptionsListPage-card-criteriaConditions', + value: 'OSIS Windows, Linux, Mac\nAND destination.ipIS 1.1.1.1', + }, + ], + }, + update: { + formFields: [ + { + type: 'input', + selector: 'hostIsolationExceptions-form-name-input', + value: 'Host Isolation exception name edited', + }, + { + type: 'input', + selector: 'hostIsolationExceptions-form-description-input', + value: 'This is the host isolation exception description edited', + }, + { + type: 'input', + selector: 'hostIsolationExceptions-form-ip-input', + value: '2.2.2.2/24', + }, + ], + checkResults: [ + { + selector: 'hostIsolationExceptionsListPage-card-criteriaConditions', + value: 'OSIS Windows, Linux, Mac\nAND destination.ipIS 2.2.2.2/24', + }, + { + selector: 'hostIsolationExceptionsListPage-card-header-title', + value: 'Host Isolation exception name edited', + }, + { + selector: 'hostIsolationExceptionsListPage-card-description', + value: 'This is the host isolation exception description edited', + }, + ], + waitForValue: 'OSIS Windows, Linux, Mac\nAND destination.ipIS 2.2.2.2/24', + }, + delete: { + confirmSelector: 'hostIsolationExceptionsDeletionConfirm', + card: 'hostIsolationExceptionsCard', + }, + pageObject: 'hostIsolationExceptions', + urlPath: 'host_isolation_exceptions', + fleetArtifact: { + identifier: 'endpoint-hostisolationexceptionlist-windows-v1', + type: 'hostisolationexceptionlist', + getExpectedUpdatedtArtifactWhenCreate: (): ArtifactElasticsearchProperties => ({ + type: 'hostisolationexceptionlist', + identifier: 'endpoint-hostisolationexceptionlist-windows-v1', + relative_url: + '/api/fleet/artifacts/endpoint-hostisolationexceptionlist-windows-v1/2c3ee2b5e7f86f8c336a3df7e59a1151b11d7eec382442032e69712d6a6459e0', + body: 'eJxVjEEKgzAUBe/y1kFwm6uIyCd5hQ9pEpKvWCR3LxVclNnNwFxgtqbs8MsF+1TCo+u7JsL9tZcyRXhEdtMspiVPWuFQKptYafDQHNIeGeGeFU8JtgXptzwk7T87TzcY61jHF647LBE=', + encryption_algorithm: 'none', + package_name: 'endpoint', + encoded_size: 104, + encoded_sha256: 'f958ada742a0be63d136901317c6bfd04b2ab5f52cdd0e872461089b0009bb3e', + decoded_size: 131, + decoded_sha256: '2c3ee2b5e7f86f8c336a3df7e59a1151b11d7eec382442032e69712d6a6459e0', + compression_algorithm: 'zlib', + created: '2000-01-01T00:00:00.000Z', + }), + getExpectedUpdatedArtifactBodyWhenCreate: (): ArtifactBodyType => ({ + entries: [ + { + type: 'simple', + entries: [ + { + field: 'destination.ip', + operator: 'included', + type: 'exact_cased', + value: '1.1.1.1', + }, + ], + }, + ], + }), + getExpectedUpdatedArtifactWhenUpdate: (): ArtifactElasticsearchProperties => ({ + type: 'hostisolationexceptionlist', + identifier: 'endpoint-hostisolationexceptionlist-windows-v1', + relative_url: + '/api/fleet/artifacts/endpoint-hostisolationexceptionlist-windows-v1/4b62473b4cf057277b3297896771cc1061c3bea2c4f7ec1ef5c2546f33d5d9e8', + body: 'eJxVjEEKwyAUBe/y1pJC6MqrlBA++gofrIr+hJbg3UsCXZTZzcAcYLam7PCPA/aphEfXV02E+2tPZYrwiOymWUxLnrTCoVQ2sdLgoTmkLTLC/VZ8S7A1SL/kLmk77Txd3OY7xjKW8QUwWyyq', + encryption_algorithm: 'none', + package_name: 'endpoint', + encoded_size: 108, + encoded_sha256: '84df618343078f43a54299bcebef03010f3ec4ffdf7160448882fee9bafa1adb', + decoded_size: 134, + decoded_sha256: '4b62473b4cf057277b3297896771cc1061c3bea2c4f7ec1ef5c2546f33d5d9e8', + compression_algorithm: 'zlib', + created: '2000-01-01T00:00:00.000Z', + }), + getExpectedUpdatedArtifactBodyWhenUpdate: (): ArtifactBodyType => ({ + entries: [ + { + type: 'simple', + entries: [ + { + field: 'destination.ip', + operator: 'included', + type: 'exact_cased', + value: '2.2.2.2/24', + }, + ], + }, + ], + }), + }, + }, +]; + +export const getCreateMultipleData = () => ({ + title: 'Trusted applications', + pagePrefix: 'trustedAppsListPage', + create: { + formFields: [ + { + type: 'input', + selector: 'trustedApps-form-descriptionField', + value: 'This is the trusted application description', + }, + { + type: 'input', + selector: 'trustedApps-form-nameTextField', + value: 'Trusted application name', + }, + { + type: 'click', + selector: 'trustedApps-form-conditionsBuilder-group1-entry0-field', + }, + { + type: 'click', + selector: 'trustedApps-form-conditionsBuilder-group1-entry0-field-type-Path', + }, + { + type: 'input', + selector: 'trustedApps-form-conditionsBuilder-group1-entry0-value', + value: 'c:\\randomFolder\\randomFile.exe', + }, + ], + }, + + urlPath: 'trusted_apps', + pageObject: 'trustedApps', + fleetArtifact: { + identifier: 'endpoint-trustlist-windows-v1', + type: 'trustedApplications', + getExpectedUpdatedArtifactWhenCreateMultipleFirst: (): ArtifactElasticsearchProperties => ({ + type: 'trustlist', + identifier: 'endpoint-trustlist-windows-v1', + body: 'eJzNjlEKwjAQBe+y38ED5ABewhaJySsubJuwu5VK6d0lgoI38PMxj2F2wuLKMIqXnfzZQJGM5yag8MMmhhSK1LRmmJ2wIa+ebu9jbdDkVSkSL1nWgkLho8OWsl9zMgjMKNAjydpBjsOgaSl1Plcp0O9iQff7nbXQMR7h79ImVvOeNh4vUR5zdA==', + package_name: 'endpoint', + created: '2000-01-01T00:00:00.000Z', + relative_url: + '/api/fleet/artifacts/endpoint-trustlist-windows-v1/329fc9176a24d64f4376d2c25d5db5b31cf86b288dac83c8a004dfe5bbfdc7d0', + compression_algorithm: 'zlib', + decoded_size: 323, + decoded_sha256: '329fc9176a24d64f4376d2c25d5db5b31cf86b288dac83c8a004dfe5bbfdc7d0', + encryption_algorithm: 'none', + encoded_sha256: '4d9eecb830948eabd721563fd2473900207d043126e66eac2ef78f9e05a80adb', + encoded_size: 136, + }), + getExpectedUpdatedArtifactBodyWhenCreateMultipleFirst: ( + firstSuffix: string, + secondSuffix: string + ): ArtifactBodyType => ({ + entries: [ + { + type: 'simple', + entries: [ + { + field: 'process.executable', + operator: 'included', + type: 'exact_caseless', + value: `c:\\randomFolder\\randomFile.exe${firstSuffix}`, + }, + ], + }, + { + entries: [ + { + field: 'process.executable', + operator: 'included', + type: 'exact_caseless', + value: `c:\\randomFolder\\randomFile.exe${secondSuffix}`, + }, + ], + type: 'simple', + }, + ], + }), + getExpectedUpdatedArtifactWhenCreateMultipleSecond: (): ArtifactElasticsearchProperties => ({ + type: 'trustlist', + identifier: 'endpoint-trustlist-windows-v1', + body: 'eJzNjlEKwjAQRO8y38ED5ABewhaJyYiBbRJ2U6mU3l1aUPAGfg5veLwVLF0zDf6yor8a4WF5akK4H3bPlASPpjXS7MSFce7hdhxro4ZeFR65RJkTE9xHxyXEfo3BKDSDwzPIvIPoh0FDSXU6V0nU78rC3d8fWRO2cXN/l2aMtRxt4/YGxIFzyA==', + package_name: 'endpoint', + created: '2000-01-01T00:00:00.000Z', + relative_url: + '/api/fleet/artifacts/endpoint-trustlist-windows-v1/3be2ce848f9b49d6531e6dc80f43579e00adbc640d3f785c14c8f9fa2652500a', + compression_algorithm: 'zlib', + decoded_size: 324, + decoded_sha256: '3be2ce848f9b49d6531e6dc80f43579e00adbc640d3f785c14c8f9fa2652500a', + encryption_algorithm: 'none', + encoded_sha256: '68304c35bbe863d0fbb15cf7e5ae5c84bad17aa7a3bc26828f9f0b20e0df6ed8', + encoded_size: 136, + }), + getExpectedUpdatedArtifactBodyWhenCreateMultipleSecond: ( + firstSuffix: string, + secondSuffix: string + ): ArtifactBodyType => ({ + entries: [ + { + type: 'simple', + entries: [ + { + field: 'process.executable', + operator: 'included', + type: 'exact_caseless', + value: `c:\\randomFolder\\randomFile.exe${firstSuffix}`, + }, + ], + }, + { + entries: [ + { + field: 'process.executable', + operator: 'included', + type: 'exact_caseless', + value: `c:\\randomFolder\\randomFile.exe${secondSuffix}`, + }, + ], + type: 'simple', + }, + ], + }), + }, +}); diff --git a/x-pack/test/security_solution_endpoint/integrations_feature_flag.config.ts b/x-pack/test/security_solution_endpoint/integrations_feature_flag.config.ts new file mode 100644 index 0000000000000..275f984307bff --- /dev/null +++ b/x-pack/test/security_solution_endpoint/integrations_feature_flag.config.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { resolve } from 'path'; +import { FtrConfigProviderContext } from '@kbn/test'; +import { generateConfig } from './config.base'; +import { services } from './services'; + +export default async function (ftrConfigProviderContext: FtrConfigProviderContext) { + const { readConfigFile } = ftrConfigProviderContext; + + const xpackFunctionalConfig = await readConfigFile( + require.resolve('../functional/config.base.js') + ); + + return generateConfig({ + ftrConfigProviderContext, + baseConfig: xpackFunctionalConfig, + testFiles: [resolve(__dirname, './apps/integrations_feature_flag')], + junitReportName: + 'X-Pack Endpoint Integrations With Feature Flags turned on Functional Tests on ESS', + target: 'ess', + kbnServerArgs: [ + // set the packagerTaskInterval to 5s in order to speed up test executions when checking fleet artifacts + '--xpack.securitySolution.packagerTaskInterval=5s', + `--xpack.securitySolution.enableExperimental=${JSON.stringify(['unifiedManifestEnabled'])}`, + ], + services, + }); +} diff --git a/x-pack/test/security_solution_endpoint/serverless.integrations_feature_flag.config.ts b/x-pack/test/security_solution_endpoint/serverless.integrations_feature_flag.config.ts new file mode 100644 index 0000000000000..7ac35bbe3a101 --- /dev/null +++ b/x-pack/test/security_solution_endpoint/serverless.integrations_feature_flag.config.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { resolve } from 'path'; +import { FtrConfigProviderContext } from '@kbn/test'; +import { generateConfig } from './config.base'; +import { svlServices } from './services'; + +export default async function (ftrConfigProviderContext: FtrConfigProviderContext) { + const { readConfigFile } = ftrConfigProviderContext; + + const svlBaseConfig = await readConfigFile( + require.resolve('../../test_serverless/shared/config.base.ts') + ); + + return generateConfig({ + ftrConfigProviderContext, + baseConfig: svlBaseConfig, + testFiles: [resolve(__dirname, './apps/integrations_feature_flag')], + junitReportName: + 'X-Pack Endpoint Integrations With Feature Flags turned on Functional Tests on ESS', + target: 'serverless', + kbnServerArgs: [ + '--serverless=security', + // set the packagerTaskInterval to 5s in order to speed up test executions when checking fleet artifacts + '--xpack.securitySolution.packagerTaskInterval=5s', + `--xpack.securitySolution.enableExperimental=${JSON.stringify(['unifiedManifestEnabled'])}`, + ], + services: svlServices, + }); +} diff --git a/x-pack/test/security_solution_endpoint/services/endpoint_artifacts.ts b/x-pack/test/security_solution_endpoint/services/endpoint_artifacts.ts index a8fcbf3f982fa..0a914226c9280 100644 --- a/x-pack/test/security_solution_endpoint/services/endpoint_artifacts.ts +++ b/x-pack/test/security_solution_endpoint/services/endpoint_artifacts.ts @@ -6,9 +6,9 @@ */ import type { - ExceptionListItemSchema, - CreateExceptionListSchema, CreateExceptionListItemSchema, + CreateExceptionListSchema, + ExceptionListItemSchema, } from '@kbn/securitysolution-io-ts-list-types'; import { EXCEPTION_LIST_ITEM_URL, EXCEPTION_LIST_URL } from '@kbn/securitysolution-list-constants'; import { Response } from 'superagent'; @@ -19,8 +19,10 @@ import { EVENT_FILTER_LIST_DEFINITION } from '@kbn/security-solution-plugin/publ import { HOST_ISOLATION_EXCEPTIONS_LIST_DEFINITION } from '@kbn/security-solution-plugin/public/management/pages/host_isolation_exceptions/constants'; import { BLOCKLISTS_LIST_DEFINITION } from '@kbn/security-solution-plugin/public/management/pages/blocklist/constants'; import { ManifestConstants } from '@kbn/security-solution-plugin/server/endpoint/lib/artifacts'; + import { FtrService } from '../../functional/ftr_provider_context'; import { InternalManifestSchemaResponseType } from '../apps/integrations/mocks'; +import { InternalUnifiedManifestSchemaResponseType } from '../apps/integrations_feature_flag/mocks'; export interface ArtifactTestData { artifact: ExceptionListItemSchema; @@ -132,8 +134,25 @@ export class EndpointArtifactsTestResources extends FtrService { }); const manifestResult = manifestResults[0] as InternalManifestSchemaResponseType; - const artifacts = manifestResult._source['endpoint:user-artifact-manifest'].artifacts; + return manifestResult._source['endpoint:user-artifact-manifest'].artifacts; + } + + async getArtifactsFromUnifiedManifestSO(): Promise< + Array< + InternalUnifiedManifestSchemaResponseType['_source']['endpoint:unified-user-artifact-manifest'] + > + > { + const { + hits: { hits: manifestResults }, + } = await this.esClient.search({ + index: '.kibana*', + query: { + bool: { filter: [{ term: { type: ManifestConstants.UNIFIED_SAVED_OBJECT_TYPE } }] }, + }, + }); - return artifacts; + return manifestResults.map( + (result) => result._source!['endpoint:unified-user-artifact-manifest'] + ); } } From ba8234e6ee081690f0f189ab980e3cef6e4f8e1b Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Fri, 17 May 2024 09:59:03 -0500 Subject: [PATCH 2/6] fix lint error --- .../functional/test_suites/security/ml/trained_models_list.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/test_serverless/functional/test_suites/security/ml/trained_models_list.ts b/x-pack/test_serverless/functional/test_suites/security/ml/trained_models_list.ts index 6ec414787dde3..8e6928b9817a2 100644 --- a/x-pack/test_serverless/functional/test_suites/security/ml/trained_models_list.ts +++ b/x-pack/test_serverless/functional/test_suites/security/ml/trained_models_list.ts @@ -14,7 +14,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // failsOnMKI, see https://github.com/elastic/kibana/issues/180481 describe('Trained models list', function () { this.tags(['failsOnMKI']); - + before(async () => { await PageObjects.svlCommonPage.login(); await ml.api.syncSavedObjects(); From 68849d439eba5017fc1c527c699a7ded08a1cb64 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Fri, 17 May 2024 11:23:02 -0400 Subject: [PATCH 3/6] skip failing test suite (#183748) --- .../test_suites/observability/slos/fetch_historical_summary.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/slos/fetch_historical_summary.ts b/x-pack/test_serverless/api_integration/test_suites/observability/slos/fetch_historical_summary.ts index d69108007477e..32992e5a3b07b 100644 --- a/x-pack/test_serverless/api_integration/test_suites/observability/slos/fetch_historical_summary.ts +++ b/x-pack/test_serverless/api_integration/test_suites/observability/slos/fetch_historical_summary.ts @@ -21,7 +21,8 @@ export default function ({ getService }: FtrProviderContext) { const sloApi = getService('sloApi'); const SLO_ID = 'slo-fake-1'; - describe('fetch historical summary', () => { + // Failing: See https://github.com/elastic/kibana/issues/183748 + describe.skip('fetch historical summary', () => { before(async () => { const now = moment().startOf('minute'); const curr = now.clone().subtract(30, 'days'); From 3d443c84266739f822c4335051907671f57bd977 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Fri, 17 May 2024 11:24:07 -0400 Subject: [PATCH 4/6] skip failing test suite (#183713) --- .../detection_engine/value_lists/value_list_items.cy.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/value_lists/value_list_items.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/value_lists/value_list_items.cy.ts index 9e9a6ec8fdf61..11fb0aa197450 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/value_lists/value_list_items.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/value_lists/value_list_items.cy.ts @@ -41,7 +41,8 @@ import { import { RULES_MANAGEMENT_URL } from '../../../../urls/rules_management'; import { getDefaultUsername } from '../../../../tasks/common/users'; -describe( +// Failing: See https://github.com/elastic/kibana/issues/183713 +describe.skip( 'Value list items', { tags: ['@ess', '@serverless'], From 8f9c4ab4277c0745d1185733fe1791d09dbacdba Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Fri, 17 May 2024 11:24:59 -0400 Subject: [PATCH 5/6] skip failing test suite (#183750) --- .../test/api_integration/apis/slos/fetch_historical_summary.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/api_integration/apis/slos/fetch_historical_summary.ts b/x-pack/test/api_integration/apis/slos/fetch_historical_summary.ts index 4acdfe569c404..94cccf1e14938 100644 --- a/x-pack/test/api_integration/apis/slos/fetch_historical_summary.ts +++ b/x-pack/test/api_integration/apis/slos/fetch_historical_summary.ts @@ -20,7 +20,8 @@ export default function ({ getService }: FtrProviderContext) { const sloApi = getService('slo'); const SLO_ID = 'slo-fake-1'; - describe('fetch historical summary', () => { + // Failing: See https://github.com/elastic/kibana/issues/183750 + describe.skip('fetch historical summary', () => { before(async () => { const now = moment().startOf('minute'); const curr = now.clone().subtract(30, 'days'); From 60cf299af8cb44e3f87ac4f88c3927973ebad1fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20=C3=81brah=C3=A1m?= Date: Fri, 17 May 2024 18:05:41 +0200 Subject: [PATCH 6/6] [EDR Workflows] Add new backfill function to set `on_write_scan` to `false` if Malware protection is off (#182598) ## Summary This PR adds an additional backfill for `on_write_scan`. This is a fix, because it has been backfilled with a `true` value, but it should be `false` when Malware protection is off. This should be a safe backfill, because: - the feature is behind feature flag, so users did not have the opportunity to modify the value, - and if they would have, still, `malware=off && on_write_scan=true` is an invalid combination, the user cannot achieve it, so there is no chance of destroying user created settings. Additionally, the new model version is added to a different folder to help transitioning our mindsets from migrations to model versions. ### Checklist Delete any items that are not applicable to this PR. - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../check_registered_types.test.ts | 2 +- .../fleet/server/saved_objects/index.ts | 9 + .../model_versions/security_solution/index.ts | 8 + .../v10_on_write_scan_fix.test.ts | 183 ++++++++++++++++++ .../v10_on_write_scan_fix.ts | 42 ++++ 5 files changed, 243 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugins/fleet/server/saved_objects/model_versions/security_solution/index.ts create mode 100644 x-pack/plugins/fleet/server/saved_objects/model_versions/security_solution/v10_on_write_scan_fix.test.ts create mode 100644 x-pack/plugins/fleet/server/saved_objects/model_versions/security_solution/v10_on_write_scan_fix.ts diff --git a/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts b/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts index 6b8a29c70227b..015c517e9a6b8 100644 --- a/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts +++ b/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts @@ -112,7 +112,7 @@ describe('checking migration metadata changes on all registered SO types', () => "ingest-agent-policies": "803dc27e106440c41e8f3c3d8ee8bbb0821bcde2", "ingest-download-sources": "279a68147e62e4d8858c09ad1cf03bd5551ce58d", "ingest-outputs": "daafff49255ab700e07491376fe89f04fc998b91", - "ingest-package-policies": "d63e091b2b3cf2eecaa46ae2533bdd5214a983fc", + "ingest-package-policies": "e6da7d0ee2996241ade23b3a7811fe5d3e449cb2", "ingest_manager_settings": "91445219e7115ff0c45d1dabd5d614a80b421797", "inventory-view": "b8683c8e352a286b4aca1ab21003115a4800af83", "kql-telemetry": "93c1d16c1a0dfca9c8842062cf5ef8f62ae401ad", diff --git a/x-pack/plugins/fleet/server/saved_objects/index.ts b/x-pack/plugins/fleet/server/saved_objects/index.ts index 22b5aa3f5fddb..f74366d924c96 100644 --- a/x-pack/plugins/fleet/server/saved_objects/index.ts +++ b/x-pack/plugins/fleet/server/saved_objects/index.ts @@ -85,6 +85,7 @@ import { migratePackagePolicyEvictionsFromV81102, } from './migrations/security_solution/to_v8_11_0_2'; import { settingsV1 } from './model_versions/v1'; +import { packagePolicyV10OnWriteScanFix } from './model_versions/security_solution'; /* * Saved object types and mappings @@ -540,6 +541,14 @@ export const getSavedObjectTypes = ( }, ], }, + '10': { + changes: [ + { + type: 'data_backfill', + backfillFn: packagePolicyV10OnWriteScanFix, + }, + ], + }, }, migrations: { '7.10.0': migratePackagePolicyToV7100, diff --git a/x-pack/plugins/fleet/server/saved_objects/model_versions/security_solution/index.ts b/x-pack/plugins/fleet/server/saved_objects/model_versions/security_solution/index.ts new file mode 100644 index 0000000000000..780bc55f9cd3e --- /dev/null +++ b/x-pack/plugins/fleet/server/saved_objects/model_versions/security_solution/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { packagePolicyV10OnWriteScanFix } from './v10_on_write_scan_fix'; diff --git a/x-pack/plugins/fleet/server/saved_objects/model_versions/security_solution/v10_on_write_scan_fix.test.ts b/x-pack/plugins/fleet/server/saved_objects/model_versions/security_solution/v10_on_write_scan_fix.test.ts new file mode 100644 index 0000000000000..37ca3dd6d4b0d --- /dev/null +++ b/x-pack/plugins/fleet/server/saved_objects/model_versions/security_solution/v10_on_write_scan_fix.test.ts @@ -0,0 +1,183 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { SavedObject } from '@kbn/core-saved-objects-api-server'; +import type { ModelVersionTestMigrator } from '@kbn/core-test-helpers-model-versions'; +import { createModelVersionTestMigrator } from '@kbn/core-test-helpers-model-versions'; + +import { getSavedObjectTypes } from '../..'; + +import type { PackagePolicy } from '../../../../common'; +import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../../common'; + +describe('backfill for modelVersion 10 - fix on_write_scan field', () => { + let migrator: ModelVersionTestMigrator; + let policyConfigSO: SavedObject; + + beforeEach(() => { + migrator = createModelVersionTestMigrator({ + type: getSavedObjectTypes()[PACKAGE_POLICY_SAVED_OBJECT_TYPE], + }); + + policyConfigSO = { + id: 'mock-saved-object-id', + attributes: { + name: 'Some Policy Name', + package: { + name: 'endpoint', + title: '', + version: '', + }, + id: 'endpoint', + policy_id: '', + enabled: true, + namespace: '', + revision: 0, + updated_at: '', + updated_by: '', + created_at: '', + created_by: '', + inputs: [ + { + type: 'endpoint', + enabled: true, + streams: [], + config: { + policy: { + value: { + windows: { + malware: { + mode: 'detect', + }, + antivirus_registration: { + enabled: true, + }, + }, + mac: { + malware: { + mode: 'detect', + }, + }, + linux: { + malware: { + mode: 'detect', + }, + }, + }, + }, + }, + }, + ], + }, + type: PACKAGE_POLICY_SAVED_OBJECT_TYPE, + references: [], + }; + }); + + describe('when updating to model version 10', () => { + it('should change `on_write_scan` from `true` to `false` if Malware is off', () => { + setMalwareMode(policyConfigSO, 'off'); + setOnWriteScan(policyConfigSO, true); + + const migratedPolicyConfigSO = migrator.migrate({ + document: policyConfigSO, + fromVersion: 9, + toVersion: 10, + }); + + expectOnWriteScanToBe(false, migratedPolicyConfigSO); + }); + + it('should not change `on_write_scan` if Malware is detect', () => { + setMalwareMode(policyConfigSO, 'detect'); + setOnWriteScan(policyConfigSO, true); + + const migratedPolicyConfigSO = migrator.migrate({ + document: policyConfigSO, + fromVersion: 9, + toVersion: 10, + }); + + expectOnWriteScanToBe(true, migratedPolicyConfigSO); + }); + + it('should not change `on_write_scan` if Malware is prevent', () => { + setMalwareMode(policyConfigSO, 'prevent'); + setOnWriteScan(policyConfigSO, true); + + const migratedPolicyConfigSO = migrator.migrate({ + document: policyConfigSO, + fromVersion: 9, + toVersion: 10, + }); + + expectOnWriteScanToBe(true, migratedPolicyConfigSO); + }); + }); + + describe('additional test: when updating from model version 5 to model version 10', () => { + it('should add `on_write_scan=false` if Malware is off', () => { + setMalwareMode(policyConfigSO, 'off'); + + const migratedPolicyConfigSO = migrator.migrate({ + document: policyConfigSO, + fromVersion: 5, + toVersion: 10, + }); + + expectOnWriteScanToBe(false, migratedPolicyConfigSO); + }); + + it('should add `on_write_scan=true` if Malware is detect', () => { + setMalwareMode(policyConfigSO, 'detect'); + + const migratedPolicyConfigSO = migrator.migrate({ + document: policyConfigSO, + fromVersion: 5, + toVersion: 10, + }); + + expectOnWriteScanToBe(true, migratedPolicyConfigSO); + }); + + it('should add `on_write_scan=true` if Malware is prevent', () => { + setMalwareMode(policyConfigSO, 'prevent'); + + const migratedPolicyConfigSO = migrator.migrate({ + document: policyConfigSO, + fromVersion: 5, + toVersion: 10, + }); + + expectOnWriteScanToBe(true, migratedPolicyConfigSO); + }); + }); + + const setMalwareMode = (so: SavedObject, level: 'off' | 'detect' | 'prevent') => { + const config = so.attributes.inputs[0].config?.policy.value; + + config.windows.malware.mode = level; + config.mac.malware.mode = level; + config.linux.malware.mode = level; + }; + + const setOnWriteScan = (so: SavedObject, value: boolean) => { + const config = so.attributes.inputs[0].config?.policy.value; + + config.windows.malware.on_write_scan = value; + config.mac.malware.on_write_scan = value; + config.linux.malware.on_write_scan = value; + }; + + const expectOnWriteScanToBe = (expectedValue: boolean, so: SavedObject) => { + const config = so.attributes.inputs[0].config?.policy.value; + + expect(config.windows.malware.on_write_scan).toBe(expectedValue); + expect(config.mac.malware.on_write_scan).toBe(expectedValue); + expect(config.linux.malware.on_write_scan).toBe(expectedValue); + }; +}); diff --git a/x-pack/plugins/fleet/server/saved_objects/model_versions/security_solution/v10_on_write_scan_fix.ts b/x-pack/plugins/fleet/server/saved_objects/model_versions/security_solution/v10_on_write_scan_fix.ts new file mode 100644 index 0000000000000..7f793f7980164 --- /dev/null +++ b/x-pack/plugins/fleet/server/saved_objects/model_versions/security_solution/v10_on_write_scan_fix.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { + SavedObjectModelDataBackfillFn, + SavedObjectUnsanitizedDoc, +} from '@kbn/core-saved-objects-server'; + +import type { PackagePolicy } from '../../../../common'; + +export const packagePolicyV10OnWriteScanFix: SavedObjectModelDataBackfillFn< + PackagePolicy, + PackagePolicy +> = (packagePolicyDoc) => { + if (packagePolicyDoc.attributes.package?.name !== 'endpoint') { + return { attributes: packagePolicyDoc.attributes }; + } + + const updatedPackagePolicyDoc: SavedObjectUnsanitizedDoc = packagePolicyDoc; + + const input = updatedPackagePolicyDoc.attributes.inputs[0]; + + if (input && input.config) { + const policy = input.config.policy.value; + + if (policy.windows.malware.mode === 'off') { + policy.windows.malware.on_write_scan = false; + } + if (policy.mac.malware.mode === 'off') { + policy.mac.malware.on_write_scan = false; + } + if (policy.linux.malware.mode === 'off') { + policy.linux.malware.on_write_scan = false; + } + } + + return { attributes: updatedPackagePolicyDoc.attributes }; +};