Skip to content

Commit dcdc9d7

Browse files
jeramysoucykibanamachineelena-shostak
authored
[On-Week] Encrypted saved objects integration tests (#232382)
Closes #231616 ## Summary This PR introduces integration tests for the Encrypted Saved Objects (ESO) domain that aim to detect when a change is made to either a registered ESO definition or to migration-related aspects (e.g. model versions) of the corresponding SO type, or when a new ESO type is registered. When these tests are updated to account for the above described changes, the Kibana Security team will be requested for a code owner review, gating the merge of the PR until the changes have been approved. ### Testing The test suite can be run with: ``` node scripts/jest_integration --config=x-pack/platform/plugins/shared/encrypted_saved_objects/jest.integration.config.js ``` ~~1. Confirm that without making any changes, the tests pass 2. Make a change to an ESO definition by adding an attribute to encrypt or to include in AAD. Example: `action` (aka `connector`) type ([link](https://github.com/elastic/kibana/blob/dea6735a5ed5ad898363475c99f758fb12d4ce96/x-pack/platform/plugins/shared/actions/server/saved_objects/index.ts#L77)). 3. Confirm that the first test fails - `detecting changes to encryption registration definitions` 4. Revert those changes, and make a change to an ESO type by changing an existing model version. Example: `action` (aka `connector`) type ([link](https://github.com/elastic/kibana/blob/dea6735a5ed5ad898363475c99f758fb12d4ce96/x-pack/platform/plugins/shared/actions/server/saved_objects/model_versions/connector_model_versions.ts#L11)). 5. Confirm that the second two tests fail - `detecting migration related changes in registered types` - `detecting modelVersion and schema changes in registered types`) 6. Revert those changes, and make a change to an ESO type by adding a model version 7. Confirm that the second two tests fail - `detecting migration related changes in registered types` - `detecting modelVersion and schema changes in registered types`)~~ #### Updated Testing 1. Confirm that without making any changes, the tests pass 2. Make a change to an ESO definition by adding an attribute to encrypt or to include in AAD. Example: `action` (aka `connector`) type ([link](https://github.com/elastic/kibana/blob/dea6735a5ed5ad898363475c99f758fb12d4ce96/x-pack/platform/plugins/shared/actions/server/saved_objects/index.ts#L77)). 3. Confirm that the first test fails - `detecting changes to encryption registration definitions` 4. Revert those changes, and make a change to an ESO type by adding a model version 5. Confirm that the second test fails - `detecting new model versions in registered encrypted types` --------- Co-authored-by: kibanamachine <[email protected]> Co-authored-by: Elena Shostak <[email protected]>
1 parent 8dc38b7 commit dcdc9d7

File tree

5 files changed

+209
-1
lines changed

5 files changed

+209
-1
lines changed
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import { sortBy } from 'lodash';
9+
10+
import type { Root } from '@kbn/core-root-server-internal';
11+
import type { ISavedObjectTypeRegistry } from '@kbn/core-saved-objects-server';
12+
import {
13+
createRootWithCorePlugins,
14+
createTestServers,
15+
type TestElasticsearchUtils,
16+
} from '@kbn/core-test-helpers-kbn-server';
17+
import { extractMigrationInfo } from '@kbn/core-test-helpers-so-type-serializer';
18+
19+
import type { EncryptedSavedObjectsService } from '../../server/crypto';
20+
import * as EncryptedSavedObjectsModule from '../../server/saved_objects';
21+
22+
// This will only change if new ESOs are introduced. This number should never get smaller.
23+
export const ESO_TYPES_COUNT = 18 as const;
24+
25+
describe('checking changes on all registered encrypted SO types', () => {
26+
let esServer: TestElasticsearchUtils;
27+
let root: Root;
28+
let typeRegistry: ISavedObjectTypeRegistry;
29+
let esoService: EncryptedSavedObjectsService;
30+
31+
beforeAll(async () => {
32+
// the ESO service is passed to the setupSavedObjects function, so we can obtain it by spying on this call.
33+
// Normally this service is not accessible outside of the ESO plugin.
34+
const setupSavedObjectsSpy = jest.spyOn(EncryptedSavedObjectsModule, 'setupSavedObjects');
35+
36+
const { startES } = createTestServers({
37+
adjustTimeout: (t: number) => jest.setTimeout(t),
38+
});
39+
40+
esServer = await startES();
41+
root = createRootWithCorePlugins({}, { oss: false });
42+
await root.preboot();
43+
await root.setup();
44+
const coreStart = await root.start();
45+
typeRegistry = coreStart.savedObjects.getTypeRegistry();
46+
esoService = setupSavedObjectsSpy.mock.calls[0][0].service as EncryptedSavedObjectsService;
47+
});
48+
49+
afterAll(async () => {
50+
if (root) {
51+
await root.shutdown();
52+
}
53+
if (esServer) {
54+
await esServer.stop();
55+
}
56+
});
57+
58+
// This test is meant to fail when any change is made to ESO registrations (change or addition).
59+
// Just update the snapshot by running this test file via jest_integration with `-u` and push the update.
60+
// The intent is to trigger a code review from the Kibana Security team to review the ESO changes.
61+
// The number of types in the hashMap should never be reduced, it should only increase.
62+
it('detecting changes to encryption registration definitions', () => {
63+
const hashMap = esoService.getRegisteredTypeHashMap();
64+
65+
expect(hashMap).toMatchInlineSnapshot(`
66+
Object {
67+
"action": "4e9f7946dfcbee267e685618638f76f3d55e65c949bd259487dc4bf004018478",
68+
"action_task_params": "06aa563283bdcd5c07ec433a7d0b8425019ad11d75595ee1431691667ecd2cec",
69+
"ad_hoc_run_params": "6539367aa4ae8340c62f123c3457c6b8d7873c92de68651c70d41028dfe7ed32",
70+
"alert": "d961ff113e2b7995a49483b8937fcbdccfe425ac82b59a050931cd620b043ed1",
71+
"api_key_pending_invalidation": "ce3641d95c31bcc2880a294f0123060dcc5026f0a493befdda74924a7ea5c4a0",
72+
"connector_token": "16ca2154c13c5ee3d3a45b55d4ea6cd33aeaceaef3dc229b002d25470bfc9b3b",
73+
"entity-discovery-api-key": "cd3b5230a513d2d3503583223e48362fbbbc7812aa4710579a62acfa5bbc30e6",
74+
"fleet-fleet-server-host": "3b8d0809aaf8a133596307bc29328207c7ceee1dc72233da75141ec47ad8d327",
75+
"fleet-message-signing-keys": "5cdcf6bf85247267f8876bda4226e871dbfefe01f050e898db7cbc267d57a275",
76+
"fleet-uninstall-tokens": "6e7d75921dcce46e566f175eab1b0e3825fe565f20cdb3c984e7037934d61e23",
77+
"ingest-download-sources": "23eb3cf789fe13b4899215c6f919705b8a44b89f8feba7181e1f5db3c7699d40",
78+
"ingest-outputs": "d66716d5333484a25c57f7917bead5ac2576ec57a4b9eb61701b573f35ab62ad",
79+
"privmon-api-key": "7d7b76b3bc5287a784518731ba66d4f761052177fc04b1a85e5605846ab9de42",
80+
"synthetics-monitor": "f1c060b7be3b30187c4adcb35d74f1fa8a4290bd7faf04fec869de2aa387e21b",
81+
"synthetics-monitor-multi-space": "39c4c6abd28c4173f77c1c89306e92b6b92492c0029274e10620a170be4d4a67",
82+
"synthetics-param": "747ba9d1b7addf5b131713abe7868bd767af6ce0cf8b6b0f335f4ef34b280c7e",
83+
"task": "2d8e9bf532f469805b82051f545b915785d99eabfa050cb1aefbc715c6096b97",
84+
"uptime-synthetics-api-key": "5ca81f180763e85397fa8c6508adcd60efd0f916e29bac6dcd5b4564f1db7375",
85+
}
86+
`);
87+
expect(Object.keys(hashMap).length).toEqual(ESO_TYPES_COUNT);
88+
});
89+
90+
// This test is meant to fail and require an update when a new model version is introduced to any ESO types.
91+
// Just update the snapshot by running this test file via jest_integration with `-u` and push the update.
92+
// There are tests in core which will catch when an SO type is changed, and help the Core team enforce when
93+
// a model version needs to be added. This purpose of this test is to ensure that new model versions for ESOs
94+
// are implemented to the security team's guidelines to adhere to all zero-downtime upgrade considerations.
95+
it('detecting new model versions in registered encrypted types', () => {
96+
const esoTypes = esoService.getRegisteredTypes();
97+
const soTypestoCheck = typeRegistry
98+
.getAllTypes()
99+
.filter((soType) => esoTypes.includes(soType.name));
100+
101+
const modelVersionMap: string[] = sortBy(soTypestoCheck, 'name').flatMap((type, index) => {
102+
const typeMigrationInfo = sortBy(extractMigrationInfo(type).modelVersions, 'version')
103+
.reverse()
104+
.map((mv) => `${type.name}|${mv.version}`);
105+
106+
return typeMigrationInfo;
107+
});
108+
109+
expect(modelVersionMap).toMatchInlineSnapshot(`
110+
Array [
111+
"action|1",
112+
"action_task_params|2",
113+
"action_task_params|1",
114+
"ad_hoc_run_params|2",
115+
"ad_hoc_run_params|1",
116+
"alert|6",
117+
"alert|5",
118+
"alert|4",
119+
"alert|3",
120+
"alert|2",
121+
"alert|1",
122+
"api_key_pending_invalidation|1",
123+
"connector_token|1",
124+
"fleet-fleet-server-host|2",
125+
"fleet-fleet-server-host|1",
126+
"fleet-uninstall-tokens|1",
127+
"ingest-download-sources|1",
128+
"ingest-outputs|8",
129+
"ingest-outputs|7",
130+
"ingest-outputs|6",
131+
"ingest-outputs|5",
132+
"ingest-outputs|4",
133+
"ingest-outputs|3",
134+
"ingest-outputs|2",
135+
"ingest-outputs|1",
136+
"synthetics-monitor|2",
137+
"synthetics-monitor|1",
138+
"task|6",
139+
"task|5",
140+
"task|4",
141+
"task|3",
142+
"task|2",
143+
"task|1",
144+
]
145+
`);
146+
});
147+
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
module.exports = {
9+
bail: true,
10+
forceExit: true,
11+
preset: '@kbn/test/jest_integration_node',
12+
rootDir: '../../../../..',
13+
roots: ['<rootDir>/x-pack/platform/plugins/shared/encrypted_saved_objects'],
14+
testMatch: ['/**/integration_tests/**/*.test.{js,mjs,ts,tsx}'],
15+
};

x-pack/platform/plugins/shared/encrypted_saved_objects/server/crypto/encrypted_saved_object_type_definition.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
* 2.0.
66
*/
77

8+
import { createHash } from 'node:crypto';
9+
810
import type { EncryptedSavedObjectTypeRegistration } from './encrypted_saved_objects_service';
911

1012
/**
@@ -101,4 +103,20 @@ export class EncryptedSavedObjectAttributesDefinition {
101103
}
102104
return aadAttributes;
103105
}
106+
107+
/**
108+
* Gets a unique hash value based on the ESO type properties
109+
* @param typeName optional name of the type.
110+
* @returns string - unique hash for the eso definition, including name if provided
111+
*/
112+
public getDefinitionHash(typeName?: string) {
113+
const hash = createHash('sha256');
114+
const globalData = [
115+
...(typeName ? [typeName] : []),
116+
...Array.from(this.attributesToEncrypt),
117+
...Array.from(this.attributesToIncludeInAAD ?? []),
118+
this.enforceRandomId.toString(),
119+
].join('|');
120+
return hash.update(globalData).digest('hex');
121+
}
104122
}

x-pack/platform/plugins/shared/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,28 @@ export class EncryptedSavedObjectsService {
153153
return this.typeDefinitions.has(type);
154154
}
155155

156+
/**
157+
* Gets an array containing all registered type name
158+
*
159+
* @returns Array<string> - all SO type names registered with the ESO service
160+
*/
161+
public getRegisteredTypes(): string[] {
162+
return Array.from(this.typeDefinitions.keys());
163+
}
164+
165+
/**
166+
* Gets a hash map for all types registered with the ESO service
167+
*
168+
* @returns Record<string, string> - type names and unique hash
169+
*/
170+
public getRegisteredTypeHashMap(): Record<string, string> {
171+
const registeredTypes = {} as Record<string, string>;
172+
for (const [key, value] of this.typeDefinitions) {
173+
registeredTypes[key] = value.getDefinitionHash(key);
174+
}
175+
return registeredTypes;
176+
}
177+
156178
/**
157179
* Checks whether the ESO type has explicitly opted out of enforcing random IDs.
158180
* @param type Saved object type.

x-pack/platform/plugins/shared/encrypted_saved_objects/tsconfig.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
"compilerOptions": {
44
"outDir": "target/types",
55
},
6-
"include": ["server/**/*"],
6+
"include": [
7+
"server/**/*",
8+
"integration_tests/**/*"
9+
],
710
"kbn_references": [
811
"@kbn/security-plugin",
912
"@kbn/config-schema",
@@ -17,6 +20,9 @@
1720
"@kbn/config",
1821
"@kbn/encrypted-saved-objects-shared",
1922
"@kbn/core-saved-objects-utils-server",
23+
"@kbn/core-root-server-internal",
24+
"@kbn/core-test-helpers-kbn-server",
25+
"@kbn/core-test-helpers-so-type-serializer",
2026
],
2127
"exclude": [
2228
"target/**/*",

0 commit comments

Comments
 (0)