Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
05185ed
Index Source Saved Object setup - added index and privmon SO to index…
CAWilson94 May 27, 2025
d6a649a
Merge branch 'main' into ea-12289-priv-users-sync-task
CAWilson94 Jun 11, 2025
12825f2
Index Source Saved Object setup - added index and privmon SO to index…
CAWilson94 May 27, 2025
949a407
Update file mappings for monitoring saved objects - index export in l…
CAWilson94 Jun 11, 2025
a907b23
Replace temp default monitoring config SO config with properly mapped SO
CAWilson94 Jun 13, 2025
8d0e052
input validation for type and name; delete unused SO (temp for testing)
CAWilson94 Jun 13, 2025
1b5de09
unused import
CAWilson94 Jun 13, 2025
fa4e739
get SO with attribute type index, delete unused imports
CAWilson94 Jun 13, 2025
e399b44
Read all 'index'-type sync saved objects and query user.names using i…
CAWilson94 Jun 13, 2025
82944d4
Wip testing save down to SO step
CAWilson94 Jun 16, 2025
ccb120e
WIP: Updates monitoring source to allow for multiple SO, list endpoin…
CAWilson94 Jun 17, 2025
702fa74
WIP: updating internal index with user.names from SO. TODO: clean up …
CAWilson94 Jun 17, 2025
7e5f2a3
Add stale users cleanup, renaming and moving all related methods to o…
CAWilson94 Jun 17, 2025
f834928
Formatting
CAWilson94 Jun 18, 2025
c530131
component testing for syncing users from privmon data client, formatt…
CAWilson94 Jun 18, 2025
3d9e49b
monitoring data client, temp ids for dynamic id addition, TODO for wo…
CAWilson94 Jun 18, 2025
b19c6c5
[CI] Auto-commit changed files from 'node scripts/eslint --no-cache -…
kibanamachine Jun 18, 2025
eceb0c6
Update constants.ts
CAWilson94 Jun 19, 2025
282fd36
Batch request for usernames WIP
CAWilson94 Jun 19, 2025
3303e4e
variable typo
CAWilson94 Jun 19, 2025
1fdaa7c
Bulk operations for create and update into internal index from monito…
CAWilson94 Jun 20, 2025
838f94b
WiP soft delete
CAWilson94 Jun 23, 2025
128b716
stale users WiP updated labelling to correct monitoring status label
CAWilson94 Jun 23, 2025
ff5f7be
soft delete working
CAWilson94 Jun 23, 2025
0cef137
formatting, addressing undefined guards, deleting unused methods
CAWilson94 Jun 23, 2025
7fdb33f
handle no default index created, continue with other indices in sync
CAWilson94 Jun 23, 2025
b352566
basic unit test coverage; splitting out esClient methods; logging
CAWilson94 Jun 24, 2025
e548366
Merge branch 'main' into ea-12289-priv-users-sync-task
CAWilson94 Jun 24, 2025
f0f6cd6
update monitoring labels to is_privileged true false with updated sof…
CAWilson94 Jun 24, 2025
d11a704
formatting, remove commented code, adding comments for complex functions
CAWilson94 Jun 24, 2025
8461686
error unknown type in test fix
CAWilson94 Jun 24, 2025
25e31c5
Merge branch 'main' into ea-12289-priv-users-sync-task
CAWilson94 Jun 24, 2025
19a20d4
Merge branch 'main' into ea-12289-priv-users-sync-task
CAWilson94 Jun 24, 2025
55b14d3
update default monitoring source index to match spec
CAWilson94 Jun 24, 2025
e254da2
Previous spec default monitoring users switch back
CAWilson94 Jun 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,19 @@ paths:
"200":
description: Entity source deleted successfully

/api/entity_analytics/monitoring/entity_source/list:
get:
operationId: listEntitySources
summary: List all entity source configurations
responses:
"200":
description: List of entity sources retrieved
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/MonitoringEntitySourceDescriptor"
components:
schemas:
MonitoringEntitySourceDescriptor:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,5 @@ export const getPrivilegedMonitorUsersIndex = (namespace: string) =>
// Not required in phase 0.
export const getPrivilegedMonitorGroupsIndex = (namespace: string) =>
`${privilegedMonitorBaseIndexName}.groups-${namespace}`;
// Default index for privileged monitoring users. Not required.
export const defaultMonitoringUsersIndex = 'entity_analytics.privileged_monitoring';
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/

import { PRIVILEGE_MONITORING_INTERNAL_INDICES_PATTERN } from '../constants';
import { privilegeMonitoringTypeName } from '../saved_object/privilege_monitoring_type';
import { privilegeMonitoringTypeName } from '../saved_objects';

export const privilegeMonitoringRuntimePrivileges = (sourceIndices: string[]) => ({
cluster: ['manage_ingest_pipelines', 'manage_index_templates'],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
elasticsearchServiceMock,
loggingSystemMock,
} from '@kbn/core/server/mocks';
import { monitoringEntitySourceTypeName } from './saved_object/monitoring_entity_source_type';
import { monitoringEntitySourceTypeName } from './saved_objects';
import type { SavedObject, SavedObjectsFindResponse } from '@kbn/core/server';

describe('MonitoringEntitySourceDataClient', () => {
Expand All @@ -32,6 +32,7 @@ describe('MonitoringEntitySourceDataClient', () => {
const testDescriptor = {
type: 'test-type',
name: 'Test Source',
indexPattern: 'test-index-pattern',
matchers: [
{
fields: ['user.role'],
Expand All @@ -49,13 +50,19 @@ describe('MonitoringEntitySourceDataClient', () => {

describe('init', () => {
it('should initialize Monitoring Entity Source Sync Config Successfully', async () => {
defaultOpts.soClient.update.mockImplementation(() => {
const err = new Error('Not found');
// Simulate Kibana-style 404 error
(err as Error & { output?: { statusCode: number } }).output = { statusCode: 404 };
throw err;
});
defaultOpts.soClient.find.mockResolvedValue({
total: 0,
saved_objects: [],
} as unknown as SavedObjectsFindResponse<unknown, unknown>);

defaultOpts.soClient.create.mockResolvedValue({
id: `entity-analytics-monitoring-entity-source-${namespace}`,
id: 'temp-id', // TODO: update to use dynamic ID
type: monitoringEntitySourceTypeName,
attributes: testDescriptor,
references: [],
Expand All @@ -66,7 +73,9 @@ describe('MonitoringEntitySourceDataClient', () => {
expect(defaultOpts.soClient.create).toHaveBeenCalledWith(
monitoringEntitySourceTypeName,
testDescriptor,
{ id: `entity-analytics-monitoring-entity-source-${namespace}` }
{
id: `entity-analytics-monitoring-entity-source-${namespace}-${testDescriptor.type}-${testDescriptor.indexPattern}`,
}
);

expect(result).toEqual(testDescriptor);
Expand All @@ -76,7 +85,6 @@ describe('MonitoringEntitySourceDataClient', () => {
describe('get', () => {
it('should get Monitoring Entity Source Sync Config Successfully', async () => {
const getResponse = {
id: `entity-analytics-monitoring-entity-source-${namespace}`,
type: monitoringEntitySourceTypeName,
attributes: testDescriptor,
references: [],
Expand All @@ -85,7 +93,7 @@ describe('MonitoringEntitySourceDataClient', () => {
const result = await dataClient.get();
expect(defaultOpts.soClient.get).toHaveBeenCalledWith(
monitoringEntitySourceTypeName,
`entity-analytics-monitoring-entity-source-${namespace}`
`temp-id` // TODO: https://github.com/elastic/security-team/issues/12851
);
expect(result).toEqual(getResponse.attributes);
});
Expand All @@ -98,12 +106,25 @@ describe('MonitoringEntitySourceDataClient', () => {
saved_objects: [{ attributes: testDescriptor }],
} as unknown as SavedObjectsFindResponse<unknown, unknown>;

const testSourceObject = {
filter: {},
indexPattern: 'test-index-pattern',
matchers: [
{
fields: ['user.role'],
values: ['admin'],
},
],
name: 'Test Source',
type: 'test-type',
};

defaultOpts.soClient.find.mockResolvedValue(
existingDescriptor as unknown as SavedObjectsFindResponse<unknown, unknown>
);

defaultOpts.soClient.update.mockResolvedValue({
id: `entity-analytics-monitoring-entity-source-${namespace}`,
id: `temp-id`, // TODO: https://github.com/elastic/security-team/issues/12851
type: monitoringEntitySourceTypeName,
attributes: { ...testDescriptor, name: 'Updated Source' },
references: [],
Expand All @@ -114,8 +135,8 @@ describe('MonitoringEntitySourceDataClient', () => {

expect(defaultOpts.soClient.update).toHaveBeenCalledWith(
monitoringEntitySourceTypeName,
`entity-analytics-monitoring-entity-source-${namespace}`,
testDescriptor,
`entity-analytics-monitoring-entity-source-${namespace}-${testDescriptor.type}-${testDescriptor.indexPattern}`,
testSourceObject,
{ refresh: 'wait_for' }
);

Expand All @@ -128,7 +149,7 @@ describe('MonitoringEntitySourceDataClient', () => {
await dataClient.delete();
expect(mockSavedObjectClient.delete).toHaveBeenCalledWith(
monitoringEntitySourceTypeName,
`entity-analytics-monitoring-entity-source-${namespace}`
`temp-id` // TODO: https://github.com/elastic/security-team/issues/12851
);
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import type {
MonitoringEntitySourceDescriptor,
MonitoringEntitySourceResponse,
} from '../../../../common/api/entity_analytics/privilege_monitoring/monitoring_entity_source/monitoring_entity_source.gen';
import { MonitoringEntitySourceDescriptorClient } from './saved_object/monitoring_entity_source';
import { MonitoringEntitySourceDescriptorClient } from './saved_objects';

interface MonitoringEntitySourceDataClientOpts {
logger: Logger;
Expand All @@ -31,7 +31,9 @@ export class MonitoringEntitySourceDataClient {
public async init(
input: MonitoringEntitySourceDescriptor
): Promise<MonitoringEntitySourceResponse> {
const descriptor = await this.monitoringEntitySourceClient.create(input);
const descriptor = await this.monitoringEntitySourceClient.create({
...input,
});
this.log('debug', 'Initializing MonitoringEntitySourceDataClient Saved Object');
return descriptor;
}
Expand All @@ -46,7 +48,7 @@ export class MonitoringEntitySourceDataClient {

const sanitizedUpdate = {
...update,
matchers: update.matchers?.map((matcher) => ({
matchers: update.matchers?.map((matcher: { fields: string[]; values: string[] }) => ({
fields: matcher.fields ?? [],
values: matcher.values ?? [],
})),
Expand All @@ -60,6 +62,11 @@ export class MonitoringEntitySourceDataClient {
return this.monitoringEntitySourceClient.delete();
}

public async list(): Promise<MonitoringEntitySourceResponse[]> {
this.log('debug', 'Finding all Monitoring Entity Source Sync saved objects');
return this.monitoringEntitySourceClient.findAll();
}

private log(level: Exclude<keyof Logger, 'get' | 'log' | 'isLevelEnabled'>, msg: string) {
this.opts.logger[level](
`[Monitoring Entity Source Sync][namespace: ${this.opts.namespace}] ${msg}`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,23 @@ jest.mock('./tasks/privilege_monitoring_task', () => {
};
});

jest.mock('./saved_object/privilege_monitoring', () => {
jest.mock('./saved_objects', () => {
return {
MonitoringEntitySourceDescriptorClient: jest.fn().mockImplementation(() => ({
findByIndex: jest.fn().mockResolvedValue([]),
create: jest.fn(),
})),
PrivilegeMonitoringEngineDescriptorClient: jest.fn().mockImplementation(() => ({
init: jest.fn().mockResolvedValue({ status: 'success' }),
update: jest.fn(),
})),
};
});

describe('Privilege Monitoring Data Client', () => {
const mockSavedObjectClient = savedObjectsClientMock.create();
const clusterClientMock = elasticsearchServiceMock.createScopedClusterClient();
const esClientMock = clusterClientMock.asCurrentUser;
const loggerMock = loggingSystemMock.createLogger();
const auditMock = { log: jest.fn().mockReturnValue(undefined) };
loggerMock.debug = jest.fn();
Expand Down Expand Up @@ -66,7 +72,7 @@ describe('Privilege Monitoring Data Client', () => {

expect(mockCreateOrUpdateIndex).toHaveBeenCalled();
expect(mockStartPrivilegeMonitoringTask).toHaveBeenCalled();
expect(loggerMock.debug).toHaveBeenCalledTimes(1);
expect(loggerMock.debug).toHaveBeenCalledTimes(3);
expect(auditMock.log).toHaveBeenCalled();
expect(result).toEqual(mockDescriptor);
});
Expand Down Expand Up @@ -137,4 +143,124 @@ describe('Privilege Monitoring Data Client', () => {
// TODO: implement once we have more auditing
});
});

describe('syncAllIndexUsers', () => {
const mockLog = jest.fn();

it('should sync all index users successfully', async () => {
const mockMonitoringSOSources = [
{ name: 'source1', indexPattern: 'index1' },
{ name: 'source2', indexPattern: 'index2' },
];
const findByIndexMock = jest.fn().mockResolvedValue(mockMonitoringSOSources);
Object.defineProperty(dataClient, 'monitoringIndexSourceClient', {
value: {
init: jest.fn().mockResolvedValue({ status: 'success' }),
update: jest.fn(),
findByIndex: findByIndexMock,
},
});
dataClient.syncUsernamesFromIndex = jest.fn().mockResolvedValue(['user1', 'user2']);
await dataClient.plainIndexSync();
expect(findByIndexMock).toHaveBeenCalled();
expect(dataClient.syncUsernamesFromIndex).toHaveBeenCalledTimes(2);
expect(dataClient.syncUsernamesFromIndex).toHaveBeenCalledWith({
indexName: 'index1',
kuery: undefined,
});
});

it('logs and returns if no index sources', async () => {
Object.defineProperty(dataClient, 'log', { value: mockLog });
const findByIndexMock = jest.fn().mockResolvedValue([]);
Object.defineProperty(dataClient, 'monitoringIndexSourceClient', {
value: {
init: jest.fn().mockResolvedValue({ status: 'success' }),
update: jest.fn(),
findByIndex: findByIndexMock,
},
});

await dataClient.plainIndexSync();

expect(mockLog).toHaveBeenCalledWith(
'debug',
expect.stringContaining('No monitoring index sources found. Skipping sync.')
);
});

it('skips sources without indexPattern', async () => {
Object.defineProperty(dataClient, 'monitoringIndexSourceClient', {
value: {
findByIndex: jest.fn().mockResolvedValue([
{ name: 'no-index', indexPattern: undefined },
{ name: 'with-index', indexPattern: 'foo' },
]),
init: jest.fn().mockResolvedValue({ status: 'success' }),
update: jest.fn(),
},
});

dataClient.syncUsernamesFromIndex = jest.fn().mockResolvedValue(['user1']);
Object.defineProperty(dataClient, 'findStaleUsersForIndex', {
value: jest.fn().mockResolvedValue([]),
});
await dataClient.plainIndexSync();
// Should only be called for the source with indexPattern
expect(dataClient.syncUsernamesFromIndex).toHaveBeenCalledTimes(1);
expect(dataClient.syncUsernamesFromIndex).toHaveBeenCalledWith({
indexName: 'foo',
kuery: undefined,
});
});

it('should retrieve all usernames from index and perform bulk ops', async () => {
const mockHits = [
{
_source: { user: { name: 'frodo' } },
_id: '1',
sort: [1],
},
{
_source: { user: { name: 'samwise' } },
_id: '2',
sort: [2],
},
];

const mockMonitoredUserHits = {
hits: {
hits: [
{
_source: { user: { name: 'frodo' } },
_id: '1',
},
{
_source: { user: { name: 'samwise' } },
_id: '2',
},
],
},
};

dataClient.searchUsernamesInIndex = jest
.fn()
.mockResolvedValueOnce({ hits: { hits: mockHits } }) // first batch
.mockResolvedValueOnce({ hits: { hits: [] } }); // second batch = end

dataClient.getMonitoredUsers = jest.fn().mockResolvedValue(mockMonitoredUserHits);
dataClient.buildBulkOperationsForUsers = jest.fn().mockReturnValue([{ index: { _id: '1' } }]);
dataClient.getIndex = jest.fn().mockReturnValue('test-index');

const usernames = await dataClient.syncUsernamesFromIndex({
indexName: 'test-index',
});

expect(usernames).toEqual(['frodo', 'samwise']);
expect(dataClient.searchUsernamesInIndex).toHaveBeenCalledTimes(2);
expect(esClientMock.bulk).toHaveBeenCalled();
expect(dataClient.getMonitoredUsers).toHaveBeenCalledWith(['frodo', 'samwise']);
expect(dataClient.buildBulkOperationsForUsers).toHaveBeenCalled();
});
});
});
Loading