Skip to content

Commit 9a2f07c

Browse files
kibanamachineCAWilson94tiansivivenatasha-moore-elastichop-dev
authored
[9.1] [Privmon] Add Functional Tests for Index Sync and init API Access (elastic#229137) (elastic#231179)
# Backport This will backport the following commits from `main` to `9.1`: - [[Privmon] Add Functional Tests for Index Sync and init API Access (elastic#229137)](elastic#229137) <!--- Backport version: 9.6.6 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sorenlouv/backport) <!--BACKPORT [{"author":{"name":"Charlotte Alexandra Wilson","email":"[email protected]"},"sourceCommit":{"committedDate":"2025-08-08T15:43:04Z","message":"[Privmon] Add Functional Tests for Index Sync and init API Access (elastic#229137)\n\nRebased with this PR:\nhttps://github.com/elastic/pull/229019/commits\n\n## Summary\nThis PR introduces some basic FTR tests for the privilege monitor\nsyncing.\n1. ✅ Plain Index Sync Test - Verifies that the Privileged Monitoring\nsystem can correctly:\n\t•\tSync user data from a plain (non-integration) index,\n\t•\tDeduplicate users,\n\t•\tPersist the expected results in the monitoring index.\n\n2. ✅ Verify privilege-based access for init flow\n\t•\tUsers with the correct Kibana privileges receive a 200 OK response\n\t•\tUsers without the required privileges receive a 403 Forbidden\n\n---------\n\nCo-authored-by: Tiago Vila Verde <[email protected]>\nCo-authored-by: kibanamachine <[email protected]>\nCo-authored-by: Tiago Vila Verde <[email protected]>\nCo-authored-by: natasha-moore-elastic <[email protected]>\nCo-authored-by: Mark Hopkin <[email protected]>\nCo-authored-by: machadoum <[email protected]>","sha":"746b4c86024be146c3e3579057235025126aa687","branchLabelMapping":{"^v9.2.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","Team: SecuritySolution","Theme: entity_analytics","Feature:Entity Analytics","Team:Entity Analytics","backport:version","v9.1.0","v9.2.0"],"title":"[Privmon] Add Functional Tests for Index Sync and init API Access","number":229137,"url":"https://github.com/elastic/kibana/pull/229137","mergeCommit":{"message":"[Privmon] Add Functional Tests for Index Sync and init API Access (elastic#229137)\n\nRebased with this PR:\nhttps://github.com/elastic/pull/229019/commits\n\n## Summary\nThis PR introduces some basic FTR tests for the privilege monitor\nsyncing.\n1. ✅ Plain Index Sync Test - Verifies that the Privileged Monitoring\nsystem can correctly:\n\t•\tSync user data from a plain (non-integration) index,\n\t•\tDeduplicate users,\n\t•\tPersist the expected results in the monitoring index.\n\n2. ✅ Verify privilege-based access for init flow\n\t•\tUsers with the correct Kibana privileges receive a 200 OK response\n\t•\tUsers without the required privileges receive a 403 Forbidden\n\n---------\n\nCo-authored-by: Tiago Vila Verde <[email protected]>\nCo-authored-by: kibanamachine <[email protected]>\nCo-authored-by: Tiago Vila Verde <[email protected]>\nCo-authored-by: natasha-moore-elastic <[email protected]>\nCo-authored-by: Mark Hopkin <[email protected]>\nCo-authored-by: machadoum <[email protected]>","sha":"746b4c86024be146c3e3579057235025126aa687"}},"sourceBranch":"main","suggestedTargetBranches":["9.1"],"targetPullRequestStates":[{"branch":"9.1","label":"v9.1.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v9.2.0","branchLabelMappingKey":"^v9.2.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/229137","number":229137,"mergeCommit":{"message":"[Privmon] Add Functional Tests for Index Sync and init API Access (elastic#229137)\n\nRebased with this PR:\nhttps://github.com/elastic/pull/229019/commits\n\n## Summary\nThis PR introduces some basic FTR tests for the privilege monitor\nsyncing.\n1. ✅ Plain Index Sync Test - Verifies that the Privileged Monitoring\nsystem can correctly:\n\t•\tSync user data from a plain (non-integration) index,\n\t•\tDeduplicate users,\n\t•\tPersist the expected results in the monitoring index.\n\n2. ✅ Verify privilege-based access for init flow\n\t•\tUsers with the correct Kibana privileges receive a 200 OK response\n\t•\tUsers without the required privileges receive a 403 Forbidden\n\n---------\n\nCo-authored-by: Tiago Vila Verde <[email protected]>\nCo-authored-by: kibanamachine <[email protected]>\nCo-authored-by: Tiago Vila Verde <[email protected]>\nCo-authored-by: natasha-moore-elastic <[email protected]>\nCo-authored-by: Mark Hopkin <[email protected]>\nCo-authored-by: machadoum <[email protected]>","sha":"746b4c86024be146c3e3579057235025126aa687"}}]}] BACKPORT--> Co-authored-by: Charlotte Alexandra Wilson <[email protected]> Co-authored-by: Tiago Vila Verde <[email protected]> Co-authored-by: Tiago Vila Verde <[email protected]> Co-authored-by: natasha-moore-elastic <[email protected]> Co-authored-by: Mark Hopkin <[email protected]> Co-authored-by: machadoum <[email protected]>
1 parent 5c5529e commit 9a2f07c

File tree

13 files changed

+326
-115
lines changed

13 files changed

+326
-115
lines changed

x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
*/
77

88
import { EXCLUDE_ELASTIC_CLOUD_INDICES, INCLUDE_INDEX_PATTERN } from '../../../../common/constants';
9+
910
export const SCOPE = ['securitySolution'];
1011
export const TYPE = 'entity_analytics:monitoring:privileges:engine';
1112
export const VERSION = '1.0.0';

x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/privilege_monitoring_data_client.ts

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

8-
import type {
9-
Logger,
10-
ElasticsearchClient,
11-
SavedObjectsClientContract,
12-
AuditLogger,
13-
IScopedClusterClient,
14-
AnalyticsServiceSetup,
15-
AuditEvent,
8+
import {
9+
type Logger,
10+
type ElasticsearchClient,
11+
type SavedObjectsClientContract,
12+
type AuditLogger,
13+
type IScopedClusterClient,
14+
type AnalyticsServiceSetup,
15+
type AuditEvent,
1616
} from '@kbn/core/server';
1717

1818
import type { TaskManagerStartContract } from '@kbn/task-manager-plugin/server';
@@ -81,6 +81,8 @@ import {
8181
eventIngestPipeline,
8282
} from './elasticsearch/pipelines/event_ingested';
8383
import type { BulkProcessingResults } from './users/bulk/types';
84+
import { ignoreSONotFoundError } from './saved_objects/helpers';
85+
8486
interface PrivilegeMonitoringClientOpts {
8587
logger: Logger;
8688
clusterClient: IScopedClusterClient;
@@ -146,7 +148,6 @@ export class PrivilegeMonitoringDataClient {
146148
if (this.apiKeyGenerator) {
147149
await this.apiKeyGenerator.generate();
148150
}
149-
150151
await startPrivilegeMonitoringTask({
151152
logger: this.opts.logger,
152153
namespace: this.opts.namespace,
@@ -180,14 +181,13 @@ export class PrivilegeMonitoringDataClient {
180181
},
181182
});
182183
}
183-
184184
return descriptor;
185185
}
186186

187187
async delete(deleteData = false): Promise<{ deleted: boolean }> {
188188
this.log('info', 'Deleting privilege monitoring engine');
189189

190-
await this.engineClient.delete();
190+
await this.engineClient.delete().catch(ignoreSONotFoundError);
191191

192192
if (deleteData) {
193193
await this.esClient.indices.delete(
@@ -208,9 +208,11 @@ export class PrivilegeMonitoringDataClient {
208208
taskManager: this.opts.taskManager,
209209
});
210210

211-
await this.monitoringIndexSourceClient
212-
.findAll({})
213-
.then((sos) => sos.forEach((so) => this.monitoringIndexSourceClient.delete(so.id)));
211+
const allDataSources = await this.monitoringIndexSourceClient.findAll({});
212+
const deleteSourcePromises = allDataSources.map((so) =>
213+
this.monitoringIndexSourceClient.delete(so.id)
214+
);
215+
await Promise.all(deleteSourcePromises);
214216

215217
return { deleted: true };
216218
}
@@ -789,8 +791,8 @@ export class PrivilegeMonitoringDataClient {
789791
}
790792

791793
private createOrUpdateDefaultDataSource = async () => {
792-
const sourceName = `default-monitoring-index-${this.opts.namespace}`;
793-
794+
// const sourceName = `default-monitoring-index-${this.opts.namespace}`;
795+
const sourceName = this.getIndex(); // `entity_analytics.monitoring.users-${this.opts.namespace}`;
794796
const defaultIndexSource: CreateMonitoringEntitySource = {
795797
type: 'index',
796798
managed: true,

x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/routes/monitoring_entity_source/monitoring_entity_source.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,19 +64,16 @@ export const monitoringEntitySourceRoute = (
6464
const secSol = await context.securitySolution;
6565
const client = secSol.getMonitoringEntitySourceDataClient();
6666

67+
const body = await client.init(request.body);
6768
const privMonDataClient = await secSol.getPrivilegeMonitoringDataClient();
6869
const engineStatus = await privMonDataClient.getEngineStatus();
69-
7070
try {
7171
if (engineStatus.status === PRIVILEGE_MONITORING_ENGINE_STATUS.STARTED) {
7272
await privMonDataClient.scheduleNow();
7373
}
7474
} catch (e) {
7575
logger.warn(`[Privilege Monitoring] Error scheduling task, received ${e.message}`);
7676
}
77-
78-
const body = await client.init(request.body);
79-
8077
return response.ok({ body });
8178
} catch (e) {
8279
const error = transformError(e);
@@ -88,7 +85,6 @@ export const monitoringEntitySourceRoute = (
8885
}
8986
}
9087
);
91-
9288
router.versioned
9389
.get({
9490
access: 'public',
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
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 { SavedObjectsErrorHelpers } from '@kbn/core/server';
9+
10+
export const ignoreSONotFoundError = (error: Error) => {
11+
if (!SavedObjectsErrorHelpers.isNotFoundError(error)) {
12+
throw error;
13+
}
14+
};

x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/saved_objects/monitoring_entity_source.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@ export class MonitoringEntitySourceDescriptorClient {
4949
const scopedSoClient = this.dependencies.soClient.asScopedToNamespace(
5050
this.dependencies.namespace
5151
);
52-
5352
return scopedSoClient.find<MonitoringEntitySource>({
5453
type: monitoringEntitySourceTypeName,
5554
filter: this.getQueryFilters(query),

x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/saved_objects/privilege_monitoring.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ describe('PrivilegeMonitoringEngineDescriptorClient', () => {
5252
expect(soClient.create).toHaveBeenCalledWith(
5353
privilegeMonitoringTypeName,
5454
{ status: PRIVILEGE_MONITORING_ENGINE_STATUS.STARTED },
55-
{ id: `privilege-monitoring-${namespace}` }
55+
{ id: `privilege-monitoring-${namespace}`, refresh: 'wait_for' }
5656
);
5757
expect(result).toEqual({ status: 'installing' });
5858
});
@@ -161,7 +161,8 @@ describe('PrivilegeMonitoringEngineDescriptorClient', () => {
161161

162162
expect(soClient.delete).toHaveBeenCalledWith(
163163
privilegeMonitoringTypeName,
164-
`privilege-monitoring-${namespace}`
164+
`privilege-monitoring-${namespace}`,
165+
{ refresh: 'wait_for' }
165166
);
166167
});
167168
});

x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/saved_objects/privilege_monitoring.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export class PrivilegeMonitoringEngineDescriptorClient {
4040
{
4141
status: PRIVILEGE_MONITORING_ENGINE_STATUS.STARTED,
4242
},
43-
{ id: this.getSavedObjectId() }
43+
{ id: this.getSavedObjectId(), refresh: 'wait_for' }
4444
);
4545
return attributes;
4646
}
@@ -97,6 +97,6 @@ export class PrivilegeMonitoringEngineDescriptorClient {
9797

9898
async delete() {
9999
const id = this.getSavedObjectId();
100-
return this.deps.soClient.delete(privilegeMonitoringTypeName, id);
100+
return this.deps.soClient.delete(privilegeMonitoringTypeName, id, { refresh: 'wait_for' });
101101
}
102102
}

x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/tasks/privilege_monitoring_task.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,6 @@ export const startPrivilegeMonitoringTask = async ({
194194
taskManager,
195195
}: StartParams) => {
196196
const taskId = getTaskId(namespace);
197-
198197
try {
199198
await taskManager.ensureScheduled({
200199
id: taskId,

x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/types.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import type { ConfigType } from '../../config';
1818
import type { StartPlugins } from '../../plugin';
1919
import type { SecuritySolutionPluginRouter } from '../../types';
2020
export type EntityAnalyticsConfig = ConfigType['entityAnalytics'];
21-
2221
export interface EntityAnalyticsRoutesDeps {
2322
router: SecuritySolutionPluginRouter;
2423
logger: Logger;

x-pack/test/security_solution_api_integration/test_suites/entity_analytics/monitoring/trial_license_complete_tier/engine.ts

Lines changed: 116 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,39 +5,71 @@
55
* 2.0.
66
*/
77

8-
import expect from '@kbn/expect';
8+
import expect from 'expect';
99
import { FtrProviderContext } from '../../../../ftr_provider_context';
10-
import { dataViewRouteHelpersFactory } from '../../utils/data_view';
11-
import { enablePrivmonSetting } from '../../utils';
10+
import { disablePrivmonSetting, enablePrivmonSetting } from '../../utils';
11+
import { PrivMonUtils } from './privileged_users/utils';
1212

1313
export default ({ getService }: FtrProviderContext) => {
1414
const api = getService('securitySolutionApi');
15-
const supertest = getService('supertest');
15+
const kibanaServer = getService('kibanaServer');
16+
const privMonUtils = PrivMonUtils(getService);
1617
const log = getService('log');
18+
const es = getService('es');
19+
const retry = getService('retry');
1720

18-
describe('@ess @serverless @skipInServerlessMKI Entity Privilege Monitoring APIs', () => {
19-
const dataView = dataViewRouteHelpersFactory(supertest);
20-
const kibanaServer = getService('kibanaServer');
21+
const createUserIndex = async (indexName: string) =>
22+
es.indices.create({
23+
index: indexName,
24+
mappings: {
25+
properties: {
26+
user: {
27+
properties: {
28+
name: {
29+
type: 'keyword',
30+
fields: {
31+
text: { type: 'text' },
32+
},
33+
},
34+
role: {
35+
type: 'keyword',
36+
},
37+
},
38+
},
39+
},
40+
},
41+
});
42+
43+
const waitForPrivMonUsersToBeSynced = async (length = 1) =>
44+
retry.waitForWithTimeout('Wait for PrivMon users to be synced', 90000, async () => {
45+
const res = await api.listPrivMonUsers({ query: {} });
46+
log.info(`PrivMon users sync check: found ${res.body.length} users`);
47+
return res.body.length >= length; // wait until we have at least one user
48+
});
49+
50+
describe('@ess Entity Privilege Monitoring APIs', () => {
2151
before(async () => {
22-
await dataView.create('security-solution');
2352
await enablePrivmonSetting(kibanaServer);
2453
});
2554

2655
after(async () => {
27-
await dataView.delete('security-solution');
56+
await disablePrivmonSetting(kibanaServer);
57+
});
58+
59+
afterEach(async () => {
60+
await api.deleteMonitoringEngine({ query: { data: true } });
2861
});
2962

3063
describe('health', () => {
3164
it('should be healthy', async () => {
32-
log.info(`Checking health of privilege monitoring`);
3365
const res = await api.privMonHealth();
3466

3567
if (res.status !== 200) {
3668
log.error(`Health check failed`);
3769
log.error(JSON.stringify(res.body));
3870
}
3971

40-
expect(res.status).eql(200);
72+
expect(res.status).toEqual(200);
4173
});
4274
});
4375

@@ -51,7 +83,7 @@ export default ({ getService }: FtrProviderContext) => {
5183
log.error(JSON.stringify(res1.body));
5284
}
5385

54-
expect(res1.status).eql(200);
86+
expect(res1.status).toEqual(200);
5587

5688
log.info(`Re-initializing Privilege Monitoring engine`);
5789
const res2 = await api.initMonitoringEngine();
@@ -60,7 +92,78 @@ export default ({ getService }: FtrProviderContext) => {
6092
log.error(JSON.stringify(res2.body));
6193
}
6294

63-
expect(res2.status).eql(200);
95+
expect(res2.status).toEqual(200);
96+
});
97+
});
98+
99+
describe('plain index sync', () => {
100+
const indexName = 'tatooine-privileged-users';
101+
const entitySource = {
102+
type: 'index',
103+
name: 'StarWars',
104+
managed: true,
105+
indexPattern: indexName,
106+
enabled: true,
107+
matchers: [
108+
{
109+
fields: ['user.role'],
110+
values: ['admin'],
111+
},
112+
],
113+
filter: {},
114+
};
115+
116+
beforeEach(async () => {
117+
await createUserIndex(indexName);
118+
});
119+
120+
afterEach(async () => {
121+
try {
122+
await es.indices.delete({ index: indexName }, { ignore: [404] });
123+
} catch (err) {
124+
log.warning(`Failed to clean up in afterEach: ${err.message}`);
125+
}
126+
});
127+
128+
it('should sync plain index', async () => {
129+
// Bulk insert documents
130+
const uniqueUsers = [
131+
'Luke Skywalker',
132+
'Leia Organa',
133+
'Han Solo',
134+
'Chewbacca',
135+
'Obi-Wan Kenobi',
136+
'Yoda',
137+
'R2-D2',
138+
'C-3PO',
139+
'Darth Vader',
140+
].flatMap((name) => [{ index: {} }, { user: { name, role: 'admin' } }]);
141+
const repeatedUsers = Array.from({ length: 150 }).flatMap(() => [
142+
{ index: {} },
143+
{ user: { name: 'C-3PO', role: 'admin' } },
144+
]);
145+
146+
const bulkBody = [...uniqueUsers, ...repeatedUsers];
147+
await es.bulk({ index: indexName, body: bulkBody, refresh: true });
148+
149+
// Call init to trigger the sync
150+
await privMonUtils.initPrivMonEngine();
151+
152+
// register entity source
153+
const response = await api.createEntitySource({ body: entitySource });
154+
expect(response.status).toBe(200);
155+
156+
// default-monitoring-index should exist now
157+
const sources = await api.listEntitySources({ query: {} });
158+
const names = sources.body.map((s: any) => s.name);
159+
expect(names).toContain('StarWars');
160+
await waitForPrivMonUsersToBeSynced(9);
161+
// Check if the users are indexed
162+
const res = await api.listPrivMonUsers({ query: {} });
163+
const userNames = res.body.map((u: any) => u.user.name);
164+
expect(userNames).toContain('Luke Skywalker');
165+
expect(userNames).toContain('C-3PO');
166+
expect(userNames.filter((name: string) => name === 'C-3PO')).toHaveLength(1);
64167
});
65168
});
66169
});

0 commit comments

Comments
 (0)