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:
parent
3f7f0be2a0
commit
5a8a5bfd4c
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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');
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in a new issue