Skip to content

Commit 953dc2d

Browse files
authored
Monitoring sources integrations sync (#233311)
## Summary This PR introduces a new source initialisation service for privilege monitoring, adding logic to handle both first-time initialisation (when no sources exist) and re-initialisation (where existing sources are updated and missing ones are created). ### Testing 1. Enable privileged user monitoring in advanced settings 2. Enable experimental feature flag for integrations sync: ``` xpack.securitySolution: enableExperimental: [integrationsSyncEnabled] ``` 3. Dev tools 4. `POST kbn:/api/entity_analytics/monitoring/engine/init {}` 5. `GET kbn:/api/entity_analytics/monitoring/entity_source/list` 6. Should have two integrations: okta, AD and the default index installed. Should then be able to call init again and see the same entity sources from the list endpoint. Should also be able to confirm integrations sync works only if experimental features flag is set
1 parent 63a3ea1 commit 953dc2d

File tree

16 files changed

+531
-20
lines changed

16 files changed

+531
-20
lines changed

x-pack/solutions/security/plugins/security_solution/common/experimental_features.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,11 @@ export const allowedExperimentalValues = Object.freeze({
242242
*/
243243
privilegedUserMonitoringDisabled: false,
244244

245+
/**
246+
* Enables Integrations Sync for Privileged User Monitoring
247+
*/
248+
integrationsSyncEnabled: false,
249+
245250
/**
246251
* Disables the siem migrations feature
247252
*/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
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 { defaultMonitoringUsersIndex } from '../../../../../common/entity_analytics/privileged_user_monitoring/utils';
9+
import type { IntegrationType } from './constants';
10+
import {
11+
AD_ADMIN_ROLES,
12+
getMatchersFor,
13+
getStreamPatternFor,
14+
INTEGRATION_MATCHERS_DETAILED,
15+
INTEGRATION_TYPES,
16+
integrationsSourceIndex,
17+
OKTA_ADMIN_ROLES,
18+
STREAM_INDEX_PATTERNS,
19+
} from './constants';
20+
21+
describe('constants', () => {
22+
const baseIndex = `entity_analytics.privileged_monitoring`;
23+
const baseMonitoringUsersIndex = '.entity_analytics.monitoring';
24+
it('should export all constants', () => {
25+
expect(getMatchersFor).toBeDefined();
26+
});
27+
it('should have correct OKTA_ADMIN_ROLES', () => {
28+
expect(OKTA_ADMIN_ROLES).toMatchInlineSnapshot(`
29+
Array [
30+
"Super Administrator",
31+
"Organization Administrator",
32+
"Group Administrator",
33+
"Application Administrator",
34+
"Mobile Administrator",
35+
"Help Desk Administrator",
36+
"Report Administrator",
37+
"API Access Management Administrator",
38+
"Group Membership Administrator",
39+
"Read-only Administrator",
40+
]
41+
`);
42+
});
43+
44+
it('should have correct AD_ADMIN_ROLES', () => {
45+
expect(AD_ADMIN_ROLES).toEqual(['Domain Admins', 'Enterprise Admins']);
46+
expect(AD_ADMIN_ROLES.length).toBe(2);
47+
});
48+
49+
it('should have correct INTEGRATION_MATCHERS_DETAILED', () => {
50+
expect(INTEGRATION_MATCHERS_DETAILED.okta.values).toEqual(OKTA_ADMIN_ROLES);
51+
expect(INTEGRATION_MATCHERS_DETAILED.ad.values).toEqual(AD_ADMIN_ROLES);
52+
expect(INTEGRATION_MATCHERS_DETAILED.okta.fields).toEqual(['user.roles']);
53+
expect(INTEGRATION_MATCHERS_DETAILED.ad.fields).toEqual(['user.roles']);
54+
});
55+
56+
it('getMatchersFor returns correct matcher array', () => {
57+
expect(getMatchersFor('okta')).toEqual([INTEGRATION_MATCHERS_DETAILED.okta]);
58+
expect(getMatchersFor('ad')).toEqual([INTEGRATION_MATCHERS_DETAILED.ad]);
59+
});
60+
61+
it('IntegrationType type should only allow okta and ad', () => {
62+
const types: IntegrationType[] = ['okta', 'ad'];
63+
expect(types).toEqual(INTEGRATION_TYPES);
64+
});
65+
66+
it('should generate defaultMonitoringUsersIndex', () => {
67+
expect(defaultMonitoringUsersIndex('default')).toBe(`${baseIndex}.default`);
68+
expect(defaultMonitoringUsersIndex('space1')).toBe(`${baseIndex}.space1`);
69+
});
70+
71+
it('should generate integrationsSourceIndex', () => {
72+
expect(integrationsSourceIndex('default', 'okta')).toBe(
73+
`${baseMonitoringUsersIndex}.sources.okta-default`
74+
);
75+
expect(integrationsSourceIndex('space1', 'ad')).toBe(
76+
`${baseMonitoringUsersIndex}.sources.ad-space1`
77+
);
78+
});
79+
80+
it('should generate correct stream index patterns', () => {
81+
expect(STREAM_INDEX_PATTERNS.okta('default')).toBe('logs-entityanalytics_okta.user-default');
82+
expect(STREAM_INDEX_PATTERNS.ad('space1')).toBe('logs-entityanalytics_ad.user-space1');
83+
});
84+
85+
it('getStreamPatternFor returns correct pattern', () => {
86+
expect(getStreamPatternFor('okta', 'default')).toBe('logs-entityanalytics_okta.user-default');
87+
expect(getStreamPatternFor('ad', 'space1')).toBe('logs-entityanalytics_ad.user-space1');
88+
});
89+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
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 { PRIVMON_BASE_INDEX_NAME } from '../../../../../common/constants';
9+
10+
export interface Matcher {
11+
values: string[];
12+
fields: string[];
13+
}
14+
15+
export const OKTA_ADMIN_ROLES: string[] = [
16+
'Super Administrator',
17+
'Organization Administrator',
18+
'Group Administrator',
19+
'Application Administrator',
20+
'Mobile Administrator',
21+
'Help Desk Administrator',
22+
'Report Administrator',
23+
'API Access Management Administrator',
24+
'Group Membership Administrator',
25+
'Read-only Administrator',
26+
];
27+
28+
export const AD_ADMIN_ROLES: string[] = ['Domain Admins', 'Enterprise Admins'];
29+
30+
export const INTEGRATION_MATCHERS_DETAILED: Record<IntegrationType, Matcher> = {
31+
okta: { fields: ['user.roles'], values: OKTA_ADMIN_ROLES },
32+
ad: { fields: ['user.roles'], values: AD_ADMIN_ROLES },
33+
};
34+
35+
export const getMatchersFor = (integration: IntegrationType): Matcher[] => [
36+
INTEGRATION_MATCHERS_DETAILED[integration],
37+
];
38+
// TODO: this should be index source? If so, can follow pattern outlined below.
39+
// e.g. .entity_analytics.monitoring.sources.index-<space>
40+
41+
// .entity_analytics.monitoring.sources.okta-<space>
42+
export const integrationsSourceIndex = (namespace: string, integrationName: string) =>
43+
`${PRIVMON_BASE_INDEX_NAME}.sources.${integrationName}-${namespace}`;
44+
45+
export const PRIVILEGE_MONITORING_PRIVILEGE_CHECK_API =
46+
'/api/entity_analytics/monitoring/privileges/privileges';
47+
48+
export const INTEGRATION_TYPES = ['okta', 'ad'] as const;
49+
export type IntegrationType = (typeof INTEGRATION_TYPES)[number];
50+
51+
export const STREAM_INDEX_PATTERNS: Record<IntegrationType, (namespace: string) => string> = {
52+
okta: (namespace) => `logs-entityanalytics_okta.user-${namespace}`,
53+
ad: (namespace) => `logs-entityanalytics_ad.user-${namespace}`,
54+
};
55+
56+
export const getStreamPatternFor = (integration: IntegrationType, namespace: string): string =>
57+
STREAM_INDEX_PATTERNS[integration](namespace);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
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+
export * from './constants';

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import type {
1616
KibanaRequest,
1717
} from '@kbn/core/server';
1818
import type { TaskManagerStartContract } from '@kbn/task-manager-plugin/server';
19+
import type { ExperimentalFeatures } from '../../../../../common';
1920
import type { MonitoringEngineComponentResource } from '../../../../../common/api/entity_analytics';
2021
import { getPrivilegedMonitorUsersIndex } from '../../../../../common/entity_analytics/privileged_user_monitoring/utils';
2122
import type { ApiKeyManager } from '../auth/api_key';
@@ -36,6 +37,8 @@ export interface PrivilegeMonitoringGlobalDependencies {
3637

3738
apiKeyManager?: ApiKeyManager;
3839
taskManager?: TaskManagerStartContract;
40+
41+
experimentalFeatures?: ExperimentalFeatures;
3942
}
4043

4144
/**
@@ -46,6 +49,7 @@ export class PrivilegeMonitoringDataClient {
4649

4750
constructor(public readonly deps: PrivilegeMonitoringGlobalDependencies) {
4851
this.index = getPrivilegedMonitorUsersIndex(deps.namespace);
52+
this.deps.experimentalFeatures = deps.experimentalFeatures;
4953
}
5054

5155
public getScopedSoClient(request: KibanaRequest, options?: SavedObjectsClientProviderOptions) {

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import type { InitialisationService } from './initialisation_service';
1919
import { createInitialisationService } from './initialisation_service';
2020
import { MonitoringEngineComponentResourceEnum } from '../../../../../common/api/entity_analytics';
2121
import { PrivilegeMonitoringEngineActions } from '../auditing/actions';
22+
import { mockGlobalState } from '../../../../../public/common/mock';
2223

2324
const mockUpsertIndex = jest.fn();
2425
jest.mock('./elasticsearch/indices', () => {
@@ -73,6 +74,7 @@ describe('Privileged User Monitoring: Index Sync Service', () => {
7374
auditLogger: auditMock,
7475
telemetry: telemetryMock,
7576
savedObjects: savedObjectServiceMock,
77+
experimentalFeatures: mockGlobalState.app.enableExperimental,
7678
};
7779

7880
let initService: InitialisationService;

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

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,19 @@ import {
2626
PrivilegeMonitoringEngineDescriptorClient,
2727
} from '../saved_objects';
2828
import { startPrivilegeMonitoringTask } from '../tasks/privilege_monitoring_task';
29+
import { createInitialisationSourcesService } from './initialisation_sources_service';
2930

3031
export type InitialisationService = ReturnType<typeof createInitialisationService>;
3132
export const createInitialisationService = (dataClient: PrivilegeMonitoringDataClient) => {
3233
const { deps } = dataClient;
3334
const { taskManager } = deps;
35+
3436
if (!taskManager) {
3537
throw new Error('Task Manager is not available');
3638
}
3739

3840
const IndexService = createPrivmonIndexService(dataClient);
41+
const InitSourceCreationService = createInitialisationSourcesService(dataClient);
3942

4043
const init = async (
4144
soClient: SavedObjectsClientContract
@@ -56,11 +59,17 @@ export const createInitialisationService = (dataClient: PrivilegeMonitoringDataC
5659
MonitoringEngineComponentResourceEnum.privmon_engine,
5760
'Initializing privilege monitoring engine'
5861
);
59-
6062
const descriptor = await descriptorClient.init();
61-
dataClient.log('debug', `Initialized privileged monitoring engine saved object`);
63+
dataClient.log('info', `Initialized privileged monitoring engine saved object`);
64+
65+
if (deps.experimentalFeatures?.integrationsSyncEnabled ?? false) {
66+
// upsert index AND integration sources
67+
await InitSourceCreationService.upsertSources(monitoringIndexSourceClient);
68+
} else {
69+
// upsert ONLY index source
70+
await createOrUpdateDefaultDataSource(monitoringIndexSourceClient);
71+
}
6272

63-
await createOrUpdateDefaultDataSource(monitoringIndexSourceClient);
6473
try {
6574
dataClient.log('debug', 'Creating privilege user monitoring event.ingested pipeline');
6675
await IndexService.createIngestPipelineIfDoesNotExist();
@@ -148,6 +157,7 @@ export const createInitialisationService = (dataClient: PrivilegeMonitoringDataC
148157
dataClient.log('info', 'Creating default index source for privilege monitoring.');
149158

150159
try {
160+
// TODO: failing test, empty sources array. FIX
151161
const indexSourceDescriptor = monitoringIndexSourceClient.create(defaultIndexSource);
152162

153163
dataClient.log(
@@ -168,6 +178,5 @@ export const createInitialisationService = (dataClient: PrivilegeMonitoringDataC
168178
}
169179
}
170180
};
171-
172181
return { init };
173182
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
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 type { AuditLogger } from '@kbn/core/server';
9+
import type { PrivilegeMonitoringGlobalDependencies } from './data_client';
10+
import { PrivilegeMonitoringDataClient } from './data_client';
11+
import {
12+
createInitialisationSourcesService,
13+
type InitialisationSourcesService,
14+
} from './initialisation_sources_service';
15+
import {
16+
elasticsearchServiceMock,
17+
loggingSystemMock,
18+
analyticsServiceMock,
19+
savedObjectsServiceMock,
20+
} from '@kbn/core/server/mocks';
21+
import type { TaskManagerStartContract } from '@kbn/task-manager-plugin/server';
22+
import { MonitoringEntitySourceDescriptorClient } from '../saved_objects';
23+
import { mockGlobalState } from '../../../../../public/common/mock';
24+
25+
jest.mock('../saved_objects', () => {
26+
const mockEngineDescriptorInit = jest.fn();
27+
const mockFind = jest.fn().mockResolvedValue({
28+
saved_objects: [],
29+
total: 0,
30+
});
31+
return {
32+
MonitoringEntitySourceDescriptorClient: jest.fn().mockImplementation(() => ({
33+
findByIndex: jest.fn(),
34+
create: jest.fn(),
35+
bulkCreate: jest.fn(),
36+
update: jest.fn(),
37+
find: mockFind,
38+
findAll: jest.fn(),
39+
bulkUpsert: jest.fn(),
40+
})),
41+
PrivilegeMonitoringEngineDescriptorClient: jest.fn().mockImplementation(() => ({
42+
init: mockEngineDescriptorInit,
43+
update: jest.fn(),
44+
})),
45+
};
46+
});
47+
48+
describe('createInitialisationSourcesService', () => {
49+
const clusterClientMock = elasticsearchServiceMock.createScopedClusterClient();
50+
const loggerMock = loggingSystemMock.createLogger();
51+
const auditMock = { log: jest.fn().mockReturnValue(undefined) } as unknown as AuditLogger;
52+
const telemetryMock = analyticsServiceMock.createAnalyticsServiceSetup();
53+
54+
const savedObjectServiceMock = savedObjectsServiceMock.createStartContract();
55+
const dataClientDeps: PrivilegeMonitoringGlobalDependencies = {
56+
logger: loggerMock,
57+
clusterClient: clusterClientMock,
58+
namespace: 'default',
59+
kibanaVersion: '9.0.0',
60+
taskManager: {} as TaskManagerStartContract,
61+
auditLogger: auditMock,
62+
telemetry: telemetryMock,
63+
savedObjects: savedObjectServiceMock,
64+
experimentalFeatures: mockGlobalState.app.enableExperimental,
65+
};
66+
67+
let dataClient: PrivilegeMonitoringDataClient;
68+
let initSourcesService: InitialisationSourcesService;
69+
let monitoringDescriptorClient: MonitoringEntitySourceDescriptorClient;
70+
71+
beforeEach(() => {
72+
jest.clearAllMocks();
73+
dataClient = new PrivilegeMonitoringDataClient(dataClientDeps);
74+
initSourcesService = createInitialisationSourcesService(dataClient);
75+
const mockLog = jest.fn();
76+
dataClient.log = mockLog;
77+
monitoringDescriptorClient = new (MonitoringEntitySourceDescriptorClient as jest.Mock)();
78+
});
79+
80+
it('should create sources when none exist', async () => {
81+
(monitoringDescriptorClient.findAll as jest.Mock).mockResolvedValue([]);
82+
await initSourcesService.upsertSources(monitoringDescriptorClient);
83+
expect(monitoringDescriptorClient.bulkCreate).toHaveBeenCalledTimes(1);
84+
expect(monitoringDescriptorClient.update).not.toHaveBeenCalled();
85+
});
86+
87+
it('should update sources when they already exist', async () => {
88+
const existingSources = [
89+
{ id: '1', name: '.entity_analytics.monitoring.users-default' },
90+
{ id: '2', name: '.entity_analytics.monitoring.sources.okta-default' },
91+
{ id: '3', name: '.entity_analytics.monitoring.sources.ad-default' },
92+
];
93+
(monitoringDescriptorClient.findAll as jest.Mock).mockResolvedValue(existingSources);
94+
await initSourcesService.upsertSources(monitoringDescriptorClient);
95+
expect(monitoringDescriptorClient.bulkUpsert).toHaveBeenCalledTimes(1);
96+
});
97+
98+
it('should create missing sources and update existing ones', async () => {
99+
const existingSources = [{ id: '3', name: '.entity_analytics.monitoring.sources.ad-default' }];
100+
(monitoringDescriptorClient.findAll as jest.Mock).mockResolvedValue(existingSources);
101+
await initSourcesService.upsertSources(monitoringDescriptorClient);
102+
expect(monitoringDescriptorClient.bulkUpsert).toHaveBeenCalledTimes(1);
103+
});
104+
});

0 commit comments

Comments
 (0)