Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Security Assistant] Adds audit logging to knowledge base entry changes #203349

Merged
merged 9 commits into from
Dec 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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
Loading