From f638fc4f1537bd70971c20571aad9d8723bdf881 Mon Sep 17 00:00:00 2001 From: Thom Heymann Date: Wed, 9 Dec 2020 17:43:56 +0000 Subject: [PATCH] Add session id to audit log --- .../security/server/audit/audit_events.ts | 40 ++++++++++++++++--- .../server/audit/audit_service.test.ts | 20 ++++++++-- .../security/server/audit/audit_service.ts | 15 ++++--- x-pack/plugins/security/server/plugin.ts | 17 ++++---- .../server/session_management/session.mock.ts | 1 + .../server/session_management/session.test.ts | 14 +++++++ .../server/session_management/session.ts | 11 +++++ 7 files changed, 92 insertions(+), 26 deletions(-) diff --git a/x-pack/plugins/security/server/audit/audit_events.ts b/x-pack/plugins/security/server/audit/audit_events.ts index 0bb2f8ba1a246c..59184562b67ffe 100644 --- a/x-pack/plugins/security/server/audit/audit_events.ts +++ b/x-pack/plugins/security/server/audit/audit_events.ts @@ -8,8 +8,11 @@ import { KibanaRequest } from 'src/core/server'; import { AuthenticationResult } from '../authentication/authentication_result'; /** - * Audit event schema using ECS format. - * https://www.elastic.co/guide/en/ecs/1.6/index.html + * Audit event schema using ECS format: https://www.elastic.co/guide/en/ecs/1.6/index.html + * + * If you add additional fields to the schema ensure you update the Kibana Filebeat module: + * https://github.com/elastic/beats/tree/master/filebeat/module/kibana + * * @public */ export interface AuditEvent { @@ -37,20 +40,45 @@ export interface AuditEvent { }; kibana?: { /** - * Current space id of the request. + * The ID of the space associated with this event. */ space_id?: string; /** - * Saved object that was created, changed, deleted or accessed as part of the action. + * The ID of the user session associated with this event. Each login attempt + * results in a unique session id. + */ + session_id?: string; + /** + * Saved object that was created, changed, deleted or accessed as part of this event. */ saved_object?: { type: string; id: string; }; /** - * Any additional event specific fields. + * Name of authentication provider associated with a login event. + */ + authentication_provider?: string; + /** + * Type of authentication provider associated with a login event. + */ + authentication_type?: string; + /** + * Name of Elasticsearch realm that has authenticated the user. + */ + authentication_realm?: string; + /** + * Name of Elasticsearch realm where the user details were retrieved from. + */ + lookup_realm?: string; + /** + * Set of space IDs that a saved object was shared to. + */ + add_to_spaces?: readonly string[]; + /** + * Set of space IDs that a saved object was removed from. */ - [x: string]: any; + delete_from_spaces?: readonly string[]; }; error?: { code?: string; diff --git a/x-pack/plugins/security/server/audit/audit_service.test.ts b/x-pack/plugins/security/server/audit/audit_service.test.ts index 2b5208368b0317..3179eb25b44b2f 100644 --- a/x-pack/plugins/security/server/audit/audit_service.test.ts +++ b/x-pack/plugins/security/server/audit/audit_service.test.ts @@ -27,6 +27,7 @@ const { logging } = coreMock.createSetup(); const http = httpServiceMock.createSetupContract(); const getCurrentUser = jest.fn().mockReturnValue({ username: 'jdoe', roles: ['admin'] }); const getSpaceId = jest.fn().mockReturnValue('default'); +const getSessionId = jest.fn().mockResolvedValue('SESSION_ID'); beforeEach(() => { logger.info.mockClear(); @@ -45,6 +46,7 @@ describe('#setup', () => { http, getCurrentUser, getSpaceId, + getSID: getSessionId, }) ).toMatchInlineSnapshot(` Object { @@ -70,6 +72,7 @@ describe('#setup', () => { http, getCurrentUser, getSpaceId, + getSID: getSessionId, }); expect(logging.configure).toHaveBeenCalledWith(expect.any(Observable)); }); @@ -82,6 +85,7 @@ describe('#setup', () => { http, getCurrentUser, getSpaceId, + getSID: getSessionId, }); expect(http.registerOnPostAuth).toHaveBeenCalledWith(expect.any(Function)); }); @@ -96,16 +100,17 @@ describe('#asScoped', () => { http, getCurrentUser, getSpaceId, + getSID: getSessionId, }); const request = httpServerMock.createKibanaRequest({ kibanaRequestState: { requestId: 'REQUEST_ID', requestUuid: 'REQUEST_UUID' }, }); - audit.asScoped(request).log({ message: 'MESSAGE', event: { action: 'ACTION' } }); + await audit.asScoped(request).log({ message: 'MESSAGE', event: { action: 'ACTION' } }); expect(logger.info).toHaveBeenCalledWith('MESSAGE', { ecs: { version: '1.6.0' }, event: { action: 'ACTION' }, - kibana: { space_id: 'default' }, + kibana: { space_id: 'default', session_id: 'SESSION_ID' }, message: 'MESSAGE', trace: { id: 'REQUEST_ID' }, user: { name: 'jdoe', roles: ['admin'] }, @@ -123,12 +128,13 @@ describe('#asScoped', () => { http, getCurrentUser, getSpaceId, + getSID: getSessionId, }); const request = httpServerMock.createKibanaRequest({ kibanaRequestState: { requestId: 'REQUEST_ID', requestUuid: 'REQUEST_UUID' }, }); - audit.asScoped(request).log({ message: 'MESSAGE', event: { action: 'ACTION' } }); + await audit.asScoped(request).log({ message: 'MESSAGE', event: { action: 'ACTION' } }); expect(logger.info).not.toHaveBeenCalled(); }); @@ -143,12 +149,13 @@ describe('#asScoped', () => { http, getCurrentUser, getSpaceId, + getSID: getSessionId, }); const request = httpServerMock.createKibanaRequest({ kibanaRequestState: { requestId: 'REQUEST_ID', requestUuid: 'REQUEST_UUID' }, }); - audit.asScoped(request).log(undefined); + await audit.asScoped(request).log(undefined); expect(logger.info).not.toHaveBeenCalled(); }); }); @@ -368,6 +375,7 @@ describe('#getLogger', () => { http, getCurrentUser, getSpaceId, + getSID: getSessionId, }); const auditLogger = auditService.getLogger(pluginId); @@ -398,6 +406,7 @@ describe('#getLogger', () => { http, getCurrentUser, getSpaceId, + getSID: getSessionId, }); const auditLogger = auditService.getLogger(pluginId); @@ -436,6 +445,7 @@ describe('#getLogger', () => { http, getCurrentUser, getSpaceId, + getSID: getSessionId, }); const auditLogger = auditService.getLogger(pluginId); @@ -464,6 +474,7 @@ describe('#getLogger', () => { http, getCurrentUser, getSpaceId, + getSID: getSessionId, }); const auditLogger = auditService.getLogger(pluginId); @@ -493,6 +504,7 @@ describe('#getLogger', () => { http, getCurrentUser, getSpaceId, + getSID: getSessionId, }); const auditLogger = auditService.getLogger(pluginId); diff --git a/x-pack/plugins/security/server/audit/audit_service.ts b/x-pack/plugins/security/server/audit/audit_service.ts index 1a55f769d22bac..9127d8bc996dad 100644 --- a/x-pack/plugins/security/server/audit/audit_service.ts +++ b/x-pack/plugins/security/server/audit/audit_service.ts @@ -36,9 +36,6 @@ interface AuditLogMeta extends AuditEvent { ecs: { version: string; }; - session?: { - id: string; - }; trace: { id: string; }; @@ -57,6 +54,7 @@ interface AuditServiceSetupParams { getCurrentUser( request: KibanaRequest ): ReturnType | undefined; + getSID(request: KibanaRequest): Promise; getSpaceId( request: KibanaRequest ): ReturnType | undefined; @@ -84,6 +82,7 @@ export class AuditService { logging, http, getCurrentUser, + getSID, getSpaceId, }: AuditServiceSetupParams): AuditServiceSetup { if (config.enabled && !config.appender) { @@ -132,12 +131,13 @@ export class AuditService { * }); * ``` */ - const log: AuditLogger['log'] = (event) => { + const log: AuditLogger['log'] = async (event) => { if (!event) { return; } - const user = getCurrentUser(request); const spaceId = getSpaceId(request); + const user = getCurrentUser(request); + const sessionId = await getSID(request); const meta: AuditLogMeta = { ecs: { version: ECS_VERSION }, ...event, @@ -149,11 +149,10 @@ export class AuditService { event.user, kibana: { space_id: spaceId, + session_id: sessionId, ...event.kibana, }, - trace: { - id: request.id, - }, + trace: { id: request.id }, }; if (filterEvent(meta, config.ignore_filters)) { this.ecsLogger.info(event.message!, meta); diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts index 15d25971800f87..edbc6c465dd0d5 100644 --- a/x-pack/plugins/security/server/plugin.ts +++ b/x-pack/plugins/security/server/plugin.ts @@ -173,24 +173,25 @@ export class Plugin { registerSecurityUsageCollector({ usageCollection, config, license }); + const { session } = this.sessionManagementService.setup({ + config, + clusterClient, + http: core.http, + kibanaIndexName: legacyConfig.kibana.index, + taskManager, + }); + const audit = this.auditService.setup({ license, config: config.audit, logging: core.logging, http: core.http, getSpaceId: (request) => spaces?.spacesService.getSpaceId(request), + getSID: (request) => session.getSID(request), getCurrentUser: (request) => this.authc?.getCurrentUser(request), }); const legacyAuditLogger = new SecurityAuditLogger(audit.getLogger()); - const { session } = this.sessionManagementService.setup({ - config, - clusterClient, - http: core.http, - kibanaIndexName: legacyConfig.kibana.index, - taskManager, - }); - this.authc = await setupAuthentication({ legacyAuditLogger, audit, diff --git a/x-pack/plugins/security/server/session_management/session.mock.ts b/x-pack/plugins/security/server/session_management/session.mock.ts index 973341acbfce39..b7402491804077 100644 --- a/x-pack/plugins/security/server/session_management/session.mock.ts +++ b/x-pack/plugins/security/server/session_management/session.mock.ts @@ -10,6 +10,7 @@ import { sessionIndexMock } from './session_index.mock'; export const sessionMock = { create: (): jest.Mocked> => ({ + getSID: jest.fn(), get: jest.fn(), create: jest.fn(), update: jest.fn(), diff --git a/x-pack/plugins/security/server/session_management/session.test.ts b/x-pack/plugins/security/server/session_management/session.test.ts index 3010e70c314217..47e391ed579254 100644 --- a/x-pack/plugins/security/server/session_management/session.test.ts +++ b/x-pack/plugins/security/server/session_management/session.test.ts @@ -56,6 +56,20 @@ describe('Session', () => { }); }); + describe('#getSID', () => { + const mockRequest = httpServerMock.createKibanaRequest(); + + it('returns `undefined` if session cookie does not exist', async () => { + mockSessionCookie.get.mockResolvedValue(null); + await expect(session.getSID(mockRequest)).resolves.toBeUndefined(); + }); + + it('returns session id', async () => { + mockSessionCookie.get.mockResolvedValue(sessionCookieMock.createValue()); + await expect(session.getSID(mockRequest)).resolves.toEqual('some-long-sid'); + }); + }); + describe('#get', () => { const mockAAD = Buffer.from([2, ...Array(255).keys()]).toString('base64'); diff --git a/x-pack/plugins/security/server/session_management/session.ts b/x-pack/plugins/security/server/session_management/session.ts index 4dc83a1abe4af6..3c97c13c2d41d5 100644 --- a/x-pack/plugins/security/server/session_management/session.ts +++ b/x-pack/plugins/security/server/session_management/session.ts @@ -99,6 +99,17 @@ export class Session { this.crypto = nodeCrypto({ encryptionKey: this.options.config.encryptionKey }); } + /** + * Extracts session id for the specified request. + * @param request Request instance to get session value for. + */ + async getSID(request: KibanaRequest) { + const sessionCookieValue = await this.options.sessionCookie.get(request); + if (sessionCookieValue) { + return sessionCookieValue.sid; + } + } + /** * Extracts session value for the specified request. Under the hood it can clear session if it is * invalid or created by the legacy versions of Kibana.