Index Patterns page',
- }
- );
- return (
-
- );
-}
diff --git a/src/plugins/embeddable/public/public.api.md b/src/plugins/embeddable/public/public.api.md
index c1db6e98e54de..1621dfa3f2c3c 100644
--- a/src/plugins/embeddable/public/public.api.md
+++ b/src/plugins/embeddable/public/public.api.md
@@ -28,6 +28,7 @@ import { EuiButtonEmptyProps } from '@elastic/eui';
import { EuiComboBoxProps } from '@elastic/eui';
import { EuiConfirmModalProps } from '@elastic/eui';
import { EuiContextMenuPanelDescriptor } from '@elastic/eui';
+import { EuiFlyoutSize } from '@elastic/eui';
import { EuiGlobalToastListToast } from '@elastic/eui';
import { EventEmitter } from 'events';
import { ExclusiveUnion } from '@elastic/eui';
@@ -73,6 +74,7 @@ import { SavedObjectAttributes } from 'kibana/server';
import { SavedObjectAttributes as SavedObjectAttributes_2 } from 'src/core/public';
import { SavedObjectAttributes as SavedObjectAttributes_3 } from 'kibana/public';
import { SavedObjectsClientContract as SavedObjectsClientContract_3 } from 'src/core/public';
+import { SavedObjectsFindOptions as SavedObjectsFindOptions_3 } from 'kibana/public';
import { SavedObjectsFindResponse as SavedObjectsFindResponse_2 } from 'kibana/server';
import { Search } from '@elastic/elasticsearch/api/requestParams';
import { SearchResponse } from 'elasticsearch';
diff --git a/src/plugins/maps_legacy/config.ts b/src/plugins/maps_legacy/config.ts
index 68595944e68b3..9a4e2cb9cb639 100644
--- a/src/plugins/maps_legacy/config.ts
+++ b/src/plugins/maps_legacy/config.ts
@@ -35,7 +35,12 @@ export const configSchema = schema.object({
regionmap: regionmapSchema,
manifestServiceUrl: schema.string({ defaultValue: '' }),
- emsUrl: schema.string({ defaultValue: '' }),
+ emsUrl: schema.conditional(
+ schema.siblingRef('proxyElasticMapsServiceInMaps'),
+ true,
+ schema.never(),
+ schema.string({ defaultValue: '' })
+ ),
emsFileApiUrl: schema.string({ defaultValue: DEFAULT_EMS_FILE_API_URL }),
emsTileApiUrl: schema.string({ defaultValue: DEFAULT_EMS_TILE_API_URL }),
diff --git a/vars/workers.groovy b/vars/workers.groovy
index b6ff5b27667dd..a1d569595ab4b 100644
--- a/vars/workers.groovy
+++ b/vars/workers.groovy
@@ -9,6 +9,8 @@ def label(size) {
return 'docker && linux && immutable'
case 's-highmem':
return 'docker && tests-s'
+ case 'm-highmem':
+ return 'docker && linux && immutable && gobld/machineType:n1-highmem-8'
case 'l':
return 'docker && tests-l'
case 'xl':
@@ -132,7 +134,7 @@ def ci(Map params, Closure closure) {
// Worker for running the current intake jobs. Just runs a single script after bootstrap.
def intake(jobName, String script) {
return {
- ci(name: jobName, size: 's-highmem', ramDisk: true) {
+ ci(name: jobName, size: 'm-highmem', ramDisk: true) {
withEnv(["JOB=${jobName}"]) {
kibanaPipeline.notifyOnError {
runbld(script, "Execute ${jobName}")
diff --git a/x-pack/plugins/actions/server/actions_client.test.ts b/x-pack/plugins/actions/server/actions_client.test.ts
index 171f8d4b0b1d4..8b6c25e1c3f24 100644
--- a/x-pack/plugins/actions/server/actions_client.test.ts
+++ b/x-pack/plugins/actions/server/actions_client.test.ts
@@ -15,6 +15,8 @@ import { actionsConfigMock } from './actions_config.mock';
import { getActionsConfigurationUtilities } from './actions_config';
import { licenseStateMock } from './lib/license_state.mock';
import { licensingMock } from '../../licensing/server/mocks';
+import { httpServerMock } from '../../../../src/core/server/mocks';
+import { auditServiceMock } from '../../security/server/audit/index.mock';
import {
elasticsearchServiceMock,
@@ -22,17 +24,23 @@ import {
} from '../../../../src/core/server/mocks';
import { actionExecutorMock } from './lib/action_executor.mock';
import uuid from 'uuid';
-import { KibanaRequest } from 'kibana/server';
import { ActionsAuthorization } from './authorization/actions_authorization';
import { actionsAuthorizationMock } from './authorization/actions_authorization.mock';
+jest.mock('../../../../src/core/server/saved_objects/service/lib/utils', () => ({
+ SavedObjectsUtils: {
+ generateId: () => 'mock-saved-object-id',
+ },
+}));
+
const defaultKibanaIndex = '.kibana';
const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
const scopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient();
const actionExecutor = actionExecutorMock.create();
const authorization = actionsAuthorizationMock.create();
const executionEnqueuer = jest.fn();
-const request = {} as KibanaRequest;
+const request = httpServerMock.createKibanaRequest();
+const auditLogger = auditServiceMock.create().asScoped(request);
const mockTaskManager = taskManagerMock.createSetup();
@@ -68,6 +76,7 @@ beforeEach(() => {
executionEnqueuer,
request,
authorization: (authorization as unknown) as ActionsAuthorization,
+ auditLogger,
});
});
@@ -142,6 +151,95 @@ describe('create()', () => {
});
});
+ describe('auditLogger', () => {
+ test('logs audit event when creating a connector', async () => {
+ const savedObjectCreateResult = {
+ id: '1',
+ type: 'action',
+ attributes: {
+ name: 'my name',
+ actionTypeId: 'my-action-type',
+ config: {},
+ },
+ references: [],
+ };
+ actionTypeRegistry.register({
+ id: savedObjectCreateResult.attributes.actionTypeId,
+ name: 'My action type',
+ minimumLicenseRequired: 'basic',
+ executor,
+ });
+ unsecuredSavedObjectsClient.create.mockResolvedValueOnce(savedObjectCreateResult);
+
+ await actionsClient.create({
+ action: {
+ ...savedObjectCreateResult.attributes,
+ secrets: {},
+ },
+ });
+
+ expect(auditLogger.log).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event: expect.objectContaining({
+ action: 'connector_create',
+ outcome: 'unknown',
+ }),
+ kibana: { saved_object: { id: 'mock-saved-object-id', type: 'action' } },
+ })
+ );
+ });
+
+ test('logs audit event when not authorised to create a connector', async () => {
+ const savedObjectCreateResult = {
+ id: '1',
+ type: 'action',
+ attributes: {
+ name: 'my name',
+ actionTypeId: 'my-action-type',
+ config: {},
+ },
+ references: [],
+ };
+ actionTypeRegistry.register({
+ id: savedObjectCreateResult.attributes.actionTypeId,
+ name: 'My action type',
+ minimumLicenseRequired: 'basic',
+ executor,
+ });
+ unsecuredSavedObjectsClient.create.mockResolvedValueOnce(savedObjectCreateResult);
+
+ authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized'));
+
+ await expect(
+ async () =>
+ await actionsClient.create({
+ action: {
+ ...savedObjectCreateResult.attributes,
+ secrets: {},
+ },
+ })
+ ).rejects.toThrow();
+ expect(auditLogger.log).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event: expect.objectContaining({
+ action: 'connector_create',
+ outcome: 'failure',
+ }),
+ kibana: {
+ saved_object: {
+ id: 'mock-saved-object-id',
+ type: 'action',
+ },
+ },
+ error: {
+ code: 'Error',
+ message: 'Unauthorized',
+ },
+ })
+ );
+ });
+ });
+
test('creates an action with all given properties', async () => {
const savedObjectCreateResult = {
id: '1',
@@ -185,6 +283,9 @@ describe('create()', () => {
"name": "my name",
"secrets": Object {},
},
+ Object {
+ "id": "mock-saved-object-id",
+ },
]
`);
});
@@ -289,6 +390,9 @@ describe('create()', () => {
"name": "my name",
"secrets": Object {},
},
+ Object {
+ "id": "mock-saved-object-id",
+ },
]
`);
});
@@ -440,7 +544,7 @@ describe('get()', () => {
expect(authorization.ensureAuthorized).toHaveBeenCalledWith('get');
});
- test('throws when user is not authorised to create the type of action', async () => {
+ test('throws when user is not authorised to get the type of action', async () => {
unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
id: '1',
type: 'type',
@@ -463,7 +567,7 @@ describe('get()', () => {
expect(authorization.ensureAuthorized).toHaveBeenCalledWith('get');
});
- test('throws when user is not authorised to create preconfigured of action', async () => {
+ test('throws when user is not authorised to get preconfigured of action', async () => {
actionsClient = new ActionsClient({
actionTypeRegistry,
unsecuredSavedObjectsClient,
@@ -501,6 +605,61 @@ describe('get()', () => {
});
});
+ describe('auditLogger', () => {
+ test('logs audit event when getting a connector', async () => {
+ unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
+ id: '1',
+ type: 'type',
+ attributes: {
+ name: 'my name',
+ actionTypeId: 'my-action-type',
+ config: {},
+ },
+ references: [],
+ });
+
+ await actionsClient.get({ id: '1' });
+
+ expect(auditLogger.log).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event: expect.objectContaining({
+ action: 'connector_get',
+ outcome: 'success',
+ }),
+ kibana: { saved_object: { id: '1', type: 'action' } },
+ })
+ );
+ });
+
+ test('logs audit event when not authorised to get a connector', async () => {
+ unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
+ id: '1',
+ type: 'type',
+ attributes: {
+ name: 'my name',
+ actionTypeId: 'my-action-type',
+ config: {},
+ },
+ references: [],
+ });
+
+ authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized'));
+
+ await expect(actionsClient.get({ id: '1' })).rejects.toThrow();
+
+ expect(auditLogger.log).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event: expect.objectContaining({
+ action: 'connector_get',
+ outcome: 'failure',
+ }),
+ kibana: { saved_object: { id: '1', type: 'action' } },
+ error: { code: 'Error', message: 'Unauthorized' },
+ })
+ );
+ });
+ });
+
test('calls unsecuredSavedObjectsClient with id', async () => {
unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
id: '1',
@@ -632,6 +791,64 @@ describe('getAll()', () => {
});
});
+ describe('auditLogger', () => {
+ test('logs audit event when searching connectors', async () => {
+ unsecuredSavedObjectsClient.find.mockResolvedValueOnce({
+ total: 1,
+ per_page: 10,
+ page: 1,
+ saved_objects: [
+ {
+ id: '1',
+ type: 'type',
+ attributes: {
+ name: 'test',
+ config: {
+ foo: 'bar',
+ },
+ },
+ score: 1,
+ references: [],
+ },
+ ],
+ });
+ scopedClusterClient.callAsInternalUser.mockResolvedValueOnce({
+ aggregations: {
+ '1': { doc_count: 6 },
+ testPreconfigured: { doc_count: 2 },
+ },
+ });
+
+ await actionsClient.getAll();
+
+ expect(auditLogger.log).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event: expect.objectContaining({
+ action: 'connector_find',
+ outcome: 'success',
+ }),
+ kibana: { saved_object: { id: '1', type: 'action' } },
+ })
+ );
+ });
+
+ test('logs audit event when not authorised to search connectors', async () => {
+ authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized'));
+
+ await expect(actionsClient.getAll()).rejects.toThrow();
+
+ expect(auditLogger.log).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event: expect.objectContaining({
+ action: 'connector_find',
+ outcome: 'failure',
+ }),
+ error: { code: 'Error', message: 'Unauthorized' },
+ })
+ );
+ });
+ });
+
test('calls unsecuredSavedObjectsClient with parameters', async () => {
const expectedResult = {
total: 1,
@@ -773,6 +990,62 @@ describe('getBulk()', () => {
});
});
+ describe('auditLogger', () => {
+ test('logs audit event when bulk getting connectors', async () => {
+ unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({
+ saved_objects: [
+ {
+ id: '1',
+ type: 'action',
+ attributes: {
+ actionTypeId: 'test',
+ name: 'test',
+ config: {
+ foo: 'bar',
+ },
+ },
+ references: [],
+ },
+ ],
+ });
+ scopedClusterClient.callAsInternalUser.mockResolvedValueOnce({
+ aggregations: {
+ '1': { doc_count: 6 },
+ testPreconfigured: { doc_count: 2 },
+ },
+ });
+
+ await actionsClient.getBulk(['1']);
+
+ expect(auditLogger.log).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event: expect.objectContaining({
+ action: 'connector_get',
+ outcome: 'success',
+ }),
+ kibana: { saved_object: { id: '1', type: 'action' } },
+ })
+ );
+ });
+
+ test('logs audit event when not authorised to bulk get connectors', async () => {
+ authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized'));
+
+ await expect(actionsClient.getBulk(['1'])).rejects.toThrow();
+
+ expect(auditLogger.log).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event: expect.objectContaining({
+ action: 'connector_get',
+ outcome: 'failure',
+ }),
+ kibana: { saved_object: { id: '1', type: 'action' } },
+ error: { code: 'Error', message: 'Unauthorized' },
+ })
+ );
+ });
+ });
+
test('calls getBulk unsecuredSavedObjectsClient with parameters', async () => {
unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({
saved_objects: [
@@ -864,6 +1137,39 @@ describe('delete()', () => {
});
});
+ describe('auditLogger', () => {
+ test('logs audit event when deleting a connector', async () => {
+ await actionsClient.delete({ id: '1' });
+
+ expect(auditLogger.log).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event: expect.objectContaining({
+ action: 'connector_delete',
+ outcome: 'unknown',
+ }),
+ kibana: { saved_object: { id: '1', type: 'action' } },
+ })
+ );
+ });
+
+ test('logs audit event when not authorised to delete a connector', async () => {
+ authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized'));
+
+ await expect(actionsClient.delete({ id: '1' })).rejects.toThrow();
+
+ expect(auditLogger.log).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event: expect.objectContaining({
+ action: 'connector_delete',
+ outcome: 'failure',
+ }),
+ kibana: { saved_object: { id: '1', type: 'action' } },
+ error: { code: 'Error', message: 'Unauthorized' },
+ })
+ );
+ });
+ });
+
test('calls unsecuredSavedObjectsClient with id', async () => {
const expectedResult = Symbol();
unsecuredSavedObjectsClient.delete.mockResolvedValueOnce(expectedResult);
@@ -880,42 +1186,43 @@ describe('delete()', () => {
});
describe('update()', () => {
+ function updateOperation(): ReturnType
{
+ actionTypeRegistry.register({
+ id: 'my-action-type',
+ name: 'My action type',
+ minimumLicenseRequired: 'basic',
+ executor,
+ });
+ unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
+ id: '1',
+ type: 'action',
+ attributes: {
+ actionTypeId: 'my-action-type',
+ },
+ references: [],
+ });
+ unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
+ id: 'my-action',
+ type: 'action',
+ attributes: {
+ actionTypeId: 'my-action-type',
+ name: 'my name',
+ config: {},
+ secrets: {},
+ },
+ references: [],
+ });
+ return actionsClient.update({
+ id: 'my-action',
+ action: {
+ name: 'my name',
+ config: {},
+ secrets: {},
+ },
+ });
+ }
+
describe('authorization', () => {
- function updateOperation(): ReturnType {
- actionTypeRegistry.register({
- id: 'my-action-type',
- name: 'My action type',
- minimumLicenseRequired: 'basic',
- executor,
- });
- unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
- id: '1',
- type: 'action',
- attributes: {
- actionTypeId: 'my-action-type',
- },
- references: [],
- });
- unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
- id: 'my-action',
- type: 'action',
- attributes: {
- actionTypeId: 'my-action-type',
- name: 'my name',
- config: {},
- secrets: {},
- },
- references: [],
- });
- return actionsClient.update({
- id: 'my-action',
- action: {
- name: 'my name',
- config: {},
- secrets: {},
- },
- });
- }
test('ensures user is authorised to update actions', async () => {
await updateOperation();
expect(authorization.ensureAuthorized).toHaveBeenCalledWith('update');
@@ -934,6 +1241,39 @@ describe('update()', () => {
});
});
+ describe('auditLogger', () => {
+ test('logs audit event when updating a connector', async () => {
+ await updateOperation();
+
+ expect(auditLogger.log).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event: expect.objectContaining({
+ action: 'connector_update',
+ outcome: 'unknown',
+ }),
+ kibana: { saved_object: { id: 'my-action', type: 'action' } },
+ })
+ );
+ });
+
+ test('logs audit event when not authorised to update a connector', async () => {
+ authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized'));
+
+ await expect(updateOperation()).rejects.toThrow();
+
+ expect(auditLogger.log).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event: expect.objectContaining({
+ action: 'connector_update',
+ outcome: 'failure',
+ }),
+ kibana: { saved_object: { id: 'my-action', type: 'action' } },
+ error: { code: 'Error', message: 'Unauthorized' },
+ })
+ );
+ });
+ });
+
test('updates an action with all given properties', async () => {
actionTypeRegistry.register({
id: 'my-action-type',
diff --git a/x-pack/plugins/actions/server/actions_client.ts b/x-pack/plugins/actions/server/actions_client.ts
index 0d41b520501ad..ab693dc340c92 100644
--- a/x-pack/plugins/actions/server/actions_client.ts
+++ b/x-pack/plugins/actions/server/actions_client.ts
@@ -4,16 +4,19 @@
* you may not use this file except in compliance with the Elastic License.
*/
import Boom from '@hapi/boom';
+
+import { i18n } from '@kbn/i18n';
+import { omitBy, isUndefined } from 'lodash';
import {
ILegacyScopedClusterClient,
SavedObjectsClientContract,
SavedObjectAttributes,
SavedObject,
KibanaRequest,
-} from 'src/core/server';
-
-import { i18n } from '@kbn/i18n';
-import { omitBy, isUndefined } from 'lodash';
+ SavedObjectsUtils,
+} from '../../../../src/core/server';
+import { AuditLogger, EventOutcome } from '../../security/server';
+import { ActionType } from '../common';
import { ActionTypeRegistry } from './action_type_registry';
import { validateConfig, validateSecrets, ActionExecutorContract } from './lib';
import {
@@ -30,11 +33,11 @@ import {
ExecuteOptions as EnqueueExecutionOptions,
} from './create_execute_function';
import { ActionsAuthorization } from './authorization/actions_authorization';
-import { ActionType } from '../common';
import {
getAuthorizationModeBySource,
AuthorizationMode,
} from './authorization/get_authorization_mode_by_source';
+import { connectorAuditEvent, ConnectorAuditAction } from './lib/audit_events';
// We are assuming there won't be many actions. This is why we will load
// all the actions in advance and assume the total count to not go over 10000.
@@ -65,6 +68,7 @@ interface ConstructorOptions {
executionEnqueuer: ExecutionEnqueuer;
request: KibanaRequest;
authorization: ActionsAuthorization;
+ auditLogger?: AuditLogger;
}
interface UpdateOptions {
@@ -82,6 +86,7 @@ export class ActionsClient {
private readonly request: KibanaRequest;
private readonly authorization: ActionsAuthorization;
private readonly executionEnqueuer: ExecutionEnqueuer;
+ private readonly auditLogger?: AuditLogger;
constructor({
actionTypeRegistry,
@@ -93,6 +98,7 @@ export class ActionsClient {
executionEnqueuer,
request,
authorization,
+ auditLogger,
}: ConstructorOptions) {
this.actionTypeRegistry = actionTypeRegistry;
this.unsecuredSavedObjectsClient = unsecuredSavedObjectsClient;
@@ -103,6 +109,7 @@ export class ActionsClient {
this.executionEnqueuer = executionEnqueuer;
this.request = request;
this.authorization = authorization;
+ this.auditLogger = auditLogger;
}
/**
@@ -111,7 +118,20 @@ export class ActionsClient {
public async create({
action: { actionTypeId, name, config, secrets },
}: CreateOptions): Promise {
- await this.authorization.ensureAuthorized('create', actionTypeId);
+ const id = SavedObjectsUtils.generateId();
+
+ try {
+ await this.authorization.ensureAuthorized('create', actionTypeId);
+ } catch (error) {
+ this.auditLogger?.log(
+ connectorAuditEvent({
+ action: ConnectorAuditAction.CREATE,
+ savedObject: { type: 'action', id },
+ error,
+ })
+ );
+ throw error;
+ }
const actionType = this.actionTypeRegistry.get(actionTypeId);
const validatedActionTypeConfig = validateConfig(actionType, config);
@@ -119,12 +139,24 @@ export class ActionsClient {
this.actionTypeRegistry.ensureActionTypeEnabled(actionTypeId);
- const result = await this.unsecuredSavedObjectsClient.create('action', {
- actionTypeId,
- name,
- config: validatedActionTypeConfig as SavedObjectAttributes,
- secrets: validatedActionTypeSecrets as SavedObjectAttributes,
- });
+ this.auditLogger?.log(
+ connectorAuditEvent({
+ action: ConnectorAuditAction.CREATE,
+ savedObject: { type: 'action', id },
+ outcome: EventOutcome.UNKNOWN,
+ })
+ );
+
+ const result = await this.unsecuredSavedObjectsClient.create(
+ 'action',
+ {
+ actionTypeId,
+ name,
+ config: validatedActionTypeConfig as SavedObjectAttributes,
+ secrets: validatedActionTypeSecrets as SavedObjectAttributes,
+ },
+ { id }
+ );
return {
id: result.id,
@@ -139,21 +171,32 @@ export class ActionsClient {
* Update action
*/
public async update({ id, action }: UpdateOptions): Promise {
- await this.authorization.ensureAuthorized('update');
-
- if (
- this.preconfiguredActions.find((preconfiguredAction) => preconfiguredAction.id === id) !==
- undefined
- ) {
- throw new PreconfiguredActionDisabledModificationError(
- i18n.translate('xpack.actions.serverSideErrors.predefinedActionUpdateDisabled', {
- defaultMessage: 'Preconfigured action {id} is not allowed to update.',
- values: {
- id,
- },
- }),
- 'update'
+ try {
+ await this.authorization.ensureAuthorized('update');
+
+ if (
+ this.preconfiguredActions.find((preconfiguredAction) => preconfiguredAction.id === id) !==
+ undefined
+ ) {
+ throw new PreconfiguredActionDisabledModificationError(
+ i18n.translate('xpack.actions.serverSideErrors.predefinedActionUpdateDisabled', {
+ defaultMessage: 'Preconfigured action {id} is not allowed to update.',
+ values: {
+ id,
+ },
+ }),
+ 'update'
+ );
+ }
+ } catch (error) {
+ this.auditLogger?.log(
+ connectorAuditEvent({
+ action: ConnectorAuditAction.UPDATE,
+ savedObject: { type: 'action', id },
+ error,
+ })
);
+ throw error;
}
const {
attributes,
@@ -168,6 +211,14 @@ export class ActionsClient {
this.actionTypeRegistry.ensureActionTypeEnabled(actionTypeId);
+ this.auditLogger?.log(
+ connectorAuditEvent({
+ action: ConnectorAuditAction.UPDATE,
+ savedObject: { type: 'action', id },
+ outcome: EventOutcome.UNKNOWN,
+ })
+ );
+
const result = await this.unsecuredSavedObjectsClient.create(
'action',
{
@@ -201,12 +252,30 @@ export class ActionsClient {
* Get an action
*/
public async get({ id }: { id: string }): Promise {
- await this.authorization.ensureAuthorized('get');
+ try {
+ await this.authorization.ensureAuthorized('get');
+ } catch (error) {
+ this.auditLogger?.log(
+ connectorAuditEvent({
+ action: ConnectorAuditAction.GET,
+ savedObject: { type: 'action', id },
+ error,
+ })
+ );
+ throw error;
+ }
const preconfiguredActionsList = this.preconfiguredActions.find(
(preconfiguredAction) => preconfiguredAction.id === id
);
if (preconfiguredActionsList !== undefined) {
+ this.auditLogger?.log(
+ connectorAuditEvent({
+ action: ConnectorAuditAction.GET,
+ savedObject: { type: 'action', id },
+ })
+ );
+
return {
id,
actionTypeId: preconfiguredActionsList.actionTypeId,
@@ -214,8 +283,16 @@ export class ActionsClient {
isPreconfigured: true,
};
}
+
const result = await this.unsecuredSavedObjectsClient.get('action', id);
+ this.auditLogger?.log(
+ connectorAuditEvent({
+ action: ConnectorAuditAction.GET,
+ savedObject: { type: 'action', id },
+ })
+ );
+
return {
id,
actionTypeId: result.attributes.actionTypeId,
@@ -229,7 +306,17 @@ export class ActionsClient {
* Get all actions with preconfigured list
*/
public async getAll(): Promise {
- await this.authorization.ensureAuthorized('get');
+ try {
+ await this.authorization.ensureAuthorized('get');
+ } catch (error) {
+ this.auditLogger?.log(
+ connectorAuditEvent({
+ action: ConnectorAuditAction.FIND,
+ error,
+ })
+ );
+ throw error;
+ }
const savedObjectsActions = (
await this.unsecuredSavedObjectsClient.find({
@@ -238,6 +325,15 @@ export class ActionsClient {
})
).saved_objects.map(actionFromSavedObject);
+ savedObjectsActions.forEach(({ id }) =>
+ this.auditLogger?.log(
+ connectorAuditEvent({
+ action: ConnectorAuditAction.FIND,
+ savedObject: { type: 'action', id },
+ })
+ )
+ );
+
const mergedResult = [
...savedObjectsActions,
...this.preconfiguredActions.map((preconfiguredAction) => ({
@@ -258,7 +354,20 @@ export class ActionsClient {
* Get bulk actions with preconfigured list
*/
public async getBulk(ids: string[]): Promise {
- await this.authorization.ensureAuthorized('get');
+ try {
+ await this.authorization.ensureAuthorized('get');
+ } catch (error) {
+ ids.forEach((id) =>
+ this.auditLogger?.log(
+ connectorAuditEvent({
+ action: ConnectorAuditAction.GET,
+ savedObject: { type: 'action', id },
+ error,
+ })
+ )
+ );
+ throw error;
+ }
const actionResults = new Array();
for (const actionId of ids) {
@@ -283,6 +392,17 @@ export class ActionsClient {
const bulkGetOpts = actionSavedObjectsIds.map((id) => ({ id, type: 'action' }));
const bulkGetResult = await this.unsecuredSavedObjectsClient.bulkGet(bulkGetOpts);
+ bulkGetResult.saved_objects.forEach(({ id, error }) => {
+ if (!error && this.auditLogger) {
+ this.auditLogger.log(
+ connectorAuditEvent({
+ action: ConnectorAuditAction.GET,
+ savedObject: { type: 'action', id },
+ })
+ );
+ }
+ });
+
for (const action of bulkGetResult.saved_objects) {
if (action.error) {
throw Boom.badRequest(
@@ -298,22 +418,42 @@ export class ActionsClient {
* Delete action
*/
public async delete({ id }: { id: string }) {
- await this.authorization.ensureAuthorized('delete');
-
- if (
- this.preconfiguredActions.find((preconfiguredAction) => preconfiguredAction.id === id) !==
- undefined
- ) {
- throw new PreconfiguredActionDisabledModificationError(
- i18n.translate('xpack.actions.serverSideErrors.predefinedActionDeleteDisabled', {
- defaultMessage: 'Preconfigured action {id} is not allowed to delete.',
- values: {
- id,
- },
- }),
- 'delete'
+ try {
+ await this.authorization.ensureAuthorized('delete');
+
+ if (
+ this.preconfiguredActions.find((preconfiguredAction) => preconfiguredAction.id === id) !==
+ undefined
+ ) {
+ throw new PreconfiguredActionDisabledModificationError(
+ i18n.translate('xpack.actions.serverSideErrors.predefinedActionDeleteDisabled', {
+ defaultMessage: 'Preconfigured action {id} is not allowed to delete.',
+ values: {
+ id,
+ },
+ }),
+ 'delete'
+ );
+ }
+ } catch (error) {
+ this.auditLogger?.log(
+ connectorAuditEvent({
+ action: ConnectorAuditAction.DELETE,
+ savedObject: { type: 'action', id },
+ error,
+ })
);
+ throw error;
}
+
+ this.auditLogger?.log(
+ connectorAuditEvent({
+ action: ConnectorAuditAction.DELETE,
+ outcome: EventOutcome.UNKNOWN,
+ savedObject: { type: 'action', id },
+ })
+ );
+
return await this.unsecuredSavedObjectsClient.delete('action', id);
}
diff --git a/x-pack/plugins/actions/server/lib/audit_events.test.ts b/x-pack/plugins/actions/server/lib/audit_events.test.ts
new file mode 100644
index 0000000000000..6c2fd99c2eafd
--- /dev/null
+++ b/x-pack/plugins/actions/server/lib/audit_events.test.ts
@@ -0,0 +1,93 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { EventOutcome } from '../../../security/server/audit';
+import { ConnectorAuditAction, connectorAuditEvent } from './audit_events';
+
+describe('#connectorAuditEvent', () => {
+ test('creates event with `unknown` outcome', () => {
+ expect(
+ connectorAuditEvent({
+ action: ConnectorAuditAction.CREATE,
+ outcome: EventOutcome.UNKNOWN,
+ savedObject: { type: 'action', id: 'ACTION_ID' },
+ })
+ ).toMatchInlineSnapshot(`
+ Object {
+ "error": undefined,
+ "event": Object {
+ "action": "connector_create",
+ "category": "database",
+ "outcome": "unknown",
+ "type": "creation",
+ },
+ "kibana": Object {
+ "saved_object": Object {
+ "id": "ACTION_ID",
+ "type": "action",
+ },
+ },
+ "message": "User is creating connector [id=ACTION_ID]",
+ }
+ `);
+ });
+
+ test('creates event with `success` outcome', () => {
+ expect(
+ connectorAuditEvent({
+ action: ConnectorAuditAction.CREATE,
+ savedObject: { type: 'action', id: 'ACTION_ID' },
+ })
+ ).toMatchInlineSnapshot(`
+ Object {
+ "error": undefined,
+ "event": Object {
+ "action": "connector_create",
+ "category": "database",
+ "outcome": "success",
+ "type": "creation",
+ },
+ "kibana": Object {
+ "saved_object": Object {
+ "id": "ACTION_ID",
+ "type": "action",
+ },
+ },
+ "message": "User has created connector [id=ACTION_ID]",
+ }
+ `);
+ });
+
+ test('creates event with `failure` outcome', () => {
+ expect(
+ connectorAuditEvent({
+ action: ConnectorAuditAction.CREATE,
+ savedObject: { type: 'action', id: 'ACTION_ID' },
+ error: new Error('ERROR_MESSAGE'),
+ })
+ ).toMatchInlineSnapshot(`
+ Object {
+ "error": Object {
+ "code": "Error",
+ "message": "ERROR_MESSAGE",
+ },
+ "event": Object {
+ "action": "connector_create",
+ "category": "database",
+ "outcome": "failure",
+ "type": "creation",
+ },
+ "kibana": Object {
+ "saved_object": Object {
+ "id": "ACTION_ID",
+ "type": "action",
+ },
+ },
+ "message": "Failed attempt to create connector [id=ACTION_ID]",
+ }
+ `);
+ });
+});
diff --git a/x-pack/plugins/actions/server/lib/audit_events.ts b/x-pack/plugins/actions/server/lib/audit_events.ts
new file mode 100644
index 0000000000000..7d25b5c0cd479
--- /dev/null
+++ b/x-pack/plugins/actions/server/lib/audit_events.ts
@@ -0,0 +1,76 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { AuditEvent, EventOutcome, EventCategory, EventType } from '../../../security/server';
+
+export enum ConnectorAuditAction {
+ CREATE = 'connector_create',
+ GET = 'connector_get',
+ UPDATE = 'connector_update',
+ DELETE = 'connector_delete',
+ FIND = 'connector_find',
+ EXECUTE = 'connector_execute',
+}
+
+type VerbsTuple = [string, string, string];
+
+const eventVerbs: Record = {
+ connector_create: ['create', 'creating', 'created'],
+ connector_get: ['access', 'accessing', 'accessed'],
+ connector_update: ['update', 'updating', 'updated'],
+ connector_delete: ['delete', 'deleting', 'deleted'],
+ connector_find: ['access', 'accessing', 'accessed'],
+ connector_execute: ['execute', 'executing', 'executed'],
+};
+
+const eventTypes: Record = {
+ connector_create: EventType.CREATION,
+ connector_get: EventType.ACCESS,
+ connector_update: EventType.CHANGE,
+ connector_delete: EventType.DELETION,
+ connector_find: EventType.ACCESS,
+ connector_execute: undefined,
+};
+
+export interface ConnectorAuditEventParams {
+ action: ConnectorAuditAction;
+ outcome?: EventOutcome;
+ savedObject?: NonNullable['saved_object'];
+ error?: Error;
+}
+
+export function connectorAuditEvent({
+ action,
+ savedObject,
+ outcome,
+ error,
+}: ConnectorAuditEventParams): AuditEvent {
+ const doc = savedObject ? `connector [id=${savedObject.id}]` : 'a connector';
+ const [present, progressive, past] = eventVerbs[action];
+ const message = error
+ ? `Failed attempt to ${present} ${doc}`
+ : outcome === EventOutcome.UNKNOWN
+ ? `User is ${progressive} ${doc}`
+ : `User has ${past} ${doc}`;
+ const type = eventTypes[action];
+
+ return {
+ message,
+ event: {
+ action,
+ category: EventCategory.DATABASE,
+ type,
+ outcome: outcome ?? (error ? EventOutcome.FAILURE : EventOutcome.SUCCESS),
+ },
+ kibana: {
+ saved_object: savedObject,
+ },
+ error: error && {
+ code: error.name,
+ message: error.message,
+ },
+ };
+}
diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts
index e61936321b8e0..6e37d4bd7a92a 100644
--- a/x-pack/plugins/actions/server/plugin.ts
+++ b/x-pack/plugins/actions/server/plugin.ts
@@ -314,6 +314,7 @@ export class ActionsPlugin implements Plugin, Plugi
isESOUsingEphemeralEncryptionKey: isESOUsingEphemeralEncryptionKey!,
preconfiguredActions,
}),
+ auditLogger: this.security?.audit.asScoped(request),
});
};
@@ -439,6 +440,7 @@ export class ActionsPlugin implements Plugin, Plugi
preconfiguredActions,
actionExecutor,
instantiateAuthorization,
+ security,
} = this;
return async function actionsRouteHandlerContext(context, request) {
@@ -468,6 +470,7 @@ export class ActionsPlugin implements Plugin, Plugi
isESOUsingEphemeralEncryptionKey: isESOUsingEphemeralEncryptionKey!,
preconfiguredActions,
}),
+ auditLogger: security?.audit.asScoped(request),
});
},
listTypes: actionTypeRegistry!.list.bind(actionTypeRegistry!),
diff --git a/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts b/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts
index c83e24c5a45f4..d697817be734b 100644
--- a/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts
+++ b/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts
@@ -13,7 +13,8 @@ import {
SavedObjectReference,
SavedObject,
PluginInitializerContext,
-} from 'src/core/server';
+ SavedObjectsUtils,
+} from '../../../../../src/core/server';
import { esKuery } from '../../../../../src/plugins/data/server';
import { ActionsClient, ActionsAuthorization } from '../../../actions/server';
import {
@@ -44,10 +45,12 @@ import { IEventLogClient } from '../../../../plugins/event_log/server';
import { parseIsoOrRelativeDate } from '../lib/iso_or_relative_date';
import { alertInstanceSummaryFromEventLog } from '../lib/alert_instance_summary_from_event_log';
import { IEvent } from '../../../event_log/server';
+import { AuditLogger, EventOutcome } from '../../../security/server';
import { parseDuration } from '../../common/parse_duration';
import { retryIfConflicts } from '../lib/retry_if_conflicts';
import { partiallyUpdateAlert } from '../saved_objects';
import { markApiKeyForInvalidation } from '../invalidate_pending_api_keys/mark_api_key_for_invalidation';
+import { alertAuditEvent, AlertAuditAction } from './audit_events';
export interface RegistryAlertTypeWithAuth extends RegistryAlertType {
authorizedConsumers: string[];
@@ -75,6 +78,7 @@ export interface ConstructorOptions {
getActionsClient: () => Promise;
getEventLogClient: () => Promise;
kibanaVersion: PluginInitializerContext['env']['packageInfo']['version'];
+ auditLogger?: AuditLogger;
}
export interface MuteOptions extends IndexType {
@@ -176,6 +180,7 @@ export class AlertsClient {
private readonly getEventLogClient: () => Promise;
private readonly encryptedSavedObjectsClient: EncryptedSavedObjectsClient;
private readonly kibanaVersion!: PluginInitializerContext['env']['packageInfo']['version'];
+ private readonly auditLogger?: AuditLogger;
constructor({
alertTypeRegistry,
@@ -192,6 +197,7 @@ export class AlertsClient {
actionsAuthorization,
getEventLogClient,
kibanaVersion,
+ auditLogger,
}: ConstructorOptions) {
this.logger = logger;
this.getUserName = getUserName;
@@ -207,14 +213,28 @@ export class AlertsClient {
this.actionsAuthorization = actionsAuthorization;
this.getEventLogClient = getEventLogClient;
this.kibanaVersion = kibanaVersion;
+ this.auditLogger = auditLogger;
}
public async create({ data, options }: CreateOptions): Promise {
- await this.authorization.ensureAuthorized(
- data.alertTypeId,
- data.consumer,
- WriteOperations.Create
- );
+ const id = SavedObjectsUtils.generateId();
+
+ try {
+ await this.authorization.ensureAuthorized(
+ data.alertTypeId,
+ data.consumer,
+ WriteOperations.Create
+ );
+ } catch (error) {
+ this.auditLogger?.log(
+ alertAuditEvent({
+ action: AlertAuditAction.CREATE,
+ savedObject: { type: 'alert', id },
+ error,
+ })
+ );
+ throw error;
+ }
// Throws an error if alert type isn't registered
const alertType = this.alertTypeRegistry.get(data.alertTypeId);
@@ -248,6 +268,15 @@ export class AlertsClient {
error: null,
},
};
+
+ this.auditLogger?.log(
+ alertAuditEvent({
+ action: AlertAuditAction.CREATE,
+ outcome: EventOutcome.UNKNOWN,
+ savedObject: { type: 'alert', id },
+ })
+ );
+
let createdAlert: SavedObject;
try {
createdAlert = await this.unsecuredSavedObjectsClient.create(
@@ -256,6 +285,7 @@ export class AlertsClient {
{
...options,
references,
+ id,
}
);
} catch (e) {
@@ -297,10 +327,27 @@ export class AlertsClient {
public async get({ id }: { id: string }): Promise {
const result = await this.unsecuredSavedObjectsClient.get('alert', id);
- await this.authorization.ensureAuthorized(
- result.attributes.alertTypeId,
- result.attributes.consumer,
- ReadOperations.Get
+ try {
+ await this.authorization.ensureAuthorized(
+ result.attributes.alertTypeId,
+ result.attributes.consumer,
+ ReadOperations.Get
+ );
+ } catch (error) {
+ this.auditLogger?.log(
+ alertAuditEvent({
+ action: AlertAuditAction.GET,
+ savedObject: { type: 'alert', id },
+ error,
+ })
+ );
+ throw error;
+ }
+ this.auditLogger?.log(
+ alertAuditEvent({
+ action: AlertAuditAction.GET,
+ savedObject: { type: 'alert', id },
+ })
);
return this.getAlertFromRaw(result.id, result.attributes, result.references);
}
@@ -370,11 +417,23 @@ export class AlertsClient {
public async find({
options: { fields, ...options } = {},
}: { options?: FindOptions } = {}): Promise {
+ let authorizationTuple;
+ try {
+ authorizationTuple = await this.authorization.getFindAuthorizationFilter();
+ } catch (error) {
+ this.auditLogger?.log(
+ alertAuditEvent({
+ action: AlertAuditAction.FIND,
+ error,
+ })
+ );
+ throw error;
+ }
const {
filter: authorizationFilter,
ensureAlertTypeIsAuthorized,
logSuccessfulAuthorization,
- } = await this.authorization.getFindAuthorizationFilter();
+ } = authorizationTuple;
const {
page,
@@ -392,7 +451,18 @@ export class AlertsClient {
});
const authorizedData = data.map(({ id, attributes, references }) => {
- ensureAlertTypeIsAuthorized(attributes.alertTypeId, attributes.consumer);
+ try {
+ ensureAlertTypeIsAuthorized(attributes.alertTypeId, attributes.consumer);
+ } catch (error) {
+ this.auditLogger?.log(
+ alertAuditEvent({
+ action: AlertAuditAction.FIND,
+ savedObject: { type: 'alert', id },
+ error,
+ })
+ );
+ throw error;
+ }
return this.getAlertFromRaw(
id,
fields ? (pick(attributes, fields) as RawAlert) : attributes,
@@ -400,6 +470,15 @@ export class AlertsClient {
);
});
+ authorizedData.forEach(({ id }) =>
+ this.auditLogger?.log(
+ alertAuditEvent({
+ action: AlertAuditAction.FIND,
+ savedObject: { type: 'alert', id },
+ })
+ )
+ );
+
logSuccessfulAuthorization();
return {
@@ -473,10 +552,29 @@ export class AlertsClient {
attributes = alert.attributes;
}
- await this.authorization.ensureAuthorized(
- attributes.alertTypeId,
- attributes.consumer,
- WriteOperations.Delete
+ try {
+ await this.authorization.ensureAuthorized(
+ attributes.alertTypeId,
+ attributes.consumer,
+ WriteOperations.Delete
+ );
+ } catch (error) {
+ this.auditLogger?.log(
+ alertAuditEvent({
+ action: AlertAuditAction.DELETE,
+ savedObject: { type: 'alert', id },
+ error,
+ })
+ );
+ throw error;
+ }
+
+ this.auditLogger?.log(
+ alertAuditEvent({
+ action: AlertAuditAction.DELETE,
+ outcome: EventOutcome.UNKNOWN,
+ savedObject: { type: 'alert', id },
+ })
);
const removeResult = await this.unsecuredSavedObjectsClient.delete('alert', id);
@@ -520,10 +618,30 @@ export class AlertsClient {
// Still attempt to load the object using SOC
alertSavedObject = await this.unsecuredSavedObjectsClient.get('alert', id);
}
- await this.authorization.ensureAuthorized(
- alertSavedObject.attributes.alertTypeId,
- alertSavedObject.attributes.consumer,
- WriteOperations.Update
+
+ try {
+ await this.authorization.ensureAuthorized(
+ alertSavedObject.attributes.alertTypeId,
+ alertSavedObject.attributes.consumer,
+ WriteOperations.Update
+ );
+ } catch (error) {
+ this.auditLogger?.log(
+ alertAuditEvent({
+ action: AlertAuditAction.UPDATE,
+ savedObject: { type: 'alert', id },
+ error,
+ })
+ );
+ throw error;
+ }
+
+ this.auditLogger?.log(
+ alertAuditEvent({
+ action: AlertAuditAction.UPDATE,
+ outcome: EventOutcome.UNKNOWN,
+ savedObject: { type: 'alert', id },
+ })
);
const updateResult = await this.updateAlert({ id, data }, alertSavedObject);
@@ -658,14 +776,28 @@ export class AlertsClient {
attributes = alert.attributes;
version = alert.version;
}
- await this.authorization.ensureAuthorized(
- attributes.alertTypeId,
- attributes.consumer,
- WriteOperations.UpdateApiKey
- );
- if (attributes.actions.length && !this.authorization.shouldUseLegacyAuthorization(attributes)) {
- await this.actionsAuthorization.ensureAuthorized('execute');
+ try {
+ await this.authorization.ensureAuthorized(
+ attributes.alertTypeId,
+ attributes.consumer,
+ WriteOperations.UpdateApiKey
+ );
+ if (
+ attributes.actions.length &&
+ !this.authorization.shouldUseLegacyAuthorization(attributes)
+ ) {
+ await this.actionsAuthorization.ensureAuthorized('execute');
+ }
+ } catch (error) {
+ this.auditLogger?.log(
+ alertAuditEvent({
+ action: AlertAuditAction.UPDATE_API_KEY,
+ savedObject: { type: 'alert', id },
+ error,
+ })
+ );
+ throw error;
}
const username = await this.getUserName();
@@ -678,6 +810,15 @@ export class AlertsClient {
updatedAt: new Date().toISOString(),
updatedBy: username,
});
+
+ this.auditLogger?.log(
+ alertAuditEvent({
+ action: AlertAuditAction.UPDATE_API_KEY,
+ outcome: EventOutcome.UNKNOWN,
+ savedObject: { type: 'alert', id },
+ })
+ );
+
try {
await this.unsecuredSavedObjectsClient.update('alert', id, updateAttributes, { version });
} catch (e) {
@@ -732,16 +873,35 @@ export class AlertsClient {
version = alert.version;
}
- await this.authorization.ensureAuthorized(
- attributes.alertTypeId,
- attributes.consumer,
- WriteOperations.Enable
- );
+ try {
+ await this.authorization.ensureAuthorized(
+ attributes.alertTypeId,
+ attributes.consumer,
+ WriteOperations.Enable
+ );
- if (attributes.actions.length) {
- await this.actionsAuthorization.ensureAuthorized('execute');
+ if (attributes.actions.length) {
+ await this.actionsAuthorization.ensureAuthorized('execute');
+ }
+ } catch (error) {
+ this.auditLogger?.log(
+ alertAuditEvent({
+ action: AlertAuditAction.ENABLE,
+ savedObject: { type: 'alert', id },
+ error,
+ })
+ );
+ throw error;
}
+ this.auditLogger?.log(
+ alertAuditEvent({
+ action: AlertAuditAction.ENABLE,
+ outcome: EventOutcome.UNKNOWN,
+ savedObject: { type: 'alert', id },
+ })
+ );
+
if (attributes.enabled === false) {
const username = await this.getUserName();
const updateAttributes = this.updateMeta({
@@ -816,10 +976,29 @@ export class AlertsClient {
version = alert.version;
}
- await this.authorization.ensureAuthorized(
- attributes.alertTypeId,
- attributes.consumer,
- WriteOperations.Disable
+ try {
+ await this.authorization.ensureAuthorized(
+ attributes.alertTypeId,
+ attributes.consumer,
+ WriteOperations.Disable
+ );
+ } catch (error) {
+ this.auditLogger?.log(
+ alertAuditEvent({
+ action: AlertAuditAction.DISABLE,
+ savedObject: { type: 'alert', id },
+ error,
+ })
+ );
+ throw error;
+ }
+
+ this.auditLogger?.log(
+ alertAuditEvent({
+ action: AlertAuditAction.DISABLE,
+ outcome: EventOutcome.UNKNOWN,
+ savedObject: { type: 'alert', id },
+ })
);
if (attributes.enabled === true) {
@@ -866,16 +1045,36 @@ export class AlertsClient {
'alert',
id
);
- await this.authorization.ensureAuthorized(
- attributes.alertTypeId,
- attributes.consumer,
- WriteOperations.MuteAll
- );
- if (attributes.actions.length) {
- await this.actionsAuthorization.ensureAuthorized('execute');
+ try {
+ await this.authorization.ensureAuthorized(
+ attributes.alertTypeId,
+ attributes.consumer,
+ WriteOperations.MuteAll
+ );
+
+ if (attributes.actions.length) {
+ await this.actionsAuthorization.ensureAuthorized('execute');
+ }
+ } catch (error) {
+ this.auditLogger?.log(
+ alertAuditEvent({
+ action: AlertAuditAction.MUTE,
+ savedObject: { type: 'alert', id },
+ error,
+ })
+ );
+ throw error;
}
+ this.auditLogger?.log(
+ alertAuditEvent({
+ action: AlertAuditAction.MUTE,
+ outcome: EventOutcome.UNKNOWN,
+ savedObject: { type: 'alert', id },
+ })
+ );
+
const updateAttributes = this.updateMeta({
muteAll: true,
mutedInstanceIds: [],
@@ -905,16 +1104,36 @@ export class AlertsClient {
'alert',
id
);
- await this.authorization.ensureAuthorized(
- attributes.alertTypeId,
- attributes.consumer,
- WriteOperations.UnmuteAll
- );
- if (attributes.actions.length) {
- await this.actionsAuthorization.ensureAuthorized('execute');
+ try {
+ await this.authorization.ensureAuthorized(
+ attributes.alertTypeId,
+ attributes.consumer,
+ WriteOperations.UnmuteAll
+ );
+
+ if (attributes.actions.length) {
+ await this.actionsAuthorization.ensureAuthorized('execute');
+ }
+ } catch (error) {
+ this.auditLogger?.log(
+ alertAuditEvent({
+ action: AlertAuditAction.UNMUTE,
+ savedObject: { type: 'alert', id },
+ error,
+ })
+ );
+ throw error;
}
+ this.auditLogger?.log(
+ alertAuditEvent({
+ action: AlertAuditAction.UNMUTE,
+ outcome: EventOutcome.UNKNOWN,
+ savedObject: { type: 'alert', id },
+ })
+ );
+
const updateAttributes = this.updateMeta({
muteAll: false,
mutedInstanceIds: [],
@@ -945,16 +1164,35 @@ export class AlertsClient {
alertId
);
- await this.authorization.ensureAuthorized(
- attributes.alertTypeId,
- attributes.consumer,
- WriteOperations.MuteInstance
- );
+ try {
+ await this.authorization.ensureAuthorized(
+ attributes.alertTypeId,
+ attributes.consumer,
+ WriteOperations.MuteInstance
+ );
- if (attributes.actions.length) {
- await this.actionsAuthorization.ensureAuthorized('execute');
+ if (attributes.actions.length) {
+ await this.actionsAuthorization.ensureAuthorized('execute');
+ }
+ } catch (error) {
+ this.auditLogger?.log(
+ alertAuditEvent({
+ action: AlertAuditAction.MUTE_INSTANCE,
+ savedObject: { type: 'alert', id: alertId },
+ error,
+ })
+ );
+ throw error;
}
+ this.auditLogger?.log(
+ alertAuditEvent({
+ action: AlertAuditAction.MUTE_INSTANCE,
+ outcome: EventOutcome.UNKNOWN,
+ savedObject: { type: 'alert', id: alertId },
+ })
+ );
+
const mutedInstanceIds = attributes.mutedInstanceIds || [];
if (!attributes.muteAll && !mutedInstanceIds.includes(alertInstanceId)) {
mutedInstanceIds.push(alertInstanceId);
@@ -991,15 +1229,34 @@ export class AlertsClient {
alertId
);
- await this.authorization.ensureAuthorized(
- attributes.alertTypeId,
- attributes.consumer,
- WriteOperations.UnmuteInstance
- );
- if (attributes.actions.length) {
- await this.actionsAuthorization.ensureAuthorized('execute');
+ try {
+ await this.authorization.ensureAuthorized(
+ attributes.alertTypeId,
+ attributes.consumer,
+ WriteOperations.UnmuteInstance
+ );
+ if (attributes.actions.length) {
+ await this.actionsAuthorization.ensureAuthorized('execute');
+ }
+ } catch (error) {
+ this.auditLogger?.log(
+ alertAuditEvent({
+ action: AlertAuditAction.UNMUTE_INSTANCE,
+ savedObject: { type: 'alert', id: alertId },
+ error,
+ })
+ );
+ throw error;
}
+ this.auditLogger?.log(
+ alertAuditEvent({
+ action: AlertAuditAction.UNMUTE_INSTANCE,
+ outcome: EventOutcome.UNKNOWN,
+ savedObject: { type: 'alert', id: alertId },
+ })
+ );
+
const mutedInstanceIds = attributes.mutedInstanceIds || [];
if (!attributes.muteAll && mutedInstanceIds.includes(alertInstanceId)) {
await this.unsecuredSavedObjectsClient.update(
diff --git a/x-pack/plugins/alerts/server/alerts_client/audit_events.test.ts b/x-pack/plugins/alerts/server/alerts_client/audit_events.test.ts
new file mode 100644
index 0000000000000..9cd48248320c0
--- /dev/null
+++ b/x-pack/plugins/alerts/server/alerts_client/audit_events.test.ts
@@ -0,0 +1,93 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { EventOutcome } from '../../../security/server/audit';
+import { AlertAuditAction, alertAuditEvent } from './audit_events';
+
+describe('#alertAuditEvent', () => {
+ test('creates event with `unknown` outcome', () => {
+ expect(
+ alertAuditEvent({
+ action: AlertAuditAction.CREATE,
+ outcome: EventOutcome.UNKNOWN,
+ savedObject: { type: 'alert', id: 'ALERT_ID' },
+ })
+ ).toMatchInlineSnapshot(`
+ Object {
+ "error": undefined,
+ "event": Object {
+ "action": "alert_create",
+ "category": "database",
+ "outcome": "unknown",
+ "type": "creation",
+ },
+ "kibana": Object {
+ "saved_object": Object {
+ "id": "ALERT_ID",
+ "type": "alert",
+ },
+ },
+ "message": "User is creating alert [id=ALERT_ID]",
+ }
+ `);
+ });
+
+ test('creates event with `success` outcome', () => {
+ expect(
+ alertAuditEvent({
+ action: AlertAuditAction.CREATE,
+ savedObject: { type: 'alert', id: 'ALERT_ID' },
+ })
+ ).toMatchInlineSnapshot(`
+ Object {
+ "error": undefined,
+ "event": Object {
+ "action": "alert_create",
+ "category": "database",
+ "outcome": "success",
+ "type": "creation",
+ },
+ "kibana": Object {
+ "saved_object": Object {
+ "id": "ALERT_ID",
+ "type": "alert",
+ },
+ },
+ "message": "User has created alert [id=ALERT_ID]",
+ }
+ `);
+ });
+
+ test('creates event with `failure` outcome', () => {
+ expect(
+ alertAuditEvent({
+ action: AlertAuditAction.CREATE,
+ savedObject: { type: 'alert', id: 'ALERT_ID' },
+ error: new Error('ERROR_MESSAGE'),
+ })
+ ).toMatchInlineSnapshot(`
+ Object {
+ "error": Object {
+ "code": "Error",
+ "message": "ERROR_MESSAGE",
+ },
+ "event": Object {
+ "action": "alert_create",
+ "category": "database",
+ "outcome": "failure",
+ "type": "creation",
+ },
+ "kibana": Object {
+ "saved_object": Object {
+ "id": "ALERT_ID",
+ "type": "alert",
+ },
+ },
+ "message": "Failed attempt to create alert [id=ALERT_ID]",
+ }
+ `);
+ });
+});
diff --git a/x-pack/plugins/alerts/server/alerts_client/audit_events.ts b/x-pack/plugins/alerts/server/alerts_client/audit_events.ts
new file mode 100644
index 0000000000000..f3e3959824084
--- /dev/null
+++ b/x-pack/plugins/alerts/server/alerts_client/audit_events.ts
@@ -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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { AuditEvent, EventOutcome, EventCategory, EventType } from '../../../security/server';
+
+export enum AlertAuditAction {
+ CREATE = 'alert_create',
+ GET = 'alert_get',
+ UPDATE = 'alert_update',
+ UPDATE_API_KEY = 'alert_update_api_key',
+ ENABLE = 'alert_enable',
+ DISABLE = 'alert_disable',
+ DELETE = 'alert_delete',
+ FIND = 'alert_find',
+ MUTE = 'alert_mute',
+ UNMUTE = 'alert_unmute',
+ MUTE_INSTANCE = 'alert_instance_mute',
+ UNMUTE_INSTANCE = 'alert_instance_unmute',
+}
+
+type VerbsTuple = [string, string, string];
+
+const eventVerbs: Record = {
+ alert_create: ['create', 'creating', 'created'],
+ alert_get: ['access', 'accessing', 'accessed'],
+ alert_update: ['update', 'updating', 'updated'],
+ alert_update_api_key: ['update API key of', 'updating API key of', 'updated API key of'],
+ alert_enable: ['enable', 'enabling', 'enabled'],
+ alert_disable: ['disable', 'disabling', 'disabled'],
+ alert_delete: ['delete', 'deleting', 'deleted'],
+ alert_find: ['access', 'accessing', 'accessed'],
+ alert_mute: ['mute', 'muting', 'muted'],
+ alert_unmute: ['unmute', 'unmuting', 'unmuted'],
+ alert_instance_mute: ['mute instance of', 'muting instance of', 'muted instance of'],
+ alert_instance_unmute: ['unmute instance of', 'unmuting instance of', 'unmuted instance of'],
+};
+
+const eventTypes: Record = {
+ alert_create: EventType.CREATION,
+ alert_get: EventType.ACCESS,
+ alert_update: EventType.CHANGE,
+ alert_update_api_key: EventType.CHANGE,
+ alert_enable: EventType.CHANGE,
+ alert_disable: EventType.CHANGE,
+ alert_delete: EventType.DELETION,
+ alert_find: EventType.ACCESS,
+ alert_mute: EventType.CHANGE,
+ alert_unmute: EventType.CHANGE,
+ alert_instance_mute: EventType.CHANGE,
+ alert_instance_unmute: EventType.CHANGE,
+};
+
+export interface AlertAuditEventParams {
+ action: AlertAuditAction;
+ outcome?: EventOutcome;
+ savedObject?: NonNullable['saved_object'];
+ error?: Error;
+}
+
+export function alertAuditEvent({
+ action,
+ savedObject,
+ outcome,
+ error,
+}: AlertAuditEventParams): AuditEvent {
+ const doc = savedObject ? `alert [id=${savedObject.id}]` : 'an alert';
+ const [present, progressive, past] = eventVerbs[action];
+ const message = error
+ ? `Failed attempt to ${present} ${doc}`
+ : outcome === EventOutcome.UNKNOWN
+ ? `User is ${progressive} ${doc}`
+ : `User has ${past} ${doc}`;
+ const type = eventTypes[action];
+
+ return {
+ message,
+ event: {
+ action,
+ category: EventCategory.DATABASE,
+ type,
+ outcome: outcome ?? (error ? EventOutcome.FAILURE : EventOutcome.SUCCESS),
+ },
+ kibana: {
+ saved_object: savedObject,
+ },
+ error: error && {
+ code: error.name,
+ message: error.message,
+ },
+ };
+}
diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/create.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/create.test.ts
index dcbb33d849405..b943a21ba9bb6 100644
--- a/x-pack/plugins/alerts/server/alerts_client/tests/create.test.ts
+++ b/x-pack/plugins/alerts/server/alerts_client/tests/create.test.ts
@@ -14,15 +14,24 @@ import { actionsAuthorizationMock } from '../../../../actions/server/mocks';
import { AlertsAuthorization } from '../../authorization/alerts_authorization';
import { ActionsAuthorization, ActionsClient } from '../../../../actions/server';
import { TaskStatus } from '../../../../task_manager/server';
+import { auditServiceMock } from '../../../../security/server/audit/index.mock';
+import { httpServerMock } from '../../../../../../src/core/server/mocks';
import { getBeforeSetup, setGlobalDate } from './lib';
import { RecoveredActionGroup } from '../../../common';
+jest.mock('../../../../../../src/core/server/saved_objects/service/lib/utils', () => ({
+ SavedObjectsUtils: {
+ generateId: () => 'mock-saved-object-id',
+ },
+}));
+
const taskManager = taskManagerMock.createStart();
const alertTypeRegistry = alertTypeRegistryMock.create();
const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
const authorization = alertsAuthorizationMock.create();
const actionsAuthorization = actionsAuthorizationMock.create();
+const auditLogger = auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest());
const kibanaVersion = 'v7.10.0';
const alertsClientParams: jest.Mocked = {
@@ -40,10 +49,12 @@ const alertsClientParams: jest.Mocked = {
getActionsClient: jest.fn(),
getEventLogClient: jest.fn(),
kibanaVersion,
+ auditLogger,
};
beforeEach(() => {
getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry);
+ (auditLogger.log as jest.Mock).mockClear();
});
setGlobalDate();
@@ -185,6 +196,62 @@ describe('create()', () => {
});
});
+ describe('auditLogger', () => {
+ test('logs audit event when creating an alert', async () => {
+ const data = getMockData({
+ enabled: false,
+ actions: [],
+ });
+ unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
+ id: '1',
+ type: 'alert',
+ attributes: data,
+ references: [],
+ });
+ await alertsClient.create({ data });
+ expect(auditLogger.log).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event: expect.objectContaining({
+ action: 'alert_create',
+ outcome: 'unknown',
+ }),
+ kibana: { saved_object: { id: 'mock-saved-object-id', type: 'alert' } },
+ })
+ );
+ });
+
+ test('logs audit event when not authorised to create an alert', async () => {
+ authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized'));
+
+ await expect(
+ alertsClient.create({
+ data: getMockData({
+ enabled: false,
+ actions: [],
+ }),
+ })
+ ).rejects.toThrow();
+ expect(auditLogger.log).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event: expect.objectContaining({
+ action: 'alert_create',
+ outcome: 'failure',
+ }),
+ kibana: {
+ saved_object: {
+ id: 'mock-saved-object-id',
+ type: 'alert',
+ },
+ },
+ error: {
+ code: 'Error',
+ message: 'Unauthorized',
+ },
+ })
+ );
+ });
+ });
+
test('creates an alert', async () => {
const data = getMockData();
const createdAttributes = {
@@ -337,16 +404,17 @@ describe('create()', () => {
}
`);
expect(unsecuredSavedObjectsClient.create.mock.calls[0][2]).toMatchInlineSnapshot(`
- Object {
- "references": Array [
- Object {
- "id": "1",
- "name": "action_0",
- "type": "action",
- },
- ],
- }
- `);
+ Object {
+ "id": "mock-saved-object-id",
+ "references": Array [
+ Object {
+ "id": "1",
+ "name": "action_0",
+ "type": "action",
+ },
+ ],
+ }
+ `);
expect(taskManager.schedule).toHaveBeenCalledTimes(1);
expect(taskManager.schedule.mock.calls[0]).toMatchInlineSnapshot(`
Array [
@@ -991,6 +1059,7 @@ describe('create()', () => {
},
},
{
+ id: 'mock-saved-object-id',
references: [
{
id: '1',
@@ -1113,6 +1182,7 @@ describe('create()', () => {
},
},
{
+ id: 'mock-saved-object-id',
references: [
{
id: '1',
diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/delete.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/delete.test.ts
index e7b975aec8eb0..a7ef008eaa2ee 100644
--- a/x-pack/plugins/alerts/server/alerts_client/tests/delete.test.ts
+++ b/x-pack/plugins/alerts/server/alerts_client/tests/delete.test.ts
@@ -12,6 +12,8 @@ import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/s
import { actionsAuthorizationMock } from '../../../../actions/server/mocks';
import { AlertsAuthorization } from '../../authorization/alerts_authorization';
import { ActionsAuthorization } from '../../../../actions/server';
+import { httpServerMock } from '../../../../../../src/core/server/mocks';
+import { auditServiceMock } from '../../../../security/server/audit/index.mock';
import { getBeforeSetup } from './lib';
const taskManager = taskManagerMock.createStart();
@@ -20,6 +22,7 @@ const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
const authorization = alertsAuthorizationMock.create();
const actionsAuthorization = actionsAuthorizationMock.create();
+const auditLogger = auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest());
const kibanaVersion = 'v7.10.0';
const alertsClientParams: jest.Mocked = {
@@ -37,10 +40,12 @@ const alertsClientParams: jest.Mocked = {
getActionsClient: jest.fn(),
getEventLogClient: jest.fn(),
kibanaVersion,
+ auditLogger,
};
beforeEach(() => {
getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry);
+ (auditLogger.log as jest.Mock).mockClear();
});
describe('delete()', () => {
@@ -239,4 +244,43 @@ describe('delete()', () => {
expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'delete');
});
});
+
+ describe('auditLogger', () => {
+ test('logs audit event when deleting an alert', async () => {
+ await alertsClient.delete({ id: '1' });
+ expect(auditLogger.log).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event: expect.objectContaining({
+ action: 'alert_delete',
+ outcome: 'unknown',
+ }),
+ kibana: { saved_object: { id: '1', type: 'alert' } },
+ })
+ );
+ });
+
+ test('logs audit event when not authorised to delete an alert', async () => {
+ authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized'));
+
+ await expect(alertsClient.delete({ id: '1' })).rejects.toThrow();
+ expect(auditLogger.log).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event: expect.objectContaining({
+ action: 'alert_delete',
+ outcome: 'failure',
+ }),
+ kibana: {
+ saved_object: {
+ id: '1',
+ type: 'alert',
+ },
+ },
+ error: {
+ code: 'Error',
+ message: 'Unauthorized',
+ },
+ })
+ );
+ });
+ });
});
diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/disable.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/disable.test.ts
index 8c9ab9494a50a..ce0688a5ab2ff 100644
--- a/x-pack/plugins/alerts/server/alerts_client/tests/disable.test.ts
+++ b/x-pack/plugins/alerts/server/alerts_client/tests/disable.test.ts
@@ -12,16 +12,18 @@ import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/s
import { actionsAuthorizationMock } from '../../../../actions/server/mocks';
import { AlertsAuthorization } from '../../authorization/alerts_authorization';
import { ActionsAuthorization } from '../../../../actions/server';
-import { getBeforeSetup, setGlobalDate } from './lib';
import { InvalidatePendingApiKey } from '../../types';
+import { httpServerMock } from '../../../../../../src/core/server/mocks';
+import { auditServiceMock } from '../../../../security/server/audit/index.mock';
+import { getBeforeSetup, setGlobalDate } from './lib';
const taskManager = taskManagerMock.createStart();
const alertTypeRegistry = alertTypeRegistryMock.create();
const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
-
const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
const authorization = alertsAuthorizationMock.create();
const actionsAuthorization = actionsAuthorizationMock.create();
+const auditLogger = auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest());
const kibanaVersion = 'v7.10.0';
const alertsClientParams: jest.Mocked = {
@@ -39,10 +41,12 @@ const alertsClientParams: jest.Mocked = {
getActionsClient: jest.fn(),
getEventLogClient: jest.fn(),
kibanaVersion,
+ auditLogger,
};
beforeEach(() => {
getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry);
+ (auditLogger.log as jest.Mock).mockClear();
});
setGlobalDate();
@@ -109,6 +113,45 @@ describe('disable()', () => {
});
});
+ describe('auditLogger', () => {
+ test('logs audit event when disabling an alert', async () => {
+ await alertsClient.disable({ id: '1' });
+ expect(auditLogger.log).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event: expect.objectContaining({
+ action: 'alert_disable',
+ outcome: 'unknown',
+ }),
+ kibana: { saved_object: { id: '1', type: 'alert' } },
+ })
+ );
+ });
+
+ test('logs audit event when not authorised to disable an alert', async () => {
+ authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized'));
+
+ await expect(alertsClient.disable({ id: '1' })).rejects.toThrow();
+ expect(auditLogger.log).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event: expect.objectContaining({
+ action: 'alert_disable',
+ outcome: 'failure',
+ }),
+ kibana: {
+ saved_object: {
+ id: '1',
+ type: 'alert',
+ },
+ },
+ error: {
+ code: 'Error',
+ message: 'Unauthorized',
+ },
+ })
+ );
+ });
+ });
+
test('disables an alert', async () => {
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
id: '1',
diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/enable.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/enable.test.ts
index feec1d1b9334a..daac6689a183b 100644
--- a/x-pack/plugins/alerts/server/alerts_client/tests/enable.test.ts
+++ b/x-pack/plugins/alerts/server/alerts_client/tests/enable.test.ts
@@ -13,16 +13,18 @@ import { actionsAuthorizationMock } from '../../../../actions/server/mocks';
import { AlertsAuthorization } from '../../authorization/alerts_authorization';
import { ActionsAuthorization } from '../../../../actions/server';
import { TaskStatus } from '../../../../task_manager/server';
-import { getBeforeSetup, setGlobalDate } from './lib';
+import { httpServerMock } from '../../../../../../src/core/server/mocks';
+import { auditServiceMock } from '../../../../security/server/audit/index.mock';
import { InvalidatePendingApiKey } from '../../types';
+import { getBeforeSetup, setGlobalDate } from './lib';
const taskManager = taskManagerMock.createStart();
const alertTypeRegistry = alertTypeRegistryMock.create();
const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
-
const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
const authorization = alertsAuthorizationMock.create();
const actionsAuthorization = actionsAuthorizationMock.create();
+const auditLogger = auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest());
const kibanaVersion = 'v7.10.0';
const alertsClientParams: jest.Mocked = {
@@ -40,10 +42,12 @@ const alertsClientParams: jest.Mocked = {
getActionsClient: jest.fn(),
getEventLogClient: jest.fn(),
kibanaVersion,
+ auditLogger,
};
beforeEach(() => {
getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry);
+ (auditLogger.log as jest.Mock).mockClear();
});
setGlobalDate();
@@ -148,6 +152,45 @@ describe('enable()', () => {
});
});
+ describe('auditLogger', () => {
+ test('logs audit event when enabling an alert', async () => {
+ await alertsClient.enable({ id: '1' });
+ expect(auditLogger.log).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event: expect.objectContaining({
+ action: 'alert_enable',
+ outcome: 'unknown',
+ }),
+ kibana: { saved_object: { id: '1', type: 'alert' } },
+ })
+ );
+ });
+
+ test('logs audit event when not authorised to enable an alert', async () => {
+ authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized'));
+
+ await expect(alertsClient.enable({ id: '1' })).rejects.toThrow();
+ expect(auditLogger.log).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event: expect.objectContaining({
+ action: 'alert_enable',
+ outcome: 'failure',
+ }),
+ kibana: {
+ saved_object: {
+ id: '1',
+ type: 'alert',
+ },
+ },
+ error: {
+ code: 'Error',
+ message: 'Unauthorized',
+ },
+ })
+ );
+ });
+ });
+
test('enables an alert', async () => {
const createdAt = new Date().toISOString();
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/find.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/find.test.ts
index 336cb536d702b..232d48e258256 100644
--- a/x-pack/plugins/alerts/server/alerts_client/tests/find.test.ts
+++ b/x-pack/plugins/alerts/server/alerts_client/tests/find.test.ts
@@ -14,16 +14,18 @@ import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/s
import { actionsAuthorizationMock } from '../../../../actions/server/mocks';
import { AlertsAuthorization } from '../../authorization/alerts_authorization';
import { ActionsAuthorization } from '../../../../actions/server';
+import { httpServerMock } from '../../../../../../src/core/server/mocks';
+import { auditServiceMock } from '../../../../security/server/audit/index.mock';
import { getBeforeSetup, setGlobalDate } from './lib';
import { RecoveredActionGroup } from '../../../common';
const taskManager = taskManagerMock.createStart();
const alertTypeRegistry = alertTypeRegistryMock.create();
const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
-
const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
const authorization = alertsAuthorizationMock.create();
const actionsAuthorization = actionsAuthorizationMock.create();
+const auditLogger = auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest());
const kibanaVersion = 'v7.10.0';
const alertsClientParams: jest.Mocked = {
@@ -45,6 +47,7 @@ const alertsClientParams: jest.Mocked = {
beforeEach(() => {
getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry);
+ (auditLogger.log as jest.Mock).mockClear();
});
setGlobalDate();
@@ -251,4 +254,64 @@ describe('find()', () => {
expect(logSuccessfulAuthorization).toHaveBeenCalled();
});
});
+
+ describe('auditLogger', () => {
+ test('logs audit event when searching alerts', async () => {
+ const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger });
+ await alertsClient.find();
+ expect(auditLogger.log).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event: expect.objectContaining({
+ action: 'alert_find',
+ outcome: 'success',
+ }),
+ kibana: { saved_object: { id: '1', type: 'alert' } },
+ })
+ );
+ });
+
+ test('logs audit event when not authorised to search alerts', async () => {
+ const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger });
+ authorization.getFindAuthorizationFilter.mockRejectedValue(new Error('Unauthorized'));
+
+ await expect(alertsClient.find()).rejects.toThrow();
+ expect(auditLogger.log).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event: expect.objectContaining({
+ action: 'alert_find',
+ outcome: 'failure',
+ }),
+ error: {
+ code: 'Error',
+ message: 'Unauthorized',
+ },
+ })
+ );
+ });
+
+ test('logs audit event when not authorised to search alert type', async () => {
+ const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger });
+ authorization.getFindAuthorizationFilter.mockResolvedValue({
+ ensureAlertTypeIsAuthorized: jest.fn(() => {
+ throw new Error('Unauthorized');
+ }),
+ logSuccessfulAuthorization: jest.fn(),
+ });
+
+ await expect(async () => await alertsClient.find()).rejects.toThrow();
+ expect(auditLogger.log).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event: expect.objectContaining({
+ action: 'alert_find',
+ outcome: 'failure',
+ }),
+ kibana: { saved_object: { id: '1', type: 'alert' } },
+ error: {
+ code: 'Error',
+ message: 'Unauthorized',
+ },
+ })
+ );
+ });
+ });
});
diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/get.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/get.test.ts
index 3f0c783f424d1..32ac57459795e 100644
--- a/x-pack/plugins/alerts/server/alerts_client/tests/get.test.ts
+++ b/x-pack/plugins/alerts/server/alerts_client/tests/get.test.ts
@@ -12,15 +12,17 @@ import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/s
import { actionsAuthorizationMock } from '../../../../actions/server/mocks';
import { AlertsAuthorization } from '../../authorization/alerts_authorization';
import { ActionsAuthorization } from '../../../../actions/server';
+import { httpServerMock } from '../../../../../../src/core/server/mocks';
+import { auditServiceMock } from '../../../../security/server/audit/index.mock';
import { getBeforeSetup, setGlobalDate } from './lib';
const taskManager = taskManagerMock.createStart();
const alertTypeRegistry = alertTypeRegistryMock.create();
const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
-
const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
const authorization = alertsAuthorizationMock.create();
const actionsAuthorization = actionsAuthorizationMock.create();
+const auditLogger = auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest());
const kibanaVersion = 'v7.10.0';
const alertsClientParams: jest.Mocked = {
@@ -42,6 +44,7 @@ const alertsClientParams: jest.Mocked = {
beforeEach(() => {
getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry);
+ (auditLogger.log as jest.Mock).mockClear();
});
setGlobalDate();
@@ -191,4 +194,61 @@ describe('get()', () => {
expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'get');
});
});
+
+ describe('auditLogger', () => {
+ beforeEach(() => {
+ unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
+ id: '1',
+ type: 'alert',
+ attributes: {
+ alertTypeId: '123',
+ schedule: { interval: '10s' },
+ params: {
+ bar: true,
+ },
+ actions: [],
+ },
+ references: [],
+ });
+ });
+
+ test('logs audit event when getting an alert', async () => {
+ const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger });
+ await alertsClient.get({ id: '1' });
+ expect(auditLogger.log).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event: expect.objectContaining({
+ action: 'alert_get',
+ outcome: 'success',
+ }),
+ kibana: { saved_object: { id: '1', type: 'alert' } },
+ })
+ );
+ });
+
+ test('logs audit event when not authorised to get an alert', async () => {
+ const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger });
+ authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized'));
+
+ await expect(alertsClient.get({ id: '1' })).rejects.toThrow();
+ expect(auditLogger.log).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event: expect.objectContaining({
+ action: 'alert_get',
+ outcome: 'failure',
+ }),
+ kibana: {
+ saved_object: {
+ id: '1',
+ type: 'alert',
+ },
+ },
+ error: {
+ code: 'Error',
+ message: 'Unauthorized',
+ },
+ })
+ );
+ });
+ });
});
diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/mute_all.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/mute_all.test.ts
index 14ebca2135587..b3c3e1bdd2ede 100644
--- a/x-pack/plugins/alerts/server/alerts_client/tests/mute_all.test.ts
+++ b/x-pack/plugins/alerts/server/alerts_client/tests/mute_all.test.ts
@@ -12,6 +12,8 @@ import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/s
import { actionsAuthorizationMock } from '../../../../actions/server/mocks';
import { AlertsAuthorization } from '../../authorization/alerts_authorization';
import { ActionsAuthorization } from '../../../../actions/server';
+import { httpServerMock } from '../../../../../../src/core/server/mocks';
+import { auditServiceMock } from '../../../../security/server/audit/index.mock';
import { getBeforeSetup, setGlobalDate } from './lib';
const taskManager = taskManagerMock.createStart();
@@ -20,6 +22,7 @@ const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
const authorization = alertsAuthorizationMock.create();
const actionsAuthorization = actionsAuthorizationMock.create();
+const auditLogger = auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest());
const kibanaVersion = 'v7.10.0';
const alertsClientParams: jest.Mocked = {
@@ -41,6 +44,7 @@ const alertsClientParams: jest.Mocked = {
beforeEach(() => {
getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry);
+ (auditLogger.log as jest.Mock).mockClear();
});
setGlobalDate();
@@ -137,4 +141,85 @@ describe('muteAll()', () => {
expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'muteAll');
});
});
+
+ describe('auditLogger', () => {
+ test('logs audit event when muting an alert', async () => {
+ const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger });
+ unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
+ id: '1',
+ type: 'alert',
+ attributes: {
+ actions: [
+ {
+ group: 'default',
+ id: '1',
+ actionTypeId: '1',
+ actionRef: '1',
+ params: {
+ foo: true,
+ },
+ },
+ ],
+ muteAll: false,
+ },
+ references: [],
+ version: '123',
+ });
+ await alertsClient.muteAll({ id: '1' });
+ expect(auditLogger.log).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event: expect.objectContaining({
+ action: 'alert_mute',
+ outcome: 'unknown',
+ }),
+ kibana: { saved_object: { id: '1', type: 'alert' } },
+ })
+ );
+ });
+
+ test('logs audit event when not authorised to mute an alert', async () => {
+ const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger });
+ unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
+ id: '1',
+ type: 'alert',
+ attributes: {
+ actions: [
+ {
+ group: 'default',
+ id: '1',
+ actionTypeId: '1',
+ actionRef: '1',
+ params: {
+ foo: true,
+ },
+ },
+ ],
+ muteAll: false,
+ },
+ references: [],
+ version: '123',
+ });
+ authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized'));
+
+ await expect(alertsClient.muteAll({ id: '1' })).rejects.toThrow();
+ expect(auditLogger.log).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event: expect.objectContaining({
+ action: 'alert_mute',
+ outcome: 'failure',
+ }),
+ kibana: {
+ saved_object: {
+ id: '1',
+ type: 'alert',
+ },
+ },
+ error: {
+ code: 'Error',
+ message: 'Unauthorized',
+ },
+ })
+ );
+ });
+ });
});
diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/mute_instance.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/mute_instance.test.ts
index c2188f128cb4d..ec69dbdeac55f 100644
--- a/x-pack/plugins/alerts/server/alerts_client/tests/mute_instance.test.ts
+++ b/x-pack/plugins/alerts/server/alerts_client/tests/mute_instance.test.ts
@@ -12,15 +12,17 @@ import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/s
import { actionsAuthorizationMock } from '../../../../actions/server/mocks';
import { AlertsAuthorization } from '../../authorization/alerts_authorization';
import { ActionsAuthorization } from '../../../../actions/server';
+import { httpServerMock } from '../../../../../../src/core/server/mocks';
+import { auditServiceMock } from '../../../../security/server/audit/index.mock';
import { getBeforeSetup, setGlobalDate } from './lib';
const taskManager = taskManagerMock.createStart();
const alertTypeRegistry = alertTypeRegistryMock.create();
const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
-
const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
const authorization = alertsAuthorizationMock.create();
const actionsAuthorization = actionsAuthorizationMock.create();
+const auditLogger = auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest());
const kibanaVersion = 'v7.10.0';
const alertsClientParams: jest.Mocked = {
@@ -42,6 +44,7 @@ const alertsClientParams: jest.Mocked = {
beforeEach(() => {
getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry);
+ (auditLogger.log as jest.Mock).mockClear();
});
setGlobalDate();
@@ -180,4 +183,75 @@ describe('muteInstance()', () => {
);
});
});
+
+ describe('auditLogger', () => {
+ test('logs audit event when muting an alert instance', async () => {
+ const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger });
+ unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
+ id: '1',
+ type: 'alert',
+ attributes: {
+ actions: [],
+ schedule: { interval: '10s' },
+ alertTypeId: '2',
+ enabled: true,
+ scheduledTaskId: 'task-123',
+ mutedInstanceIds: [],
+ },
+ version: '123',
+ references: [],
+ });
+ await alertsClient.muteInstance({ alertId: '1', alertInstanceId: '2' });
+ expect(auditLogger.log).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event: expect.objectContaining({
+ action: 'alert_instance_mute',
+ outcome: 'unknown',
+ }),
+ kibana: { saved_object: { id: '1', type: 'alert' } },
+ })
+ );
+ });
+
+ test('logs audit event when not authorised to mute an alert instance', async () => {
+ const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger });
+ unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
+ id: '1',
+ type: 'alert',
+ attributes: {
+ actions: [],
+ schedule: { interval: '10s' },
+ alertTypeId: '2',
+ enabled: true,
+ scheduledTaskId: 'task-123',
+ mutedInstanceIds: [],
+ },
+ version: '123',
+ references: [],
+ });
+ authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized'));
+
+ await expect(
+ alertsClient.muteInstance({ alertId: '1', alertInstanceId: '2' })
+ ).rejects.toThrow();
+ expect(auditLogger.log).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event: expect.objectContaining({
+ action: 'alert_instance_mute',
+ outcome: 'failure',
+ }),
+ kibana: {
+ saved_object: {
+ id: '1',
+ type: 'alert',
+ },
+ },
+ error: {
+ code: 'Error',
+ message: 'Unauthorized',
+ },
+ })
+ );
+ });
+ });
});
diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/unmute_all.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/unmute_all.test.ts
index d92304ab873be..fd0157091e3a5 100644
--- a/x-pack/plugins/alerts/server/alerts_client/tests/unmute_all.test.ts
+++ b/x-pack/plugins/alerts/server/alerts_client/tests/unmute_all.test.ts
@@ -12,15 +12,17 @@ import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/s
import { actionsAuthorizationMock } from '../../../../actions/server/mocks';
import { AlertsAuthorization } from '../../authorization/alerts_authorization';
import { ActionsAuthorization } from '../../../../actions/server';
+import { httpServerMock } from '../../../../../../src/core/server/mocks';
+import { auditServiceMock } from '../../../../security/server/audit/index.mock';
import { getBeforeSetup, setGlobalDate } from './lib';
const taskManager = taskManagerMock.createStart();
const alertTypeRegistry = alertTypeRegistryMock.create();
const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
-
const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
const authorization = alertsAuthorizationMock.create();
const actionsAuthorization = actionsAuthorizationMock.create();
+const auditLogger = auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest());
const kibanaVersion = 'v7.10.0';
const alertsClientParams: jest.Mocked = {
@@ -42,6 +44,7 @@ const alertsClientParams: jest.Mocked = {
beforeEach(() => {
getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry);
+ (auditLogger.log as jest.Mock).mockClear();
});
setGlobalDate();
@@ -138,4 +141,85 @@ describe('unmuteAll()', () => {
expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'unmuteAll');
});
});
+
+ describe('auditLogger', () => {
+ test('logs audit event when unmuting an alert', async () => {
+ const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger });
+ unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
+ id: '1',
+ type: 'alert',
+ attributes: {
+ actions: [
+ {
+ group: 'default',
+ id: '1',
+ actionTypeId: '1',
+ actionRef: '1',
+ params: {
+ foo: true,
+ },
+ },
+ ],
+ muteAll: false,
+ },
+ references: [],
+ version: '123',
+ });
+ await alertsClient.unmuteAll({ id: '1' });
+ expect(auditLogger.log).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event: expect.objectContaining({
+ action: 'alert_unmute',
+ outcome: 'unknown',
+ }),
+ kibana: { saved_object: { id: '1', type: 'alert' } },
+ })
+ );
+ });
+
+ test('logs audit event when not authorised to unmute an alert', async () => {
+ const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger });
+ unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
+ id: '1',
+ type: 'alert',
+ attributes: {
+ actions: [
+ {
+ group: 'default',
+ id: '1',
+ actionTypeId: '1',
+ actionRef: '1',
+ params: {
+ foo: true,
+ },
+ },
+ ],
+ muteAll: false,
+ },
+ references: [],
+ version: '123',
+ });
+ authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized'));
+
+ await expect(alertsClient.unmuteAll({ id: '1' })).rejects.toThrow();
+ expect(auditLogger.log).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event: expect.objectContaining({
+ action: 'alert_unmute',
+ outcome: 'failure',
+ }),
+ kibana: {
+ saved_object: {
+ id: '1',
+ type: 'alert',
+ },
+ },
+ error: {
+ code: 'Error',
+ message: 'Unauthorized',
+ },
+ })
+ );
+ });
+ });
});
diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/unmute_instance.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/unmute_instance.test.ts
index 3486df98f2f05..c7d084a01a2a0 100644
--- a/x-pack/plugins/alerts/server/alerts_client/tests/unmute_instance.test.ts
+++ b/x-pack/plugins/alerts/server/alerts_client/tests/unmute_instance.test.ts
@@ -12,15 +12,17 @@ import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/s
import { actionsAuthorizationMock } from '../../../../actions/server/mocks';
import { AlertsAuthorization } from '../../authorization/alerts_authorization';
import { ActionsAuthorization } from '../../../../actions/server';
+import { httpServerMock } from '../../../../../../src/core/server/mocks';
+import { auditServiceMock } from '../../../../security/server/audit/index.mock';
import { getBeforeSetup, setGlobalDate } from './lib';
const taskManager = taskManagerMock.createStart();
const alertTypeRegistry = alertTypeRegistryMock.create();
const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
-
const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
const authorization = alertsAuthorizationMock.create();
const actionsAuthorization = actionsAuthorizationMock.create();
+const auditLogger = auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest());
const kibanaVersion = 'v7.10.0';
const alertsClientParams: jest.Mocked = {
@@ -42,6 +44,7 @@ const alertsClientParams: jest.Mocked