Add session id to audit log (#85451)

* Add session id to audit log

* fix naming

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Thom Heymann 2020-12-14 11:34:52 +00:00 committed by GitHub
parent 3f7f0be2a0
commit 5a8a5bfd4c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 94 additions and 28 deletions

View file

@ -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.
*/
[x: string]: any;
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.
*/
delete_from_spaces?: readonly string[];
};
error?: {
code?: string;

View file

@ -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 getSID = jest.fn().mockResolvedValue('SESSION_ID');
beforeEach(() => {
logger.info.mockClear();
@ -45,6 +46,7 @@ describe('#setup', () => {
http,
getCurrentUser,
getSpaceId,
getSID,
})
).toMatchInlineSnapshot(`
Object {
@ -70,6 +72,7 @@ describe('#setup', () => {
http,
getCurrentUser,
getSpaceId,
getSID,
});
expect(logging.configure).toHaveBeenCalledWith(expect.any(Observable));
});
@ -82,6 +85,7 @@ describe('#setup', () => {
http,
getCurrentUser,
getSpaceId,
getSID,
});
expect(http.registerOnPostAuth).toHaveBeenCalledWith(expect.any(Function));
});
@ -96,16 +100,17 @@ describe('#asScoped', () => {
http,
getCurrentUser,
getSpaceId,
getSID,
});
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,
});
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,
});
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,
});
const auditLogger = auditService.getLogger(pluginId);
@ -398,6 +406,7 @@ describe('#getLogger', () => {
http,
getCurrentUser,
getSpaceId,
getSID,
});
const auditLogger = auditService.getLogger(pluginId);
@ -436,6 +445,7 @@ describe('#getLogger', () => {
http,
getCurrentUser,
getSpaceId,
getSID,
});
const auditLogger = auditService.getLogger(pluginId);
@ -464,6 +474,7 @@ describe('#getLogger', () => {
http,
getCurrentUser,
getSpaceId,
getSID,
});
const auditLogger = auditService.getLogger(pluginId);
@ -493,6 +504,7 @@ describe('#getLogger', () => {
http,
getCurrentUser,
getSpaceId,
getSID,
});
const auditLogger = auditService.getLogger(pluginId);

View file

@ -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<SecurityPluginSetup['authc']['getCurrentUser']> | undefined;
getSID(request: KibanaRequest): Promise<string | undefined>;
getSpaceId(
request: KibanaRequest
): ReturnType<SpacesPluginSetup['spacesService']['getSpaceId']> | undefined;
@ -84,6 +82,7 @@ export class AuditService {
logging,
http,
getCurrentUser,
getSID,
getSpaceId,
}: AuditServiceSetupParams): AuditServiceSetup {
if (config.enabled && !config.appender) {
@ -134,12 +133,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,
@ -151,11 +151,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);

View file

@ -188,16 +188,6 @@ export class Plugin {
registerSecurityUsageCollector({ usageCollection, config, license });
const audit = this.auditService.setup({
license,
config: config.audit,
logging: core.logging,
http: core.http,
getSpaceId: (request) => spaces?.spacesService.getSpaceId(request),
getCurrentUser: (request) => authenticationSetup.getCurrentUser(request),
});
const legacyAuditLogger = new SecurityAuditLogger(audit.getLogger());
const { session } = this.sessionManagementService.setup({
config,
clusterClient,
@ -206,6 +196,17 @@ export class Plugin {
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) => authenticationSetup.getCurrentUser(request),
});
const legacyAuditLogger = new SecurityAuditLogger(audit.getLogger());
const authenticationSetup = this.authenticationService.setup({
legacyAuditLogger,
audit,

View file

@ -10,6 +10,7 @@ import { sessionIndexMock } from './session_index.mock';
export const sessionMock = {
create: (): jest.Mocked<PublicMethodsOf<Session>> => ({
getSID: jest.fn(),
get: jest.fn(),
create: jest.fn(),
update: jest.fn(),

View file

@ -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');

View file

@ -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.