Skip to content

Commit

Permalink
[Security Assistant] Adds audit logging to knowledge base entry chang…
Browse files Browse the repository at this point in the history
  • Loading branch information
stephmilovic authored Dec 11, 2024
1 parent b9bac16 commit 84a2d40
Show file tree
Hide file tree
Showing 10 changed files with 366 additions and 10 deletions.
12 changes: 12 additions & 0 deletions docs/user/security/audit-logging.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,18 @@ Refer to the corresponding {es} logs for potential write errors.
.1+| `product_documentation_create`
| `unknown` | User requested to install the product documentation for use in AI Assistants.

.2+| `knowledge_base_entry_create`
| `success` | User has created knowledge base entry [id=x]
| `failure` | Failed attempt to create a knowledge base entry

.2+| `knowledge_base_entry_update`
| `success` | User has updated knowledge base entry [id=x]
| `failure` | Failed attempt to update a knowledge base entry

.2+| `knowledge_base_entry_delete`
| `success` | User has deleted knowledge base entry [id=x]
| `failure` | Failed attempt to delete a knowledge base entry

3+a|
====== Type: change

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* 2.0.
*/

import { AuthenticatedUser, ElasticsearchClient, Logger } from '@kbn/core/server';
import { AuditLogger, AuthenticatedUser, ElasticsearchClient, Logger } from '@kbn/core/server';

import { DEFAULT_NAMESPACE_STRING } from '@kbn/core-saved-objects-utils-server';
import { ESSearchRequest, ESSearchResponse } from '@kbn/es-types';
Expand All @@ -19,6 +19,7 @@ export interface AIAssistantDataClientParams {
elasticsearchClientPromise: Promise<ElasticsearchClient>;
kibanaVersion: string;
spaceId: string;
auditLogger?: AuditLogger;
logger: Logger;
indexPatternsResourceName: string;
currentUser: AuthenticatedUser | null;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import {
knowledgeBaseAuditEvent,
KnowledgeBaseAuditAction,
AUDIT_OUTCOME,
AUDIT_CATEGORY,
AUDIT_TYPE,
} from './audit_events';

describe('knowledgeBaseAuditEvent', () => {
it('should generate a success event with id', () => {
const event = knowledgeBaseAuditEvent({
action: KnowledgeBaseAuditAction.CREATE,
id: '123',
outcome: AUDIT_OUTCOME.SUCCESS,
});

expect(event).toEqual({
message: 'User has created knowledge base entry [id=123]',
event: {
action: KnowledgeBaseAuditAction.CREATE,
category: [AUDIT_CATEGORY.DATABASE],
type: [AUDIT_TYPE.CREATION],
outcome: AUDIT_OUTCOME.SUCCESS,
},
});
});
it('should generate a success event with name', () => {
const event = knowledgeBaseAuditEvent({
action: KnowledgeBaseAuditAction.CREATE,
name: 'My document',
outcome: AUDIT_OUTCOME.SUCCESS,
});

expect(event).toEqual({
message: 'User has created knowledge base entry [name="My document"]',
event: {
action: KnowledgeBaseAuditAction.CREATE,
category: [AUDIT_CATEGORY.DATABASE],
type: [AUDIT_TYPE.CREATION],
outcome: AUDIT_OUTCOME.SUCCESS,
},
});
});
it('should generate a success event with name and id', () => {
const event = knowledgeBaseAuditEvent({
action: KnowledgeBaseAuditAction.CREATE,
name: 'My document',
id: '123',
outcome: AUDIT_OUTCOME.SUCCESS,
});

expect(event).toEqual({
message: 'User has created knowledge base entry [id=123, name="My document"]',
event: {
action: KnowledgeBaseAuditAction.CREATE,
category: [AUDIT_CATEGORY.DATABASE],
type: [AUDIT_TYPE.CREATION],
outcome: AUDIT_OUTCOME.SUCCESS,
},
});
});

it('should generate a success event without id or name', () => {
const event = knowledgeBaseAuditEvent({
action: KnowledgeBaseAuditAction.CREATE,
outcome: AUDIT_OUTCOME.SUCCESS,
});

expect(event).toEqual({
message: 'User has created a knowledge base entry',
event: {
action: KnowledgeBaseAuditAction.CREATE,
category: [AUDIT_CATEGORY.DATABASE],
type: [AUDIT_TYPE.CREATION],
outcome: AUDIT_OUTCOME.SUCCESS,
},
});
});

it('should generate a failure event with an error', () => {
const error = new Error('Test error');
const event = knowledgeBaseAuditEvent({
action: KnowledgeBaseAuditAction.CREATE,
id: '456',
error,
});

expect(event).toEqual({
message: 'Failed attempt to create knowledge base entry [id=456]',
event: {
action: KnowledgeBaseAuditAction.CREATE,
category: [AUDIT_CATEGORY.DATABASE],
type: [AUDIT_TYPE.CREATION],
outcome: AUDIT_OUTCOME.FAILURE,
},
error: {
code: error.name,
message: error.message,
},
});
});

it('should handle unknown outcome', () => {
const event = knowledgeBaseAuditEvent({
action: KnowledgeBaseAuditAction.CREATE,
id: '789',
outcome: AUDIT_OUTCOME.UNKNOWN,
});

expect(event).toEqual({
message: 'User is creating knowledge base entry [id=789]',
event: {
action: KnowledgeBaseAuditAction.CREATE,
category: [AUDIT_CATEGORY.DATABASE],
type: [AUDIT_TYPE.CREATION],
outcome: AUDIT_OUTCOME.UNKNOWN,
},
});
});

it('should handle update action', () => {
const event = knowledgeBaseAuditEvent({
action: KnowledgeBaseAuditAction.UPDATE,
id: '123',
outcome: AUDIT_OUTCOME.SUCCESS,
});

expect(event).toEqual({
message: 'User has updated knowledge base entry [id=123]',
event: {
action: KnowledgeBaseAuditAction.UPDATE,
category: [AUDIT_CATEGORY.DATABASE],
type: [AUDIT_TYPE.CHANGE],
outcome: AUDIT_OUTCOME.SUCCESS,
},
});
});

it('should handle delete action', () => {
const event = knowledgeBaseAuditEvent({
action: KnowledgeBaseAuditAction.DELETE,
id: '123',
});

expect(event).toEqual({
message: 'User has deleted knowledge base entry [id=123]',
event: {
action: KnowledgeBaseAuditAction.DELETE,
category: [AUDIT_CATEGORY.DATABASE],
type: [AUDIT_TYPE.DELETION],
outcome: AUDIT_OUTCOME.SUCCESS,
},
});
});

it('should default to success if outcome is not provided and no error exists', () => {
const event = knowledgeBaseAuditEvent({
action: KnowledgeBaseAuditAction.CREATE,
});

expect(event.event?.outcome).toBe(AUDIT_OUTCOME.SUCCESS);
});

it('should prioritize error outcome over provided outcome', () => {
const error = new Error('Error with priority');
const event = knowledgeBaseAuditEvent({
action: KnowledgeBaseAuditAction.CREATE,
outcome: AUDIT_OUTCOME.SUCCESS,
error,
});

expect(event.event?.outcome).toBe(AUDIT_OUTCOME.FAILURE);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { EcsEvent } from '@kbn/core/server';
import { AuditEvent } from '@kbn/security-plugin/server';
import { ArrayElement } from '@kbn/utility-types';

export enum AUDIT_TYPE {
CHANGE = 'change',
DELETION = 'deletion',
ACCESS = 'access',
CREATION = 'creation',
}

export enum AUDIT_CATEGORY {
AUTHENTICATION = 'authentication',
DATABASE = 'database',
WEB = 'web',
}

export enum AUDIT_OUTCOME {
FAILURE = 'failure',
SUCCESS = 'success',
UNKNOWN = 'unknown',
}

export enum KnowledgeBaseAuditAction {
CREATE = 'knowledge_base_entry_create',
UPDATE = 'knowledge_base_entry_update',
DELETE = 'knowledge_base_entry_delete',
}

type VerbsTuple = [string, string, string];
const knowledgeBaseEventVerbs: Record<KnowledgeBaseAuditAction, VerbsTuple> = {
knowledge_base_entry_create: ['create', 'creating', 'created'],
knowledge_base_entry_update: ['update', 'updating', 'updated'],
knowledge_base_entry_delete: ['delete', 'deleting', 'deleted'],
};

const knowledgeBaseEventTypes: Record<KnowledgeBaseAuditAction, ArrayElement<EcsEvent['type']>> = {
knowledge_base_entry_create: AUDIT_TYPE.CREATION,
knowledge_base_entry_update: AUDIT_TYPE.CHANGE,
knowledge_base_entry_delete: AUDIT_TYPE.DELETION,
};

export interface KnowledgeBaseAuditEventParams {
action: KnowledgeBaseAuditAction;
error?: Error;
id?: string;
name?: string;
outcome?: EcsEvent['outcome'];
}

export function knowledgeBaseAuditEvent({
action,
error,
id,
name,
outcome,
}: KnowledgeBaseAuditEventParams): AuditEvent {
let doc = 'a knowledge base entry';
if (id && name) {
doc = `knowledge base entry [id=${id}, name="${name}"]`;
} else if (id) {
doc = `knowledge base entry [id=${id}]`;
} else if (name) {
doc = `knowledge base entry [name="${name}"]`;
}
const [present, progressive, past] = knowledgeBaseEventVerbs[action];
const message = error
? `Failed attempt to ${present} ${doc}`
: outcome === 'unknown'
? `User is ${progressive} ${doc}`
: `User has ${past} ${doc}`;
const type = knowledgeBaseEventTypes[action];

return {
message,
event: {
action,
category: [AUDIT_CATEGORY.DATABASE],
type: type ? [type] : undefined,
outcome: error ? AUDIT_OUTCOME.FAILURE : outcome ?? AUDIT_OUTCOME.SUCCESS,
},
error: error && {
code: error.name,
message: error.message,
},
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import { v4 as uuidv4 } from 'uuid';
import {
AnalyticsServiceSetup,
type AuditLogger,
AuthenticatedUser,
ElasticsearchClient,
Logger,
Expand All @@ -18,6 +19,7 @@ import {
KnowledgeBaseEntryResponse,
KnowledgeBaseEntryUpdateProps,
} from '@kbn/elastic-assistant-common';
import { AUDIT_OUTCOME, KnowledgeBaseAuditAction, knowledgeBaseAuditEvent } from './audit_events';
import {
CREATE_KNOWLEDGE_BASE_ENTRY_ERROR_EVENT,
CREATE_KNOWLEDGE_BASE_ENTRY_SUCCESS_EVENT,
Expand All @@ -26,6 +28,7 @@ import { getKnowledgeBaseEntry } from './get_knowledge_base_entry';
import { CreateKnowledgeBaseEntrySchema, UpdateKnowledgeBaseEntrySchema } from './types';

export interface CreateKnowledgeBaseEntryParams {
auditLogger?: AuditLogger;
esClient: ElasticsearchClient;
knowledgeBaseIndex: string;
logger: Logger;
Expand All @@ -37,6 +40,7 @@ export interface CreateKnowledgeBaseEntryParams {
}

export const createKnowledgeBaseEntry = async ({
auditLogger,
esClient,
knowledgeBaseIndex,
spaceId,
Expand Down Expand Up @@ -75,13 +79,27 @@ export const createKnowledgeBaseEntry = async ({
logger,
user,
});

auditLogger?.log(
knowledgeBaseAuditEvent({
action: KnowledgeBaseAuditAction.CREATE,
id: newKnowledgeBaseEntry?.id,
name: newKnowledgeBaseEntry?.name,
outcome: AUDIT_OUTCOME.SUCCESS,
})
);
telemetry.reportEvent(CREATE_KNOWLEDGE_BASE_ENTRY_SUCCESS_EVENT.eventType, telemetryPayload);
return newKnowledgeBaseEntry;
} catch (err) {
logger.error(
`Error creating Knowledge Base Entry: ${err} with kbResource: ${knowledgeBaseEntry.name}`
);
auditLogger?.log(
knowledgeBaseAuditEvent({
action: KnowledgeBaseAuditAction.CREATE,
outcome: AUDIT_OUTCOME.FAILURE,
error: err,
})
);
telemetry.reportEvent(CREATE_KNOWLEDGE_BASE_ENTRY_ERROR_EVENT.eventType, {
...telemetryPayload,
errorMessage: err.message ?? 'Unknown error',
Expand Down
Loading

0 comments on commit 84a2d40

Please sign in to comment.