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..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 @@ -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", @@ -111,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/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/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 }; +}; diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts index e304dfb3bad4d..565177fa8b560 100644 --- a/x-pack/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/plugins/security_solution/common/experimental_features.ts @@ -257,6 +257,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/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'); 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'], 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'] + ); } } 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'); 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();