mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[8.x] [Response Ops][Alerting] Adding ability to run actions for backfill rule runs (#200784) (#207273)
# Backport This will backport the following commits from `main` to `8.x`: - [[Response Ops][Alerting] Adding ability to run actions for backfill rule runs (#200784)](https://github.com/elastic/kibana/pull/200784) <!--- Backport version: 9.6.4 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sorenlouv/backport) <!--BACKPORT [{"author":{"name":"Ying Mao","email":"ying.mao@elastic.co"},"sourceCommit":{"committedDate":"2025-01-20T15:03:33Z","message":"[Response Ops][Alerting] Adding ability to run actions for backfill rule runs (#200784)\n\nResolves https://github.com/elastic/response-ops-team/issues/251\r\n\r\n\r\n## Note\r\n\r\nThis PR includes some saved object schema changes that I will pull out\r\ninto their own separate PR in order to perform an intermediate release.\r\nI wanted to make sure all the schema changes made sense in the overall\r\ncontext of the PR before opening those separate PRs.\r\n\r\nUpdate: PR for intermediate release here:\r\nhttps://github.com/elastic/kibana/pull/203184 (Merged)\r\n\r\n## Summary\r\n\r\nAdds ability to run actions for backfill rule runs.\r\n\r\n- Updates schedule backfill API to accept `run_actions` parameter to\r\nspecify whether to run actions for backfill.\r\n- Schedule API accepts any action where `frequency.notifyWhen ===\r\n'onActiveAlert'`. If a rule has multiple actions where some are\r\n`onActiveAlert` and some are `onThrottleInterval`, the invalid actions\r\nwill be stripped and a warning returned in the schedule response but\r\nvalid actions will be scheduled.\r\n- Connector IDs are extracted and stored as references in the ad hoc run\r\nparams saved object\r\n- Any actions that result from a backfill task run are scheduled as low\r\npriority tasks\r\n\r\n## To Verify\r\n\r\n1. Create a detection rule. Make sure you have some past data that the\r\nrule can run over in order to generate actions. Make sure you add\r\nactions to the rule. For testing, I added some conditional actions so I\r\ncould see actions running only on backfill runs using\r\n`kibana.alert.rule.execution.type: \"manual\"`. Create actions with and\r\nwithout summaries.\r\n2. Schedule a backfill either directly via the API or using the\r\ndetection UI. Verify that actions are run for the backfill runs that\r\ngenerate alerts.\r\n\r\n---------\r\n\r\nCo-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>","sha":"075806bffa78cc4f42e61483dcbd24de3c87d3c8","branchLabelMapping":{"^v9.0.0$":"main","^v8.18.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["Feature:Alerting","release_note:skip","Team:ResponseOps","v9.0.0","backport:prev-minor","ci:project-deploy-security","v8.18.0"],"title":"[Response Ops][Alerting] Adding ability to run actions for backfill rule runs","number":200784,"url":"https://github.com/elastic/kibana/pull/200784","mergeCommit":{"message":"[Response Ops][Alerting] Adding ability to run actions for backfill rule runs (#200784)\n\nResolves https://github.com/elastic/response-ops-team/issues/251\r\n\r\n\r\n## Note\r\n\r\nThis PR includes some saved object schema changes that I will pull out\r\ninto their own separate PR in order to perform an intermediate release.\r\nI wanted to make sure all the schema changes made sense in the overall\r\ncontext of the PR before opening those separate PRs.\r\n\r\nUpdate: PR for intermediate release here:\r\nhttps://github.com/elastic/kibana/pull/203184 (Merged)\r\n\r\n## Summary\r\n\r\nAdds ability to run actions for backfill rule runs.\r\n\r\n- Updates schedule backfill API to accept `run_actions` parameter to\r\nspecify whether to run actions for backfill.\r\n- Schedule API accepts any action where `frequency.notifyWhen ===\r\n'onActiveAlert'`. If a rule has multiple actions where some are\r\n`onActiveAlert` and some are `onThrottleInterval`, the invalid actions\r\nwill be stripped and a warning returned in the schedule response but\r\nvalid actions will be scheduled.\r\n- Connector IDs are extracted and stored as references in the ad hoc run\r\nparams saved object\r\n- Any actions that result from a backfill task run are scheduled as low\r\npriority tasks\r\n\r\n## To Verify\r\n\r\n1. Create a detection rule. Make sure you have some past data that the\r\nrule can run over in order to generate actions. Make sure you add\r\nactions to the rule. For testing, I added some conditional actions so I\r\ncould see actions running only on backfill runs using\r\n`kibana.alert.rule.execution.type: \"manual\"`. Create actions with and\r\nwithout summaries.\r\n2. Schedule a backfill either directly via the API or using the\r\ndetection UI. Verify that actions are run for the backfill runs that\r\ngenerate alerts.\r\n\r\n---------\r\n\r\nCo-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>","sha":"075806bffa78cc4f42e61483dcbd24de3c87d3c8"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/200784","number":200784,"mergeCommit":{"message":"[Response Ops][Alerting] Adding ability to run actions for backfill rule runs (#200784)\n\nResolves https://github.com/elastic/response-ops-team/issues/251\r\n\r\n\r\n## Note\r\n\r\nThis PR includes some saved object schema changes that I will pull out\r\ninto their own separate PR in order to perform an intermediate release.\r\nI wanted to make sure all the schema changes made sense in the overall\r\ncontext of the PR before opening those separate PRs.\r\n\r\nUpdate: PR for intermediate release here:\r\nhttps://github.com/elastic/kibana/pull/203184 (Merged)\r\n\r\n## Summary\r\n\r\nAdds ability to run actions for backfill rule runs.\r\n\r\n- Updates schedule backfill API to accept `run_actions` parameter to\r\nspecify whether to run actions for backfill.\r\n- Schedule API accepts any action where `frequency.notifyWhen ===\r\n'onActiveAlert'`. If a rule has multiple actions where some are\r\n`onActiveAlert` and some are `onThrottleInterval`, the invalid actions\r\nwill be stripped and a warning returned in the schedule response but\r\nvalid actions will be scheduled.\r\n- Connector IDs are extracted and stored as references in the ad hoc run\r\nparams saved object\r\n- Any actions that result from a backfill task run are scheduled as low\r\npriority tasks\r\n\r\n## To Verify\r\n\r\n1. Create a detection rule. Make sure you have some past data that the\r\nrule can run over in order to generate actions. Make sure you add\r\nactions to the rule. For testing, I added some conditional actions so I\r\ncould see actions running only on backfill runs using\r\n`kibana.alert.rule.execution.type: \"manual\"`. Create actions with and\r\nwithout summaries.\r\n2. Schedule a backfill either directly via the API or using the\r\ndetection UI. Verify that actions are run for the backfill runs that\r\ngenerate alerts.\r\n\r\n---------\r\n\r\nCo-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>","sha":"075806bffa78cc4f42e61483dcbd24de3c87d3c8"}},{"branch":"8.x","label":"v8.18.0","branchLabelMappingKey":"^v8.18.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}] BACKPORT--> --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
3d8c4e84da
commit
b059522911
50 changed files with 4255 additions and 421 deletions
|
@ -16,6 +16,7 @@ import {
|
|||
asSavedObjectExecutionSource,
|
||||
} from './lib/action_execution_source';
|
||||
import { actionsConfigMock } from './actions_config.mock';
|
||||
import { TaskPriority } from '@kbn/task-manager-plugin/server';
|
||||
|
||||
const mockTaskManager = taskManagerMock.createStart();
|
||||
const savedObjectsClient = savedObjectsClientMock.create();
|
||||
|
@ -1189,4 +1190,210 @@ describe('bulkExecute()', () => {
|
|||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test('uses priority if specified', async () => {
|
||||
mockTaskManager.aggregate.mockResolvedValue({
|
||||
took: 1,
|
||||
timed_out: false,
|
||||
_shards: { total: 1, successful: 1, skipped: 0, failed: 0 },
|
||||
hits: { total: { value: 2, relation: 'eq' }, max_score: null, hits: [] },
|
||||
aggregations: {},
|
||||
});
|
||||
mockActionsConfig.getMaxQueued.mockReturnValueOnce(3);
|
||||
const executeFn = createBulkExecutionEnqueuerFunction({
|
||||
taskManager: mockTaskManager,
|
||||
actionTypeRegistry: actionTypeRegistryMock.create(),
|
||||
isESOCanEncrypt: true,
|
||||
inMemoryConnectors: [],
|
||||
configurationUtilities: mockActionsConfig,
|
||||
logger: mockLogger,
|
||||
});
|
||||
savedObjectsClient.bulkGet.mockResolvedValueOnce({
|
||||
saved_objects: [
|
||||
{ id: '123', type: 'action', attributes: { actionTypeId: 'mock-action' }, references: [] },
|
||||
],
|
||||
});
|
||||
savedObjectsClient.bulkCreate.mockResolvedValueOnce({
|
||||
saved_objects: [
|
||||
{ id: '234', type: 'action_task_params', attributes: { actionId: '123' }, references: [] },
|
||||
],
|
||||
});
|
||||
expect(
|
||||
await executeFn(savedObjectsClient, [
|
||||
{
|
||||
id: '123',
|
||||
params: { baz: false },
|
||||
spaceId: 'default',
|
||||
executionId: '123abc',
|
||||
apiKey: null,
|
||||
source: asHttpRequestExecutionSource(request),
|
||||
actionTypeId: 'mock-action',
|
||||
priority: TaskPriority.Low,
|
||||
},
|
||||
{
|
||||
id: '123',
|
||||
params: { baz: false },
|
||||
spaceId: 'default',
|
||||
executionId: '456xyz',
|
||||
apiKey: null,
|
||||
source: asHttpRequestExecutionSource(request),
|
||||
actionTypeId: 'mock-action',
|
||||
},
|
||||
])
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"errors": true,
|
||||
"items": Array [
|
||||
Object {
|
||||
"actionTypeId": "mock-action",
|
||||
"id": "123",
|
||||
"response": "success",
|
||||
"uuid": undefined,
|
||||
},
|
||||
Object {
|
||||
"actionTypeId": "mock-action",
|
||||
"id": "123",
|
||||
"response": "queuedActionsLimitError",
|
||||
"uuid": undefined,
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
expect(mockTaskManager.bulkSchedule).toHaveBeenCalledTimes(1);
|
||||
expect(mockTaskManager.bulkSchedule.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
Object {
|
||||
"params": Object {
|
||||
"actionTaskParamsId": "234",
|
||||
"spaceId": "default",
|
||||
},
|
||||
"priority": 1,
|
||||
"scope": Array [
|
||||
"actions",
|
||||
],
|
||||
"state": Object {},
|
||||
"taskType": "actions:mock-action",
|
||||
},
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test('uses apiKeyId if specified', async () => {
|
||||
mockTaskManager.aggregate.mockResolvedValue({
|
||||
took: 1,
|
||||
timed_out: false,
|
||||
_shards: { total: 1, successful: 1, skipped: 0, failed: 0 },
|
||||
hits: { total: { value: 2, relation: 'eq' }, max_score: null, hits: [] },
|
||||
aggregations: {},
|
||||
});
|
||||
mockActionsConfig.getMaxQueued.mockReturnValueOnce(3);
|
||||
const executeFn = createBulkExecutionEnqueuerFunction({
|
||||
taskManager: mockTaskManager,
|
||||
actionTypeRegistry: actionTypeRegistryMock.create(),
|
||||
isESOCanEncrypt: true,
|
||||
inMemoryConnectors: [],
|
||||
configurationUtilities: mockActionsConfig,
|
||||
logger: mockLogger,
|
||||
});
|
||||
savedObjectsClient.bulkGet.mockResolvedValueOnce({
|
||||
saved_objects: [
|
||||
{ id: '123', type: 'action', attributes: { actionTypeId: 'mock-action' }, references: [] },
|
||||
],
|
||||
});
|
||||
savedObjectsClient.bulkCreate.mockResolvedValueOnce({
|
||||
saved_objects: [
|
||||
{ id: '234', type: 'action_task_params', attributes: { actionId: '123' }, references: [] },
|
||||
],
|
||||
});
|
||||
expect(
|
||||
await executeFn(savedObjectsClient, [
|
||||
{
|
||||
id: '123',
|
||||
params: { baz: false },
|
||||
spaceId: 'default',
|
||||
executionId: '123abc',
|
||||
apiKey: null,
|
||||
source: asHttpRequestExecutionSource(request),
|
||||
actionTypeId: 'mock-action',
|
||||
apiKeyId: '235qgbdbqet',
|
||||
},
|
||||
{
|
||||
id: '123',
|
||||
params: { baz: false },
|
||||
spaceId: 'default',
|
||||
executionId: '456xyz',
|
||||
apiKey: null,
|
||||
source: asHttpRequestExecutionSource(request),
|
||||
actionTypeId: 'mock-action',
|
||||
apiKeyId: '235qgbdbqet',
|
||||
},
|
||||
])
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"errors": true,
|
||||
"items": Array [
|
||||
Object {
|
||||
"actionTypeId": "mock-action",
|
||||
"id": "123",
|
||||
"response": "success",
|
||||
"uuid": undefined,
|
||||
},
|
||||
Object {
|
||||
"actionTypeId": "mock-action",
|
||||
"id": "123",
|
||||
"response": "queuedActionsLimitError",
|
||||
"uuid": undefined,
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
|
||||
expect(savedObjectsClient.bulkCreate).toHaveBeenCalledWith(
|
||||
[
|
||||
{
|
||||
attributes: {
|
||||
actionId: '123',
|
||||
apiKey: null,
|
||||
apiKeyId: '235qgbdbqet',
|
||||
consumer: undefined,
|
||||
executionId: '123abc',
|
||||
params: {
|
||||
baz: false,
|
||||
},
|
||||
relatedSavedObjects: undefined,
|
||||
source: 'HTTP_REQUEST',
|
||||
},
|
||||
references: [
|
||||
{
|
||||
id: '123',
|
||||
name: 'actionRef',
|
||||
type: 'action',
|
||||
},
|
||||
],
|
||||
type: 'action_task_params',
|
||||
},
|
||||
],
|
||||
{ refresh: false }
|
||||
);
|
||||
expect(mockTaskManager.bulkSchedule).toHaveBeenCalledTimes(1);
|
||||
expect(mockTaskManager.bulkSchedule.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
Object {
|
||||
"params": Object {
|
||||
"actionTaskParamsId": "234",
|
||||
"spaceId": "default",
|
||||
},
|
||||
"scope": Array [
|
||||
"actions",
|
||||
],
|
||||
"state": Object {},
|
||||
"taskType": "actions:mock-action",
|
||||
},
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,7 +6,11 @@
|
|||
*/
|
||||
|
||||
import { SavedObjectsBulkResponse, SavedObjectsClientContract, Logger } from '@kbn/core/server';
|
||||
import { RunNowResult, TaskManagerStartContract } from '@kbn/task-manager-plugin/server';
|
||||
import {
|
||||
RunNowResult,
|
||||
TaskPriority,
|
||||
TaskManagerStartContract,
|
||||
} from '@kbn/task-manager-plugin/server';
|
||||
import {
|
||||
RawAction,
|
||||
ActionTypeRegistryContract,
|
||||
|
@ -33,9 +37,11 @@ export interface ExecuteOptions
|
|||
id: string;
|
||||
uuid?: string;
|
||||
spaceId: string;
|
||||
apiKeyId?: string;
|
||||
apiKey: string | null;
|
||||
executionId: string;
|
||||
actionTypeId: string;
|
||||
priority?: TaskPriority;
|
||||
}
|
||||
|
||||
interface ActionTaskParams
|
||||
|
@ -171,15 +177,17 @@ export function createBulkExecutionEnqueuerFunction({
|
|||
executionId: actionToExecute.executionId,
|
||||
consumer: actionToExecute.consumer,
|
||||
relatedSavedObjects: relatedSavedObjectWithRefs,
|
||||
apiKeyId: actionToExecute.apiKeyId,
|
||||
...(actionToExecute.source ? { source: actionToExecute.source.type } : {}),
|
||||
},
|
||||
references: taskReferences,
|
||||
};
|
||||
});
|
||||
|
||||
const actionTaskParamsRecords: SavedObjectsBulkResponse<ActionTaskParams> =
|
||||
await unsecuredSavedObjectsClient.bulkCreate(actions, { refresh: false });
|
||||
|
||||
const taskInstances = actionTaskParamsRecords.saved_objects.map((so) => {
|
||||
const taskInstances = actionTaskParamsRecords.saved_objects.map((so, index) => {
|
||||
const actionId = so.attributes.actionId;
|
||||
return {
|
||||
taskType: `actions:${actionTypeIds[actionId]}`,
|
||||
|
@ -189,6 +197,7 @@ export function createBulkExecutionEnqueuerFunction({
|
|||
},
|
||||
state: {},
|
||||
scope: ['actions'],
|
||||
...(runnableActions[index]?.priority ? { priority: runnableActions[index].priority } : {}),
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
@ -85,8 +85,8 @@ const mockAdHocRunSO: SavedObject<AdHocRunSO> = {
|
|||
name: fakeRuleName,
|
||||
tags: ['foo'],
|
||||
alertTypeId: 'myType',
|
||||
// @ts-expect-error
|
||||
params: {},
|
||||
actions: [],
|
||||
apiKeyOwner: 'user',
|
||||
apiKeyCreatedByUser: false,
|
||||
consumer: 'myApp',
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { ActionsAuthorization } from '@kbn/actions-plugin/server';
|
||||
import { actionsAuthorizationMock } from '@kbn/actions-plugin/server/mocks';
|
||||
import { actionsAuthorizationMock, actionsClientMock } from '@kbn/actions-plugin/server/mocks';
|
||||
import { RULE_SAVED_OBJECT_TYPE } from '../../../..';
|
||||
import { AlertingAuthorization } from '../../../../authorization';
|
||||
import { alertingAuthorizationMock } from '../../../../authorization/alerting_authorization.mock';
|
||||
|
@ -22,7 +22,7 @@ import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/s
|
|||
import { fromKueryExpression } from '@kbn/es-query';
|
||||
import { auditLoggerMock } from '@kbn/security-plugin/server/audit/mocks';
|
||||
import { taskManagerMock } from '@kbn/task-manager-plugin/server/mocks';
|
||||
import { ConstructorOptions, RulesClient } from '../../../../rules_client';
|
||||
import { RulesClient } from '../../../../rules_client';
|
||||
import { adHocRunStatus } from '../../../../../common/constants';
|
||||
import { ConnectorAdapterRegistry } from '../../../../connector_adapters/connector_adapter_registry';
|
||||
import { SavedObject } from '@kbn/core/server';
|
||||
|
@ -142,35 +142,7 @@ const authDslFilter = {
|
|||
type: 'function',
|
||||
};
|
||||
|
||||
const rulesClientParams: jest.Mocked<ConstructorOptions> = {
|
||||
taskManager,
|
||||
ruleTypeRegistry,
|
||||
unsecuredSavedObjectsClient,
|
||||
authorization: authorization as unknown as AlertingAuthorization,
|
||||
actionsAuthorization: actionsAuthorization as unknown as ActionsAuthorization,
|
||||
spaceId: 'default',
|
||||
namespace: 'default',
|
||||
getUserName: jest.fn(),
|
||||
createAPIKey: jest.fn(),
|
||||
logger: loggingSystemMock.create().get(),
|
||||
internalSavedObjectsRepository,
|
||||
encryptedSavedObjectsClient: encryptedSavedObjects,
|
||||
getActionsClient: jest.fn(),
|
||||
getEventLogClient: jest.fn(),
|
||||
kibanaVersion,
|
||||
auditLogger,
|
||||
maxScheduledPerMinute: 10000,
|
||||
minimumScheduleInterval: { value: '1m', enforce: false },
|
||||
isAuthenticationTypeAPIKey: jest.fn(),
|
||||
getAuthenticationAPIKey: jest.fn(),
|
||||
getAlertIndicesAlias: jest.fn(),
|
||||
alertsService: null,
|
||||
backfillClient,
|
||||
isSystemAction: jest.fn(),
|
||||
connectorAdapterRegistry: new ConnectorAdapterRegistry(),
|
||||
uiSettings: uiSettingsServiceMock.createStartContract(),
|
||||
};
|
||||
|
||||
const mockActionsClient = actionsClientMock.create();
|
||||
const fakeRuleName = 'fakeRuleName';
|
||||
|
||||
const mockAdHocRunSO: SavedObject<AdHocRunSO> = {
|
||||
|
@ -186,7 +158,6 @@ const mockAdHocRunSO: SavedObject<AdHocRunSO> = {
|
|||
name: fakeRuleName,
|
||||
tags: ['foo'],
|
||||
alertTypeId: 'myType',
|
||||
// @ts-expect-error
|
||||
params: {},
|
||||
apiKeyOwner: 'user',
|
||||
apiKeyCreatedByUser: false,
|
||||
|
@ -195,6 +166,7 @@ const mockAdHocRunSO: SavedObject<AdHocRunSO> = {
|
|||
schedule: {
|
||||
interval: '12h',
|
||||
},
|
||||
actions: [],
|
||||
createdBy: 'user',
|
||||
updatedBy: 'user',
|
||||
createdAt: '2019-02-12T21:01:22.479Z',
|
||||
|
@ -222,10 +194,41 @@ const mockAdHocRunSO: SavedObject<AdHocRunSO> = {
|
|||
|
||||
describe('findBackfill()', () => {
|
||||
let rulesClient: RulesClient;
|
||||
let isSystemAction: jest.Mock;
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.resetAllMocks();
|
||||
rulesClient = new RulesClient(rulesClientParams);
|
||||
isSystemAction = jest.fn().mockReturnValue(false);
|
||||
mockActionsClient.isSystemAction.mockImplementation(isSystemAction);
|
||||
|
||||
rulesClient = new RulesClient({
|
||||
taskManager,
|
||||
ruleTypeRegistry,
|
||||
unsecuredSavedObjectsClient,
|
||||
authorization: authorization as unknown as AlertingAuthorization,
|
||||
actionsAuthorization: actionsAuthorization as unknown as ActionsAuthorization,
|
||||
spaceId: 'default',
|
||||
namespace: 'default',
|
||||
getUserName: jest.fn(),
|
||||
createAPIKey: jest.fn(),
|
||||
logger: loggingSystemMock.create().get(),
|
||||
internalSavedObjectsRepository,
|
||||
encryptedSavedObjectsClient: encryptedSavedObjects,
|
||||
getActionsClient: jest.fn().mockResolvedValue(mockActionsClient),
|
||||
getEventLogClient: jest.fn(),
|
||||
kibanaVersion,
|
||||
auditLogger,
|
||||
maxScheduledPerMinute: 10000,
|
||||
minimumScheduleInterval: { value: '1m', enforce: false },
|
||||
isAuthenticationTypeAPIKey: jest.fn(),
|
||||
getAuthenticationAPIKey: jest.fn(),
|
||||
getAlertIndicesAlias: jest.fn(),
|
||||
alertsService: null,
|
||||
backfillClient,
|
||||
isSystemAction: jest.fn(),
|
||||
connectorAdapterRegistry: new ConnectorAdapterRegistry(),
|
||||
uiSettings: uiSettingsServiceMock.createStartContract(),
|
||||
});
|
||||
authorization.getFindAuthorizationFilter.mockResolvedValue({
|
||||
filter,
|
||||
ensureRuleTypeIsAuthorized() {},
|
||||
|
@ -282,7 +285,7 @@ describe('findBackfill()', () => {
|
|||
page: 1,
|
||||
perPage: 10,
|
||||
total: 1,
|
||||
data: [transformAdHocRunToBackfillResult(mockAdHocRunSO)],
|
||||
data: [transformAdHocRunToBackfillResult({ adHocRunSO: mockAdHocRunSO, isSystemAction })],
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -331,7 +334,7 @@ describe('findBackfill()', () => {
|
|||
page: 1,
|
||||
perPage: 10,
|
||||
total: 1,
|
||||
data: [transformAdHocRunToBackfillResult(mockAdHocRunSO)],
|
||||
data: [transformAdHocRunToBackfillResult({ adHocRunSO: mockAdHocRunSO, isSystemAction })],
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -398,7 +401,7 @@ describe('findBackfill()', () => {
|
|||
page: 1,
|
||||
perPage: 10,
|
||||
total: 1,
|
||||
data: [transformAdHocRunToBackfillResult(mockAdHocRunSO)],
|
||||
data: [transformAdHocRunToBackfillResult({ adHocRunSO: mockAdHocRunSO, isSystemAction })],
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -465,7 +468,7 @@ describe('findBackfill()', () => {
|
|||
page: 1,
|
||||
perPage: 10,
|
||||
total: 1,
|
||||
data: [transformAdHocRunToBackfillResult(mockAdHocRunSO)],
|
||||
data: [transformAdHocRunToBackfillResult({ adHocRunSO: mockAdHocRunSO, isSystemAction })],
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -548,7 +551,7 @@ describe('findBackfill()', () => {
|
|||
page: 1,
|
||||
perPage: 10,
|
||||
total: 1,
|
||||
data: [transformAdHocRunToBackfillResult(mockAdHocRunSO)],
|
||||
data: [transformAdHocRunToBackfillResult({ adHocRunSO: mockAdHocRunSO, isSystemAction })],
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -633,7 +636,127 @@ describe('findBackfill()', () => {
|
|||
page: 1,
|
||||
perPage: 10,
|
||||
total: 1,
|
||||
data: [transformAdHocRunToBackfillResult(mockAdHocRunSO)],
|
||||
data: [transformAdHocRunToBackfillResult({ adHocRunSO: mockAdHocRunSO, isSystemAction })],
|
||||
});
|
||||
});
|
||||
|
||||
test('should successfully find backfill for rule with actions', async () => {
|
||||
const mockAdHocRunSOWithActions = {
|
||||
...mockAdHocRunSO,
|
||||
attributes: {
|
||||
...mockAdHocRunSO.attributes,
|
||||
rule: {
|
||||
...mockAdHocRunSO.attributes.rule,
|
||||
actions: [
|
||||
{
|
||||
uuid: '123abc',
|
||||
group: 'default',
|
||||
actionRef: 'action_0',
|
||||
actionTypeId: 'test',
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
references: [
|
||||
{ id: 'abc', name: 'rule', type: RULE_SAVED_OBJECT_TYPE },
|
||||
{ id: '4', name: 'action_0', type: 'action' },
|
||||
],
|
||||
score: 0,
|
||||
};
|
||||
unsecuredSavedObjectsClient.find.mockResolvedValue({
|
||||
saved_objects: [mockAdHocRunSOWithActions],
|
||||
per_page: 10,
|
||||
page: 1,
|
||||
total: 1,
|
||||
});
|
||||
|
||||
const result = await rulesClient.findBackfill({
|
||||
page: 1,
|
||||
perPage: 10,
|
||||
start: '2024-02-09T02:07:55Z',
|
||||
end: '2024-03-29T02:07:55Z',
|
||||
ruleIds: 'abc',
|
||||
});
|
||||
|
||||
expect(authorization.getFindAuthorizationFilter).toHaveBeenCalledWith({
|
||||
authorizationEntity: 'rule',
|
||||
filterOpts: {
|
||||
fieldNames: {
|
||||
consumer: 'ad_hoc_run_params.attributes.rule.consumer',
|
||||
ruleTypeId: 'ad_hoc_run_params.attributes.rule.alertTypeId',
|
||||
},
|
||||
type: 'kql',
|
||||
},
|
||||
});
|
||||
|
||||
expect(unsecuredSavedObjectsClient.find).toHaveBeenCalledWith({
|
||||
filter: {
|
||||
type: 'function',
|
||||
function: 'and',
|
||||
arguments: [
|
||||
{
|
||||
type: 'function',
|
||||
function: 'and',
|
||||
arguments: [
|
||||
{
|
||||
type: 'function',
|
||||
function: 'range',
|
||||
arguments: [
|
||||
{ isQuoted: false, type: 'literal', value: 'ad_hoc_run_params.attributes.start' },
|
||||
'gte',
|
||||
{ isQuoted: true, type: 'literal', value: '2024-02-09T02:07:55Z' },
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
function: 'range',
|
||||
arguments: [
|
||||
{ isQuoted: false, type: 'literal', value: 'ad_hoc_run_params.attributes.end' },
|
||||
'lte',
|
||||
{ isQuoted: true, type: 'literal', value: '2024-03-29T02:07:55Z' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
authDslFilter,
|
||||
],
|
||||
},
|
||||
hasReference: [{ id: 'abc', type: RULE_SAVED_OBJECT_TYPE }],
|
||||
page: 1,
|
||||
perPage: 10,
|
||||
type: AD_HOC_RUN_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
|
||||
expect(auditLogger.log).toHaveBeenCalledTimes(1);
|
||||
expect(auditLogger.log).toHaveBeenNthCalledWith(1, {
|
||||
event: {
|
||||
action: 'ad_hoc_run_find',
|
||||
category: ['database'],
|
||||
outcome: 'success',
|
||||
type: ['access'],
|
||||
},
|
||||
kibana: {
|
||||
saved_object: {
|
||||
id: '1',
|
||||
type: AD_HOC_RUN_SAVED_OBJECT_TYPE,
|
||||
name: 'backfill for rule "fakeRuleName"',
|
||||
},
|
||||
},
|
||||
message:
|
||||
'User has found ad hoc run for ad_hoc_run_params [id=1] backfill for rule "fakeRuleName"',
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
page: 1,
|
||||
perPage: 10,
|
||||
total: 1,
|
||||
data: [
|
||||
transformAdHocRunToBackfillResult({
|
||||
adHocRunSO: mockAdHocRunSOWithActions,
|
||||
isSystemAction,
|
||||
}),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -688,7 +811,7 @@ describe('findBackfill()', () => {
|
|||
page: 1,
|
||||
perPage: 10,
|
||||
total: 1,
|
||||
data: [transformAdHocRunToBackfillResult(mockAdHocRunSO)],
|
||||
data: [transformAdHocRunToBackfillResult({ adHocRunSO: mockAdHocRunSO, isSystemAction })],
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -98,6 +98,8 @@ export async function findBackfill(
|
|||
...(params.sortOrder ? { sortOrder: params.sortOrder } : {}),
|
||||
});
|
||||
|
||||
const actionsClient = await context.getActionsClient();
|
||||
|
||||
const transformedData: Backfill[] = data.map((so: SavedObject<AdHocRunSO>) => {
|
||||
context.auditLogger?.log(
|
||||
adHocRunAuditEvent({
|
||||
|
@ -110,7 +112,10 @@ export async function findBackfill(
|
|||
})
|
||||
);
|
||||
|
||||
return transformAdHocRunToBackfillResult(so) as Backfill;
|
||||
return transformAdHocRunToBackfillResult({
|
||||
adHocRunSO: so,
|
||||
isSystemAction: (id: string) => actionsClient.isSystemAction(id),
|
||||
}) as Backfill;
|
||||
});
|
||||
|
||||
return {
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { ActionsAuthorization } from '@kbn/actions-plugin/server';
|
||||
import { actionsAuthorizationMock } from '@kbn/actions-plugin/server/mocks';
|
||||
import { actionsAuthorizationMock, actionsClientMock } from '@kbn/actions-plugin/server/mocks';
|
||||
import { RULE_SAVED_OBJECT_TYPE } from '../../../..';
|
||||
import { AlertingAuthorization } from '../../../../authorization';
|
||||
import { alertingAuthorizationMock } from '../../../../authorization/alerting_authorization.mock';
|
||||
|
@ -21,7 +21,7 @@ import { uiSettingsServiceMock } from '@kbn/core-ui-settings-server-mocks';
|
|||
import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/server/mocks';
|
||||
import { auditLoggerMock } from '@kbn/security-plugin/server/audit/mocks';
|
||||
import { taskManagerMock } from '@kbn/task-manager-plugin/server/mocks';
|
||||
import { ConstructorOptions, RulesClient } from '../../../../rules_client';
|
||||
import { RulesClient } from '../../../../rules_client';
|
||||
import { adHocRunStatus } from '../../../../../common/constants';
|
||||
import { ConnectorAdapterRegistry } from '../../../../connector_adapters/connector_adapter_registry';
|
||||
import { AD_HOC_RUN_SAVED_OBJECT_TYPE } from '../../../../saved_objects';
|
||||
|
@ -41,37 +41,9 @@ const internalSavedObjectsRepository = savedObjectsRepositoryMock.create();
|
|||
const backfillClient = backfillClientMock.create();
|
||||
const logger = loggingSystemMock.create().get();
|
||||
|
||||
const rulesClientParams: jest.Mocked<ConstructorOptions> = {
|
||||
taskManager,
|
||||
ruleTypeRegistry,
|
||||
unsecuredSavedObjectsClient,
|
||||
authorization: authorization as unknown as AlertingAuthorization,
|
||||
actionsAuthorization: actionsAuthorization as unknown as ActionsAuthorization,
|
||||
spaceId: 'default',
|
||||
namespace: 'default',
|
||||
getUserName: jest.fn(),
|
||||
createAPIKey: jest.fn(),
|
||||
logger,
|
||||
internalSavedObjectsRepository,
|
||||
encryptedSavedObjectsClient: encryptedSavedObjects,
|
||||
getActionsClient: jest.fn(),
|
||||
getEventLogClient: jest.fn(),
|
||||
kibanaVersion,
|
||||
auditLogger,
|
||||
maxScheduledPerMinute: 10000,
|
||||
minimumScheduleInterval: { value: '1m', enforce: false },
|
||||
isAuthenticationTypeAPIKey: jest.fn(),
|
||||
getAuthenticationAPIKey: jest.fn(),
|
||||
getAlertIndicesAlias: jest.fn(),
|
||||
alertsService: null,
|
||||
backfillClient,
|
||||
isSystemAction: jest.fn(),
|
||||
connectorAdapterRegistry: new ConnectorAdapterRegistry(),
|
||||
uiSettings: uiSettingsServiceMock.createStartContract(),
|
||||
};
|
||||
|
||||
const fakeRuleName = 'fakeRuleName';
|
||||
|
||||
const mockActionsClient = actionsClientMock.create();
|
||||
const mockAdHocRunSO: SavedObject<AdHocRunSO> = {
|
||||
id: '1',
|
||||
type: AD_HOC_RUN_SAVED_OBJECT_TYPE,
|
||||
|
@ -85,7 +57,7 @@ const mockAdHocRunSO: SavedObject<AdHocRunSO> = {
|
|||
name: fakeRuleName,
|
||||
tags: ['foo'],
|
||||
alertTypeId: 'myType',
|
||||
// @ts-expect-error
|
||||
actions: [],
|
||||
params: {},
|
||||
apiKeyOwner: 'user',
|
||||
apiKeyCreatedByUser: false,
|
||||
|
@ -121,10 +93,41 @@ const mockAdHocRunSO: SavedObject<AdHocRunSO> = {
|
|||
|
||||
describe('getBackfill()', () => {
|
||||
let rulesClient: RulesClient;
|
||||
let isSystemAction: jest.Mock;
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.resetAllMocks();
|
||||
rulesClient = new RulesClient(rulesClientParams);
|
||||
isSystemAction = jest.fn().mockReturnValue(false);
|
||||
mockActionsClient.isSystemAction.mockImplementation(isSystemAction);
|
||||
|
||||
rulesClient = new RulesClient({
|
||||
taskManager,
|
||||
ruleTypeRegistry,
|
||||
unsecuredSavedObjectsClient,
|
||||
authorization: authorization as unknown as AlertingAuthorization,
|
||||
actionsAuthorization: actionsAuthorization as unknown as ActionsAuthorization,
|
||||
spaceId: 'default',
|
||||
namespace: 'default',
|
||||
getUserName: jest.fn(),
|
||||
createAPIKey: jest.fn(),
|
||||
logger,
|
||||
internalSavedObjectsRepository,
|
||||
encryptedSavedObjectsClient: encryptedSavedObjects,
|
||||
getActionsClient: jest.fn().mockResolvedValue(mockActionsClient),
|
||||
getEventLogClient: jest.fn(),
|
||||
kibanaVersion,
|
||||
auditLogger,
|
||||
maxScheduledPerMinute: 10000,
|
||||
minimumScheduleInterval: { value: '1m', enforce: false },
|
||||
isAuthenticationTypeAPIKey: jest.fn(),
|
||||
getAuthenticationAPIKey: jest.fn(),
|
||||
getAlertIndicesAlias: jest.fn(),
|
||||
alertsService: null,
|
||||
backfillClient,
|
||||
isSystemAction: jest.fn(),
|
||||
connectorAdapterRegistry: new ConnectorAdapterRegistry(),
|
||||
uiSettings: uiSettingsServiceMock.createStartContract(),
|
||||
});
|
||||
unsecuredSavedObjectsClient.get.mockResolvedValue(mockAdHocRunSO);
|
||||
});
|
||||
|
||||
|
@ -158,7 +161,67 @@ describe('getBackfill()', () => {
|
|||
});
|
||||
expect(logger.error).not.toHaveBeenCalled();
|
||||
|
||||
expect(result).toEqual(transformAdHocRunToBackfillResult(mockAdHocRunSO));
|
||||
expect(result).toEqual(
|
||||
transformAdHocRunToBackfillResult({ adHocRunSO: mockAdHocRunSO, isSystemAction })
|
||||
);
|
||||
});
|
||||
|
||||
test('should successfully get backfill with actions', async () => {
|
||||
const mockAdHocRunSOWithActions = {
|
||||
...mockAdHocRunSO,
|
||||
attributes: {
|
||||
...mockAdHocRunSO.attributes,
|
||||
rule: {
|
||||
...mockAdHocRunSO.attributes.rule,
|
||||
actions: [
|
||||
{
|
||||
uuid: '123abc',
|
||||
group: 'default',
|
||||
actionRef: 'action_0',
|
||||
actionTypeId: 'test',
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
references: [
|
||||
{ id: 'abc', name: 'rule', type: RULE_SAVED_OBJECT_TYPE },
|
||||
{ id: '4', name: 'action_0', type: 'action' },
|
||||
],
|
||||
};
|
||||
unsecuredSavedObjectsClient.get.mockResolvedValue(mockAdHocRunSOWithActions);
|
||||
const result = await rulesClient.getBackfill('1');
|
||||
|
||||
expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledWith(AD_HOC_RUN_SAVED_OBJECT_TYPE, '1');
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith({
|
||||
entity: 'rule',
|
||||
consumer: 'myApp',
|
||||
operation: 'getBackfill',
|
||||
ruleTypeId: 'myType',
|
||||
});
|
||||
expect(auditLogger.log).toHaveBeenCalledTimes(1);
|
||||
expect(auditLogger.log).toHaveBeenCalledWith({
|
||||
event: {
|
||||
action: 'ad_hoc_run_get',
|
||||
category: ['database'],
|
||||
outcome: 'success',
|
||||
type: ['access'],
|
||||
},
|
||||
kibana: {
|
||||
saved_object: {
|
||||
id: '1',
|
||||
type: AD_HOC_RUN_SAVED_OBJECT_TYPE,
|
||||
name: `backfill for rule "fakeRuleName"`,
|
||||
},
|
||||
},
|
||||
message:
|
||||
'User has got ad hoc run for ad_hoc_run_params [id=1] backfill for rule "fakeRuleName"',
|
||||
});
|
||||
expect(logger.error).not.toHaveBeenCalled();
|
||||
|
||||
expect(result).toEqual(
|
||||
transformAdHocRunToBackfillResult({ adHocRunSO: mockAdHocRunSOWithActions, isSystemAction })
|
||||
);
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
|
|
|
@ -73,7 +73,11 @@ export async function getBackfill(context: RulesClientContext, id: string): Prom
|
|||
})
|
||||
);
|
||||
|
||||
return transformAdHocRunToBackfillResult(result) as Backfill;
|
||||
const actionsClient = await context.getActionsClient();
|
||||
return transformAdHocRunToBackfillResult({
|
||||
adHocRunSO: result,
|
||||
isSystemAction: (connectorId: string) => actionsClient.isSystemAction(connectorId),
|
||||
}) as Backfill;
|
||||
} catch (err) {
|
||||
const errorMessage = `Failed to get backfill by id: ${id}`;
|
||||
context.logger.error(`${errorMessage} - ${err}`);
|
||||
|
|
|
@ -180,6 +180,7 @@ function getMockData(overwrites: Record<string, unknown> = {}): ScheduleBackfill
|
|||
return {
|
||||
ruleId: '1',
|
||||
start: '2023-11-16T08:00:00.000Z',
|
||||
runActions: true,
|
||||
...overwrites,
|
||||
};
|
||||
}
|
||||
|
@ -481,13 +482,13 @@ describe('scheduleBackfill()', () => {
|
|||
// @ts-expect-error
|
||||
rulesClient.scheduleBackfill(getMockData())
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"Error validating backfill schedule parameters \\"{\\"ruleId\\":\\"1\\",\\"start\\":\\"2023-11-16T08:00:00.000Z\\"}\\" - expected value of type [array] but got [Object]"`
|
||||
`"Error validating backfill schedule parameters \\"{\\"ruleId\\":\\"1\\",\\"start\\":\\"2023-11-16T08:00:00.000Z\\",\\"runActions\\":true}\\" - expected value of type [array] but got [Object]"`
|
||||
);
|
||||
|
||||
await expect(
|
||||
rulesClient.scheduleBackfill([getMockData({ ruleId: 1 })])
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"Error validating backfill schedule parameters \\"[{\\"ruleId\\":1,\\"start\\":\\"2023-11-16T08:00:00.000Z\\"}]\\" - [0.ruleId]: expected value of type [string] but got [number]"`
|
||||
`"Error validating backfill schedule parameters \\"[{\\"ruleId\\":1,\\"start\\":\\"2023-11-16T08:00:00.000Z\\",\\"runActions\\":true}]\\" - [0.ruleId]: expected value of type [string] but got [number]"`
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -502,7 +503,7 @@ describe('scheduleBackfill()', () => {
|
|||
}),
|
||||
])
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"Error validating backfill schedule parameters \\"[{\\"ruleId\\":\\"1\\",\\"start\\":\\"2023-11-16T08:00:00.000Z\\"},{\\"ruleId\\":\\"2\\",\\"start\\":\\"2023-11-17T08:00:00.000Z\\",\\"end\\":\\"2023-11-17T08:00:00.000Z\\"}]\\" - [1]: Backfill end must be greater than backfill start"`
|
||||
`"Error validating backfill schedule parameters \\"[{\\"ruleId\\":\\"1\\",\\"start\\":\\"2023-11-16T08:00:00.000Z\\",\\"runActions\\":true},{\\"ruleId\\":\\"2\\",\\"start\\":\\"2023-11-17T08:00:00.000Z\\",\\"runActions\\":true,\\"end\\":\\"2023-11-17T08:00:00.000Z\\"}]\\" - [1]: Backfill end must be greater than backfill start"`
|
||||
);
|
||||
|
||||
await expect(
|
||||
|
@ -515,7 +516,7 @@ describe('scheduleBackfill()', () => {
|
|||
}),
|
||||
])
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"Error validating backfill schedule parameters \\"[{\\"ruleId\\":\\"1\\",\\"start\\":\\"2023-11-16T08:00:00.000Z\\"},{\\"ruleId\\":\\"2\\",\\"start\\":\\"2023-11-17T08:00:00.000Z\\",\\"end\\":\\"2023-11-16T08:00:00.000Z\\"}]\\" - [1]: Backfill end must be greater than backfill start"`
|
||||
`"Error validating backfill schedule parameters \\"[{\\"ruleId\\":\\"1\\",\\"start\\":\\"2023-11-16T08:00:00.000Z\\",\\"runActions\\":true},{\\"ruleId\\":\\"2\\",\\"start\\":\\"2023-11-17T08:00:00.000Z\\",\\"runActions\\":true,\\"end\\":\\"2023-11-16T08:00:00.000Z\\"}]\\" - [1]: Backfill end must be greater than backfill start"`
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -145,6 +145,7 @@ export async function scheduleBackfill(
|
|||
|
||||
const actionsClient = await context.getActionsClient();
|
||||
return await context.backfillClient.bulkQueue({
|
||||
actionsClient,
|
||||
auditLogger: context.auditLogger,
|
||||
params,
|
||||
rules: rulesToSchedule.map(({ id, attributes, references }) => {
|
||||
|
|
|
@ -14,6 +14,7 @@ export const scheduleBackfillParamSchema = schema.object(
|
|||
ruleId: schema.string(),
|
||||
start: schema.string(),
|
||||
end: schema.maybe(schema.string()),
|
||||
runActions: schema.maybe(schema.boolean()),
|
||||
},
|
||||
{
|
||||
validate({ start, end }) {
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import { schema } from '@kbn/config-schema';
|
||||
import { ruleParamsSchema } from '@kbn/response-ops-rule-params';
|
||||
import { adHocRunStatus } from '../../../../../common/constants';
|
||||
import { actionSchema as ruleActionSchema } from '../../../rule/schemas/action_schemas';
|
||||
|
||||
export const statusSchema = schema.oneOf([
|
||||
schema.literal(adHocRunStatus.COMPLETE),
|
||||
|
@ -32,6 +33,7 @@ export const backfillSchema = schema.object({
|
|||
id: schema.string(),
|
||||
name: schema.string(),
|
||||
tags: schema.arrayOf(schema.string()),
|
||||
actions: schema.arrayOf(ruleActionSchema),
|
||||
alertTypeId: schema.string(),
|
||||
params: ruleParamsSchema,
|
||||
apiKeyOwner: schema.nullable(schema.string()),
|
||||
|
@ -50,4 +52,5 @@ export const backfillSchema = schema.object({
|
|||
status: statusSchema,
|
||||
end: schema.maybe(schema.string()),
|
||||
schedule: schema.arrayOf(backfillScheduleSchema),
|
||||
warnings: schema.maybe(schema.arrayOf(schema.string())),
|
||||
});
|
||||
|
|
|
@ -8,17 +8,24 @@
|
|||
import { AdHocRunSO } from '../../../data/ad_hoc_run/types';
|
||||
import { SavedObject } from '@kbn/core/server';
|
||||
import { adHocRunStatus } from '../../../../common/constants';
|
||||
import { transformAdHocRunToBackfillResult } from './transform_ad_hoc_run_to_backfill_result';
|
||||
import {
|
||||
transformAdHocRunToAdHocRunData,
|
||||
transformAdHocRunToBackfillResult,
|
||||
} from './transform_ad_hoc_run_to_backfill_result';
|
||||
import { RawRule } from '../../../types';
|
||||
|
||||
const isSystemAction = jest.fn().mockReturnValue(false);
|
||||
|
||||
function getMockAdHocRunAttributes({
|
||||
ruleId,
|
||||
overwrites,
|
||||
omitApiKey = false,
|
||||
actions,
|
||||
}: {
|
||||
ruleId?: string;
|
||||
overwrites?: Record<string, unknown>;
|
||||
omitApiKey?: boolean;
|
||||
actions?: RawRule['actions'];
|
||||
} = {}): AdHocRunSO {
|
||||
// @ts-expect-error
|
||||
return {
|
||||
...(omitApiKey ? {} : { apiKeyId: '123', apiKeyToUse: 'MTIzOmFiYw==' }),
|
||||
createdAt: '2024-01-30T00:00:00.000Z',
|
||||
|
@ -29,7 +36,7 @@ function getMockAdHocRunAttributes({
|
|||
name: 'my rule name',
|
||||
tags: ['foo'],
|
||||
alertTypeId: 'myType',
|
||||
// @ts-expect-error
|
||||
actions: actions ? actions : [],
|
||||
params: {},
|
||||
apiKeyOwner: 'user',
|
||||
apiKeyCreatedByUser: false,
|
||||
|
@ -59,14 +66,14 @@ function getMockAdHocRunAttributes({
|
|||
runAt: '2023-10-20T15:07:40.011Z',
|
||||
},
|
||||
],
|
||||
...overwrites,
|
||||
};
|
||||
}
|
||||
|
||||
function getBulkCreateResponse(
|
||||
id: string,
|
||||
ruleId: string,
|
||||
attributes: AdHocRunSO
|
||||
attributes: AdHocRunSO,
|
||||
additionalReferences?: Array<{ id: string; name: string; type: string }>
|
||||
): SavedObject<AdHocRunSO> {
|
||||
return {
|
||||
type: 'ad_hoc_rule_run_params',
|
||||
|
@ -79,6 +86,7 @@ function getBulkCreateResponse(
|
|||
name: 'rule',
|
||||
type: 'alert',
|
||||
},
|
||||
...(additionalReferences ?? []),
|
||||
],
|
||||
managed: false,
|
||||
coreMigrationVersion: '8.8.0',
|
||||
|
@ -91,9 +99,10 @@ function getBulkCreateResponse(
|
|||
describe('transformAdHocRunToBackfillResult', () => {
|
||||
test('should transform bulk create response', () => {
|
||||
expect(
|
||||
transformAdHocRunToBackfillResult(
|
||||
getBulkCreateResponse('abc', '1', getMockAdHocRunAttributes())
|
||||
)
|
||||
transformAdHocRunToBackfillResult({
|
||||
adHocRunSO: getBulkCreateResponse('abc', '1', getMockAdHocRunAttributes()),
|
||||
isSystemAction,
|
||||
})
|
||||
).toEqual({
|
||||
id: 'abc',
|
||||
createdAt: '2024-01-30T00:00:00.000Z',
|
||||
|
@ -103,6 +112,71 @@ describe('transformAdHocRunToBackfillResult', () => {
|
|||
id: '1',
|
||||
name: 'my rule name',
|
||||
tags: ['foo'],
|
||||
actions: [],
|
||||
alertTypeId: 'myType',
|
||||
params: {},
|
||||
apiKeyOwner: 'user',
|
||||
apiKeyCreatedByUser: false,
|
||||
consumer: 'myApp',
|
||||
enabled: true,
|
||||
schedule: {
|
||||
interval: '12h',
|
||||
},
|
||||
createdBy: 'user',
|
||||
updatedBy: 'user',
|
||||
createdAt: '2019-02-12T21:01:22.479Z',
|
||||
updatedAt: '2019-02-12T21:01:22.479Z',
|
||||
revision: 0,
|
||||
},
|
||||
spaceId: 'default',
|
||||
start: '2023-10-19T15:07:40.011Z',
|
||||
status: adHocRunStatus.PENDING,
|
||||
schedule: [
|
||||
{
|
||||
interval: '12h',
|
||||
status: adHocRunStatus.PENDING,
|
||||
runAt: '2023-10-20T03:07:40.011Z',
|
||||
},
|
||||
{
|
||||
interval: '12h',
|
||||
status: adHocRunStatus.PENDING,
|
||||
runAt: '2023-10-20T15:07:40.011Z',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('should transform bulk create response with actions', () => {
|
||||
expect(
|
||||
transformAdHocRunToBackfillResult({
|
||||
adHocRunSO: getBulkCreateResponse(
|
||||
'abc',
|
||||
'1',
|
||||
getMockAdHocRunAttributes({
|
||||
actions: [
|
||||
{
|
||||
uuid: '123abc',
|
||||
group: 'default',
|
||||
actionRef: 'action_0',
|
||||
actionTypeId: 'test',
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
}),
|
||||
[{ id: '4', name: 'action_0', type: 'action' }]
|
||||
),
|
||||
isSystemAction,
|
||||
})
|
||||
).toEqual({
|
||||
id: 'abc',
|
||||
createdAt: '2024-01-30T00:00:00.000Z',
|
||||
duration: '12h',
|
||||
enabled: true,
|
||||
rule: {
|
||||
id: '1',
|
||||
name: 'my rule name',
|
||||
tags: ['foo'],
|
||||
actions: [{ actionTypeId: 'test', group: 'default', id: '4', params: {}, uuid: '123abc' }],
|
||||
alertTypeId: 'myType',
|
||||
params: {},
|
||||
apiKeyOwner: 'user',
|
||||
|
@ -138,10 +212,10 @@ describe('transformAdHocRunToBackfillResult', () => {
|
|||
|
||||
test('should return error for malformed responses when original create request is not provided', () => {
|
||||
expect(
|
||||
transformAdHocRunToBackfillResult(
|
||||
transformAdHocRunToBackfillResult({
|
||||
// missing id
|
||||
// @ts-expect-error
|
||||
{
|
||||
adHocRunSO: {
|
||||
type: 'ad_hoc_rule_run_params',
|
||||
namespaces: ['default'],
|
||||
attributes: getMockAdHocRunAttributes(),
|
||||
|
@ -151,8 +225,9 @@ describe('transformAdHocRunToBackfillResult', () => {
|
|||
updated_at: '2024-02-07T16:05:39.296Z',
|
||||
created_at: '2024-02-07T16:05:39.296Z',
|
||||
version: 'WzcsMV0=',
|
||||
}
|
||||
)
|
||||
},
|
||||
isSystemAction,
|
||||
})
|
||||
).toEqual({
|
||||
error: {
|
||||
message: 'Malformed saved object in bulkCreate response - Missing "id".',
|
||||
|
@ -160,10 +235,10 @@ describe('transformAdHocRunToBackfillResult', () => {
|
|||
},
|
||||
});
|
||||
expect(
|
||||
transformAdHocRunToBackfillResult(
|
||||
transformAdHocRunToBackfillResult({
|
||||
// missing attributes
|
||||
// @ts-expect-error
|
||||
{
|
||||
adHocRunSO: {
|
||||
type: 'ad_hoc_rule_run_params',
|
||||
id: 'abc',
|
||||
namespaces: ['default'],
|
||||
|
@ -173,8 +248,9 @@ describe('transformAdHocRunToBackfillResult', () => {
|
|||
updated_at: '2024-02-07T16:05:39.296Z',
|
||||
created_at: '2024-02-07T16:05:39.296Z',
|
||||
version: 'WzcsMV0=',
|
||||
}
|
||||
)
|
||||
},
|
||||
isSystemAction,
|
||||
})
|
||||
).toEqual({
|
||||
error: {
|
||||
message: 'Malformed saved object in bulkCreate response - Missing "attributes".',
|
||||
|
@ -182,10 +258,10 @@ describe('transformAdHocRunToBackfillResult', () => {
|
|||
},
|
||||
});
|
||||
expect(
|
||||
transformAdHocRunToBackfillResult(
|
||||
transformAdHocRunToBackfillResult({
|
||||
// missing references
|
||||
// @ts-expect-error
|
||||
{
|
||||
adHocRunSO: {
|
||||
type: 'ad_hoc_rule_run_params',
|
||||
id: 'def',
|
||||
namespaces: ['default'],
|
||||
|
@ -195,8 +271,9 @@ describe('transformAdHocRunToBackfillResult', () => {
|
|||
updated_at: '2024-02-07T16:05:39.296Z',
|
||||
created_at: '2024-02-07T16:05:39.296Z',
|
||||
version: 'WzcsMV0=',
|
||||
}
|
||||
)
|
||||
},
|
||||
isSystemAction,
|
||||
})
|
||||
).toEqual({
|
||||
error: {
|
||||
message: 'Malformed saved object in bulkCreate response - Missing "references".',
|
||||
|
@ -204,9 +281,9 @@ describe('transformAdHocRunToBackfillResult', () => {
|
|||
},
|
||||
});
|
||||
expect(
|
||||
transformAdHocRunToBackfillResult(
|
||||
transformAdHocRunToBackfillResult({
|
||||
// empty references
|
||||
{
|
||||
adHocRunSO: {
|
||||
type: 'ad_hoc_rule_run_params',
|
||||
id: 'ghi',
|
||||
namespaces: ['default'],
|
||||
|
@ -217,8 +294,9 @@ describe('transformAdHocRunToBackfillResult', () => {
|
|||
updated_at: '2024-02-07T16:05:39.296Z',
|
||||
created_at: '2024-02-07T16:05:39.296Z',
|
||||
version: 'WzcsMV0=',
|
||||
}
|
||||
)
|
||||
},
|
||||
isSystemAction,
|
||||
})
|
||||
).toEqual({
|
||||
error: {
|
||||
message: 'Malformed saved object in bulkCreate response - Missing "references".',
|
||||
|
@ -230,10 +308,10 @@ describe('transformAdHocRunToBackfillResult', () => {
|
|||
test('should return error for malformed responses when original create request is provided', () => {
|
||||
const attributes = getMockAdHocRunAttributes();
|
||||
expect(
|
||||
transformAdHocRunToBackfillResult(
|
||||
transformAdHocRunToBackfillResult({
|
||||
// missing id
|
||||
// @ts-expect-error
|
||||
{
|
||||
adHocRunSO: {
|
||||
type: 'ad_hoc_rule_run_params',
|
||||
namespaces: ['default'],
|
||||
attributes,
|
||||
|
@ -244,12 +322,13 @@ describe('transformAdHocRunToBackfillResult', () => {
|
|||
created_at: '2024-02-07T16:05:39.296Z',
|
||||
version: 'WzcsMV0=',
|
||||
},
|
||||
{
|
||||
originalSO: {
|
||||
type: 'ad_hoc_rule_run_params',
|
||||
attributes,
|
||||
references: [{ id: '1', name: 'rule', type: 'alert' }],
|
||||
}
|
||||
)
|
||||
},
|
||||
isSystemAction,
|
||||
})
|
||||
).toEqual({
|
||||
error: {
|
||||
message: 'Malformed saved object in bulkCreate response - Missing "id".',
|
||||
|
@ -257,10 +336,10 @@ describe('transformAdHocRunToBackfillResult', () => {
|
|||
},
|
||||
});
|
||||
expect(
|
||||
transformAdHocRunToBackfillResult(
|
||||
transformAdHocRunToBackfillResult({
|
||||
// missing attributes
|
||||
// @ts-expect-error
|
||||
{
|
||||
adHocRunSO: {
|
||||
type: 'ad_hoc_rule_run_params',
|
||||
id: 'abc',
|
||||
namespaces: ['default'],
|
||||
|
@ -271,12 +350,13 @@ describe('transformAdHocRunToBackfillResult', () => {
|
|||
created_at: '2024-02-07T16:05:39.296Z',
|
||||
version: 'WzcsMV0=',
|
||||
},
|
||||
{
|
||||
originalSO: {
|
||||
type: 'ad_hoc_rule_run_params',
|
||||
attributes,
|
||||
references: [{ id: '1', name: 'rule', type: 'alert' }],
|
||||
}
|
||||
)
|
||||
},
|
||||
isSystemAction,
|
||||
})
|
||||
).toEqual({
|
||||
error: {
|
||||
message: 'Malformed saved object in bulkCreate response - Missing "attributes".',
|
||||
|
@ -284,10 +364,10 @@ describe('transformAdHocRunToBackfillResult', () => {
|
|||
},
|
||||
});
|
||||
expect(
|
||||
transformAdHocRunToBackfillResult(
|
||||
transformAdHocRunToBackfillResult({
|
||||
// missing references
|
||||
// @ts-expect-error
|
||||
{
|
||||
adHocRunSO: {
|
||||
type: 'ad_hoc_rule_run_params',
|
||||
id: 'def',
|
||||
namespaces: ['default'],
|
||||
|
@ -298,12 +378,13 @@ describe('transformAdHocRunToBackfillResult', () => {
|
|||
created_at: '2024-02-07T16:05:39.296Z',
|
||||
version: 'WzcsMV0=',
|
||||
},
|
||||
{
|
||||
originalSO: {
|
||||
type: 'ad_hoc_rule_run_params',
|
||||
attributes,
|
||||
references: [{ id: '1', name: 'rule', type: 'alert' }],
|
||||
}
|
||||
)
|
||||
},
|
||||
isSystemAction,
|
||||
})
|
||||
).toEqual({
|
||||
error: {
|
||||
message: 'Malformed saved object in bulkCreate response - Missing "references".',
|
||||
|
@ -311,9 +392,9 @@ describe('transformAdHocRunToBackfillResult', () => {
|
|||
},
|
||||
});
|
||||
expect(
|
||||
transformAdHocRunToBackfillResult(
|
||||
transformAdHocRunToBackfillResult({
|
||||
// empty references
|
||||
{
|
||||
adHocRunSO: {
|
||||
type: 'ad_hoc_rule_run_params',
|
||||
id: 'ghi',
|
||||
namespaces: ['default'],
|
||||
|
@ -325,12 +406,13 @@ describe('transformAdHocRunToBackfillResult', () => {
|
|||
created_at: '2024-02-07T16:05:39.296Z',
|
||||
version: 'WzcsMV0=',
|
||||
},
|
||||
{
|
||||
originalSO: {
|
||||
type: 'ad_hoc_rule_run_params',
|
||||
attributes,
|
||||
references: [{ id: '1', name: 'rule', type: 'alert' }],
|
||||
}
|
||||
)
|
||||
},
|
||||
isSystemAction,
|
||||
})
|
||||
).toEqual({
|
||||
error: {
|
||||
message: 'Malformed saved object in bulkCreate response - Missing "references".',
|
||||
|
@ -341,9 +423,9 @@ describe('transformAdHocRunToBackfillResult', () => {
|
|||
|
||||
test('should pass through error if saved object error when original create request is not provided', () => {
|
||||
expect(
|
||||
transformAdHocRunToBackfillResult(
|
||||
transformAdHocRunToBackfillResult({
|
||||
// @ts-expect-error
|
||||
{
|
||||
adHocRunSO: {
|
||||
type: 'ad_hoc_rule_run_params',
|
||||
id: '788a2784-c021-484f-a53e-0c1c63c7567c',
|
||||
error: {
|
||||
|
@ -351,8 +433,9 @@ describe('transformAdHocRunToBackfillResult', () => {
|
|||
message: 'Unable to create',
|
||||
statusCode: 404,
|
||||
},
|
||||
}
|
||||
)
|
||||
},
|
||||
isSystemAction,
|
||||
})
|
||||
).toEqual({
|
||||
error: {
|
||||
message: 'Unable to create',
|
||||
|
@ -363,9 +446,9 @@ describe('transformAdHocRunToBackfillResult', () => {
|
|||
|
||||
test('should pass through error if saved object error when original create request is provided', () => {
|
||||
expect(
|
||||
transformAdHocRunToBackfillResult(
|
||||
transformAdHocRunToBackfillResult({
|
||||
// @ts-expect-error
|
||||
{
|
||||
adHocRunSO: {
|
||||
type: 'ad_hoc_rule_run_params',
|
||||
id: '788a2784-c021-484f-a53e-0c1c63c7567c',
|
||||
error: {
|
||||
|
@ -374,12 +457,13 @@ describe('transformAdHocRunToBackfillResult', () => {
|
|||
statusCode: 404,
|
||||
},
|
||||
},
|
||||
{
|
||||
originalSO: {
|
||||
type: 'ad_hoc_rule_run_params',
|
||||
attributes: getMockAdHocRunAttributes(),
|
||||
references: [{ id: '1', name: 'rule', type: 'alert' }],
|
||||
}
|
||||
)
|
||||
},
|
||||
isSystemAction,
|
||||
})
|
||||
).toEqual({
|
||||
error: {
|
||||
message: 'Unable to create',
|
||||
|
@ -388,3 +472,122 @@ describe('transformAdHocRunToBackfillResult', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('transformAdHocRunToAdHocRunData', () => {
|
||||
test('should transform bulk create response and include api key', () => {
|
||||
expect(
|
||||
transformAdHocRunToAdHocRunData({
|
||||
adHocRunSO: getBulkCreateResponse('abc', '1', getMockAdHocRunAttributes()),
|
||||
isSystemAction,
|
||||
})
|
||||
).toEqual({
|
||||
id: 'abc',
|
||||
apiKeyId: '123',
|
||||
apiKeyToUse: 'MTIzOmFiYw==',
|
||||
createdAt: '2024-01-30T00:00:00.000Z',
|
||||
duration: '12h',
|
||||
enabled: true,
|
||||
rule: {
|
||||
id: '1',
|
||||
name: 'my rule name',
|
||||
tags: ['foo'],
|
||||
actions: [],
|
||||
alertTypeId: 'myType',
|
||||
params: {},
|
||||
apiKeyOwner: 'user',
|
||||
apiKeyCreatedByUser: false,
|
||||
consumer: 'myApp',
|
||||
enabled: true,
|
||||
schedule: {
|
||||
interval: '12h',
|
||||
},
|
||||
createdBy: 'user',
|
||||
updatedBy: 'user',
|
||||
createdAt: '2019-02-12T21:01:22.479Z',
|
||||
updatedAt: '2019-02-12T21:01:22.479Z',
|
||||
revision: 0,
|
||||
},
|
||||
spaceId: 'default',
|
||||
start: '2023-10-19T15:07:40.011Z',
|
||||
status: adHocRunStatus.PENDING,
|
||||
schedule: [
|
||||
{
|
||||
interval: '12h',
|
||||
status: adHocRunStatus.PENDING,
|
||||
runAt: '2023-10-20T03:07:40.011Z',
|
||||
},
|
||||
{
|
||||
interval: '12h',
|
||||
status: adHocRunStatus.PENDING,
|
||||
runAt: '2023-10-20T15:07:40.011Z',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('should transform bulk create response with actions and include api key', () => {
|
||||
expect(
|
||||
transformAdHocRunToAdHocRunData({
|
||||
adHocRunSO: getBulkCreateResponse(
|
||||
'abc',
|
||||
'1',
|
||||
getMockAdHocRunAttributes({
|
||||
actions: [
|
||||
{
|
||||
uuid: '123abc',
|
||||
group: 'default',
|
||||
actionRef: 'action_0',
|
||||
actionTypeId: 'test',
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
}),
|
||||
[{ id: '4', name: 'action_0', type: 'action' }]
|
||||
),
|
||||
isSystemAction,
|
||||
})
|
||||
).toEqual({
|
||||
id: 'abc',
|
||||
apiKeyId: '123',
|
||||
apiKeyToUse: 'MTIzOmFiYw==',
|
||||
createdAt: '2024-01-30T00:00:00.000Z',
|
||||
duration: '12h',
|
||||
enabled: true,
|
||||
rule: {
|
||||
id: '1',
|
||||
name: 'my rule name',
|
||||
tags: ['foo'],
|
||||
actions: [{ actionTypeId: 'test', group: 'default', id: '4', params: {}, uuid: '123abc' }],
|
||||
alertTypeId: 'myType',
|
||||
params: {},
|
||||
apiKeyOwner: 'user',
|
||||
apiKeyCreatedByUser: false,
|
||||
consumer: 'myApp',
|
||||
enabled: true,
|
||||
schedule: {
|
||||
interval: '12h',
|
||||
},
|
||||
createdBy: 'user',
|
||||
updatedBy: 'user',
|
||||
createdAt: '2019-02-12T21:01:22.479Z',
|
||||
updatedAt: '2019-02-12T21:01:22.479Z',
|
||||
revision: 0,
|
||||
},
|
||||
spaceId: 'default',
|
||||
start: '2023-10-19T15:07:40.011Z',
|
||||
status: adHocRunStatus.PENDING,
|
||||
schedule: [
|
||||
{
|
||||
interval: '12h',
|
||||
status: adHocRunStatus.PENDING,
|
||||
runAt: '2023-10-20T03:07:40.011Z',
|
||||
},
|
||||
{
|
||||
interval: '12h',
|
||||
status: adHocRunStatus.PENDING,
|
||||
runAt: '2023-10-20T15:07:40.011Z',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,14 +6,25 @@
|
|||
*/
|
||||
|
||||
import { SavedObject, SavedObjectsBulkCreateObject } from '@kbn/core/server';
|
||||
import { AdHocRunSO } from '../../../data/ad_hoc_run/types';
|
||||
import { AdHocRun, AdHocRunSO } from '../../../data/ad_hoc_run/types';
|
||||
import { createBackfillError } from '../../../backfill_client/lib';
|
||||
import { ScheduleBackfillResult } from '../methods/schedule/types';
|
||||
import { transformRawActionsToDomainActions } from '../../rule/transforms';
|
||||
|
||||
export const transformAdHocRunToBackfillResult = (
|
||||
{ id, attributes, references, error }: SavedObject<AdHocRunSO>,
|
||||
originalSO?: SavedObjectsBulkCreateObject<AdHocRunSO>
|
||||
): ScheduleBackfillResult => {
|
||||
interface TransformAdHocRunToBackfillResultOpts {
|
||||
adHocRunSO: SavedObject<AdHocRunSO>;
|
||||
isSystemAction: (connectorId: string) => boolean;
|
||||
originalSO?: SavedObjectsBulkCreateObject<AdHocRunSO>;
|
||||
omitGeneratedActionValues?: boolean;
|
||||
}
|
||||
|
||||
export const transformAdHocRunToBackfillResult = ({
|
||||
adHocRunSO,
|
||||
isSystemAction,
|
||||
originalSO,
|
||||
omitGeneratedActionValues = true,
|
||||
}: TransformAdHocRunToBackfillResultOpts): ScheduleBackfillResult => {
|
||||
const { id, attributes, references, error } = adHocRunSO;
|
||||
const ruleId = references?.[0]?.id ?? originalSO?.references?.[0]?.id ?? 'unknown';
|
||||
const ruleName = attributes?.rule?.name ?? originalSO?.attributes?.rule.name;
|
||||
if (error) {
|
||||
|
@ -55,6 +66,13 @@ export const transformAdHocRunToBackfillResult = (
|
|||
rule: {
|
||||
...attributes.rule,
|
||||
id: references[0].id,
|
||||
actions: transformRawActionsToDomainActions({
|
||||
ruleId: id,
|
||||
actions: attributes.rule.actions,
|
||||
references,
|
||||
isSystemAction,
|
||||
omitGeneratedValues: omitGeneratedActionValues,
|
||||
}),
|
||||
},
|
||||
spaceId: attributes.spaceId,
|
||||
start: attributes.start,
|
||||
|
@ -62,3 +80,24 @@ export const transformAdHocRunToBackfillResult = (
|
|||
schedule: attributes.schedule,
|
||||
};
|
||||
};
|
||||
|
||||
// includes API key information
|
||||
export const transformAdHocRunToAdHocRunData = ({
|
||||
adHocRunSO,
|
||||
isSystemAction,
|
||||
originalSO,
|
||||
omitGeneratedActionValues = true,
|
||||
}: TransformAdHocRunToBackfillResultOpts): AdHocRun => {
|
||||
const result = transformAdHocRunToBackfillResult({
|
||||
adHocRunSO,
|
||||
isSystemAction,
|
||||
originalSO,
|
||||
omitGeneratedActionValues,
|
||||
});
|
||||
|
||||
return {
|
||||
...result,
|
||||
apiKeyId: adHocRunSO.attributes.apiKeyId,
|
||||
apiKeyToUse: adHocRunSO.attributes.apiKeyToUse,
|
||||
} as AdHocRun;
|
||||
};
|
||||
|
|
|
@ -14,6 +14,7 @@ function getMockData(overwrites: Record<string, unknown> = {}): ScheduleBackfill
|
|||
return {
|
||||
ruleId: '1',
|
||||
start: '2023-11-16T08:00:00.000Z',
|
||||
runActions: true,
|
||||
...overwrites,
|
||||
};
|
||||
}
|
||||
|
@ -63,7 +64,7 @@ describe('transformBackfillParamToAdHocRun', () => {
|
|||
});
|
||||
|
||||
test('should transform backfill param with start', () => {
|
||||
expect(transformBackfillParamToAdHocRun(getMockData(), getMockRule(), 'default')).toEqual({
|
||||
expect(transformBackfillParamToAdHocRun(getMockData(), getMockRule(), [], 'default')).toEqual({
|
||||
apiKeyId: '123',
|
||||
apiKeyToUse: 'MTIzOmFiYw==',
|
||||
createdAt: '2024-01-30T00:00:00.000Z',
|
||||
|
@ -75,6 +76,7 @@ describe('transformBackfillParamToAdHocRun', () => {
|
|||
name: 'my rule name',
|
||||
tags: ['foo'],
|
||||
alertTypeId: 'myType',
|
||||
actions: [],
|
||||
params: {},
|
||||
apiKeyOwner: 'user',
|
||||
apiKeyCreatedByUser: false,
|
||||
|
@ -107,6 +109,7 @@ describe('transformBackfillParamToAdHocRun', () => {
|
|||
transformBackfillParamToAdHocRun(
|
||||
getMockData({ end: '2023-11-17T08:00:00.000Z' }),
|
||||
getMockRule(),
|
||||
[],
|
||||
'default'
|
||||
)
|
||||
).toEqual({
|
||||
|
@ -120,6 +123,7 @@ describe('transformBackfillParamToAdHocRun', () => {
|
|||
name: 'my rule name',
|
||||
tags: ['foo'],
|
||||
alertTypeId: 'myType',
|
||||
actions: [],
|
||||
params: {},
|
||||
apiKeyOwner: 'user',
|
||||
apiKeyCreatedByUser: false,
|
||||
|
@ -151,4 +155,101 @@ describe('transformBackfillParamToAdHocRun', () => {
|
|||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('should transform backfill param with rule actions', () => {
|
||||
const actions = [
|
||||
{ uuid: '123abc', group: 'default', actionRef: 'action_0', actionTypeId: 'test', params: {} },
|
||||
];
|
||||
expect(
|
||||
transformBackfillParamToAdHocRun(getMockData(), getMockRule(), actions, 'default')
|
||||
).toEqual({
|
||||
apiKeyId: '123',
|
||||
apiKeyToUse: 'MTIzOmFiYw==',
|
||||
createdAt: '2024-01-30T00:00:00.000Z',
|
||||
duration: '12h',
|
||||
enabled: true,
|
||||
// injects end parameter
|
||||
end: '2023-11-16T20:00:00.000Z',
|
||||
rule: {
|
||||
name: 'my rule name',
|
||||
tags: ['foo'],
|
||||
alertTypeId: 'myType',
|
||||
actions,
|
||||
params: {},
|
||||
apiKeyOwner: 'user',
|
||||
apiKeyCreatedByUser: false,
|
||||
consumer: 'myApp',
|
||||
enabled: true,
|
||||
schedule: {
|
||||
interval: '12h',
|
||||
},
|
||||
createdBy: 'user',
|
||||
updatedBy: 'user',
|
||||
createdAt: '2019-02-12T21:01:22.479Z',
|
||||
updatedAt: '2019-02-12T21:01:22.479Z',
|
||||
revision: 0,
|
||||
},
|
||||
spaceId: 'default',
|
||||
start: '2023-11-16T08:00:00.000Z',
|
||||
status: adHocRunStatus.PENDING,
|
||||
schedule: [
|
||||
{
|
||||
runAt: '2023-11-16T20:00:00.000Z',
|
||||
interval: '12h',
|
||||
status: adHocRunStatus.PENDING,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('should omit rule actions when runActions=false', () => {
|
||||
const actions = [
|
||||
{ uuid: '123abc', group: 'default', actionRef: 'action_0', actionTypeId: 'test', params: {} },
|
||||
];
|
||||
expect(
|
||||
transformBackfillParamToAdHocRun(
|
||||
getMockData({ runActions: false }),
|
||||
getMockRule(),
|
||||
actions,
|
||||
'default'
|
||||
)
|
||||
).toEqual({
|
||||
apiKeyId: '123',
|
||||
apiKeyToUse: 'MTIzOmFiYw==',
|
||||
createdAt: '2024-01-30T00:00:00.000Z',
|
||||
duration: '12h',
|
||||
enabled: true,
|
||||
// injects end parameter
|
||||
end: '2023-11-16T20:00:00.000Z',
|
||||
rule: {
|
||||
name: 'my rule name',
|
||||
tags: ['foo'],
|
||||
alertTypeId: 'myType',
|
||||
actions: [],
|
||||
params: {},
|
||||
apiKeyOwner: 'user',
|
||||
apiKeyCreatedByUser: false,
|
||||
consumer: 'myApp',
|
||||
enabled: true,
|
||||
schedule: {
|
||||
interval: '12h',
|
||||
},
|
||||
createdBy: 'user',
|
||||
updatedBy: 'user',
|
||||
createdAt: '2019-02-12T21:01:22.479Z',
|
||||
updatedAt: '2019-02-12T21:01:22.479Z',
|
||||
revision: 0,
|
||||
},
|
||||
spaceId: 'default',
|
||||
start: '2023-11-16T08:00:00.000Z',
|
||||
status: adHocRunStatus.PENDING,
|
||||
schedule: [
|
||||
{
|
||||
runAt: '2023-11-16T20:00:00.000Z',
|
||||
interval: '12h',
|
||||
status: adHocRunStatus.PENDING,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import { isString } from 'lodash';
|
||||
import { DenormalizedAction } from '../../../rules_client';
|
||||
import { AdHocRunSO } from '../../../data/ad_hoc_run/types';
|
||||
import { calculateSchedule } from '../../../backfill_client/lib';
|
||||
import { adHocRunStatus } from '../../../../common/constants';
|
||||
|
@ -15,9 +16,12 @@ import { ScheduleBackfillParam } from '../methods/schedule/types';
|
|||
export const transformBackfillParamToAdHocRun = (
|
||||
param: ScheduleBackfillParam,
|
||||
rule: RuleDomain,
|
||||
actions: DenormalizedAction[],
|
||||
spaceId: string
|
||||
): AdHocRunSO => {
|
||||
const schedule = calculateSchedule(param.start, rule.schedule.interval, param.end);
|
||||
const shouldRunActions = param.runActions !== undefined ? param.runActions : true;
|
||||
|
||||
return {
|
||||
apiKeyId: Buffer.from(rule.apiKey!, 'base64').toString().split(':')[0],
|
||||
apiKeyToUse: rule.apiKey!,
|
||||
|
@ -32,6 +36,7 @@ export const transformBackfillParamToAdHocRun = (
|
|||
params: rule.params,
|
||||
apiKeyOwner: rule.apiKeyOwner,
|
||||
apiKeyCreatedByUser: rule.apiKeyCreatedByUser,
|
||||
actions: shouldRunActions ? actions : [],
|
||||
consumer: rule.consumer,
|
||||
enabled: rule.enabled,
|
||||
schedule: rule.schedule,
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -23,9 +23,9 @@ import {
|
|||
TaskPriority,
|
||||
} from '@kbn/task-manager-plugin/server';
|
||||
import { isNumber } from 'lodash';
|
||||
import { ActionsClient } from '@kbn/actions-plugin/server';
|
||||
import {
|
||||
ScheduleBackfillError,
|
||||
ScheduleBackfillParam,
|
||||
ScheduleBackfillParams,
|
||||
ScheduleBackfillResult,
|
||||
ScheduleBackfillResults,
|
||||
|
@ -42,6 +42,8 @@ import { AD_HOC_RUN_SAVED_OBJECT_TYPE, RULE_SAVED_OBJECT_TYPE } from '../saved_o
|
|||
import { TaskRunnerFactory } from '../task_runner';
|
||||
import { RuleTypeRegistry } from '../types';
|
||||
import { createBackfillError } from './lib';
|
||||
import { denormalizeActions } from '../rules_client/lib/denormalize_actions';
|
||||
import { DenormalizedAction, NormalizedAlertActionWithGeneratedValues } from '../rules_client';
|
||||
|
||||
export const BACKFILL_TASK_TYPE = 'ad_hoc_run-backfill';
|
||||
|
||||
|
@ -53,6 +55,7 @@ interface ConstructorOpts {
|
|||
}
|
||||
|
||||
interface BulkQueueOpts {
|
||||
actionsClient: ActionsClient;
|
||||
auditLogger?: AuditLogger;
|
||||
params: ScheduleBackfillParams;
|
||||
rules: RuleDomain[];
|
||||
|
@ -86,6 +89,7 @@ export class BackfillClient {
|
|||
}
|
||||
|
||||
public async bulkQueue({
|
||||
actionsClient,
|
||||
auditLogger,
|
||||
params,
|
||||
rules,
|
||||
|
@ -117,10 +121,16 @@ export class BackfillClient {
|
|||
*/
|
||||
|
||||
const soToCreateIndexOrErrorMap: Map<number, number | ScheduleBackfillError> = new Map();
|
||||
const rulesWithUnsupportedActions = new Set<number>();
|
||||
|
||||
params.forEach((param: ScheduleBackfillParam, ndx: number) => {
|
||||
for (let ndx = 0; ndx < params.length; ndx++) {
|
||||
const param = params[ndx];
|
||||
// For this schedule request, look up the rule or return error
|
||||
const { rule, error } = getRuleOrError(param.ruleId, rules, ruleTypeRegistry);
|
||||
const { rule, error } = getRuleOrError({
|
||||
ruleId: param.ruleId,
|
||||
rules,
|
||||
ruleTypeRegistry,
|
||||
});
|
||||
if (rule) {
|
||||
// keep track of index of this request in the adHocSOsToCreate array
|
||||
soToCreateIndexOrErrorMap.set(ndx, adHocSOsToCreate.length);
|
||||
|
@ -129,22 +139,31 @@ export class BackfillClient {
|
|||
name: `rule`,
|
||||
type: RULE_SAVED_OBJECT_TYPE,
|
||||
};
|
||||
|
||||
const { actions, hasUnsupportedActions, references } = await extractRuleActions({
|
||||
actionsClient,
|
||||
rule,
|
||||
runActions: param.runActions,
|
||||
});
|
||||
|
||||
if (hasUnsupportedActions) {
|
||||
rulesWithUnsupportedActions.add(ndx);
|
||||
}
|
||||
|
||||
adHocSOsToCreate.push({
|
||||
type: AD_HOC_RUN_SAVED_OBJECT_TYPE,
|
||||
attributes: transformBackfillParamToAdHocRun(param, rule, spaceId),
|
||||
references: [reference],
|
||||
attributes: transformBackfillParamToAdHocRun(param, rule, actions, spaceId),
|
||||
references: [reference, ...references],
|
||||
});
|
||||
} else if (error) {
|
||||
// keep track of the error encountered for this request by index so
|
||||
// we can return it in order
|
||||
soToCreateIndexOrErrorMap.set(ndx, error);
|
||||
this.logger.warn(
|
||||
`No rule found for ruleId ${param.ruleId} - not scheduling backfill for ${JSON.stringify(
|
||||
param
|
||||
)}`
|
||||
`Error for ruleId ${param.ruleId} - not scheduling backfill for ${JSON.stringify(param)}`
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Every request encountered an error, so short-circuit the logic here
|
||||
if (!adHocSOsToCreate.length) {
|
||||
|
@ -175,7 +194,11 @@ export class BackfillClient {
|
|||
})
|
||||
);
|
||||
}
|
||||
return transformAdHocRunToBackfillResult(so, adHocSOsToCreate?.[index]);
|
||||
return transformAdHocRunToBackfillResult({
|
||||
adHocRunSO: so,
|
||||
isSystemAction: (id: string) => actionsClient.isSystemAction(id),
|
||||
originalSO: adHocSOsToCreate?.[index],
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -202,7 +225,16 @@ export class BackfillClient {
|
|||
|
||||
if (isNumber(indexOrError)) {
|
||||
// This number is the index of the response from the savedObjects bulkCreate function
|
||||
return transformedResponse[indexOrError];
|
||||
const response = transformedResponse[indexOrError];
|
||||
if (rulesWithUnsupportedActions.has(indexOrError)) {
|
||||
return {
|
||||
...response,
|
||||
warnings: [
|
||||
`Rule has actions that are not supported for backfill. Those actions will be skipped.`,
|
||||
],
|
||||
};
|
||||
}
|
||||
return response;
|
||||
} else {
|
||||
// Return the error we encountered
|
||||
return indexOrError as ScheduleBackfillError;
|
||||
|
@ -301,11 +333,16 @@ export class BackfillClient {
|
|||
}
|
||||
}
|
||||
|
||||
function getRuleOrError(
|
||||
ruleId: string,
|
||||
rules: RuleDomain[],
|
||||
ruleTypeRegistry: RuleTypeRegistry
|
||||
): { rule?: RuleDomain; error?: ScheduleBackfillError } {
|
||||
interface GetRuleOrErrorOpts {
|
||||
ruleId: string;
|
||||
rules: RuleDomain[];
|
||||
ruleTypeRegistry: RuleTypeRegistry;
|
||||
}
|
||||
|
||||
function getRuleOrError({ ruleId, rules, ruleTypeRegistry }: GetRuleOrErrorOpts): {
|
||||
rule?: RuleDomain;
|
||||
error?: ScheduleBackfillError;
|
||||
} {
|
||||
const rule = rules.find((r: RuleDomain) => r.id === ruleId);
|
||||
|
||||
// if rule not found, return not found error
|
||||
|
@ -345,3 +382,55 @@ function getRuleOrError(
|
|||
|
||||
return { rule };
|
||||
}
|
||||
|
||||
interface ExtractRuleActions {
|
||||
actionsClient: ActionsClient;
|
||||
rule: RuleDomain;
|
||||
runActions?: boolean;
|
||||
}
|
||||
|
||||
interface ExtractRuleActionsResult {
|
||||
actions: DenormalizedAction[];
|
||||
hasUnsupportedActions: boolean;
|
||||
references: SavedObjectReference[];
|
||||
}
|
||||
|
||||
async function extractRuleActions({
|
||||
actionsClient,
|
||||
rule,
|
||||
runActions,
|
||||
}: ExtractRuleActions): Promise<ExtractRuleActionsResult> {
|
||||
// defauts to true if not specified
|
||||
const shouldRunActions = runActions !== undefined ? runActions : true;
|
||||
|
||||
if (!shouldRunActions) {
|
||||
return { hasUnsupportedActions: false, actions: [], references: [] };
|
||||
}
|
||||
|
||||
const ruleLevelNotifyWhen = rule.notifyWhen;
|
||||
const normalizedActions = [];
|
||||
for (const action of rule.actions) {
|
||||
// if action level frequency is not defined and rule level notifyWhen is, set the action level frequency
|
||||
if (!action.frequency && ruleLevelNotifyWhen) {
|
||||
normalizedActions.push({
|
||||
...action,
|
||||
frequency: { notifyWhen: ruleLevelNotifyWhen, summary: false, throttle: null },
|
||||
});
|
||||
} else {
|
||||
normalizedActions.push(action);
|
||||
}
|
||||
}
|
||||
|
||||
const hasUnsupportedActions = normalizedActions.some(
|
||||
(action) => action.frequency?.notifyWhen !== 'onActiveAlert'
|
||||
);
|
||||
|
||||
const allActions = [
|
||||
...normalizedActions.filter((action) => action.frequency?.notifyWhen === 'onActiveAlert'),
|
||||
...(rule.systemActions ?? []),
|
||||
] as NormalizedAlertActionWithGeneratedValues[];
|
||||
|
||||
const { references, actions } = await denormalizeActions(actionsClient, allActions);
|
||||
|
||||
return { hasUnsupportedActions, actions, references };
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { RawRule } from '../../../types';
|
||||
import { RuleDomain } from '../../../application/rule/types';
|
||||
import { AdHocRunStatus } from '../../../../common/constants';
|
||||
|
||||
|
@ -23,10 +24,11 @@ export interface AdHocRunSchedule extends Record<string, unknown> {
|
|||
// the backfill job was scheduled. if there are updates to the rule configuration
|
||||
// after the backfill is scheduled, they will not be reflected during the backfill run.
|
||||
type AdHocRunSORule = Pick<
|
||||
RuleDomain,
|
||||
RawRule,
|
||||
| 'name'
|
||||
| 'tags'
|
||||
| 'alertTypeId'
|
||||
| 'actions'
|
||||
| 'params'
|
||||
| 'apiKeyOwner'
|
||||
| 'apiKeyCreatedByUser'
|
||||
|
@ -43,7 +45,7 @@ type AdHocRunSORule = Pick<
|
|||
|
||||
// This is the rule information after loaded from persistence with the
|
||||
// rule ID injected from the SO references array
|
||||
type AdHocRunRule = AdHocRunSORule & Pick<RuleDomain, 'id'>;
|
||||
type AdHocRunRule = Omit<AdHocRunSORule, 'actions'> & Pick<RuleDomain, 'id' | 'actions'>;
|
||||
|
||||
export interface AdHocRunSO extends Record<string, unknown> {
|
||||
apiKeyId: string;
|
||||
|
|
|
@ -17,6 +17,7 @@ import {
|
|||
invalidateApiKeysAndDeletePendingApiKeySavedObject,
|
||||
runInvalidate,
|
||||
} from './task';
|
||||
import { ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE } from '@kbn/actions-plugin/server/constants/saved_objects';
|
||||
|
||||
let fakeTimer: sinon.SinonFakeTimers;
|
||||
const encryptedSavedObjectsClient = encryptedSavedObjectsMock.createClient();
|
||||
|
@ -92,17 +93,24 @@ describe('Invalidate API Keys Task', () => {
|
|||
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(
|
||||
mockInvalidatePendingApiKeyObject2
|
||||
);
|
||||
// first call to find aggregates any AD_HOC_RUN_SAVED_OBJECT_TYPE SOs by apiKeyId
|
||||
internalSavedObjectsRepository.find.mockResolvedValueOnce({
|
||||
saved_objects: [],
|
||||
total: 2,
|
||||
page: 1,
|
||||
per_page: 10,
|
||||
aggregations: {
|
||||
apiKeyId: {
|
||||
doc_count_error_upper_bound: 0,
|
||||
sum_other_doc_count: 0,
|
||||
buckets: [],
|
||||
},
|
||||
apiKeyId: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] },
|
||||
},
|
||||
});
|
||||
// second call to find aggregates any ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE SOs by apiKeyId
|
||||
internalSavedObjectsRepository.find.mockResolvedValueOnce({
|
||||
saved_objects: [],
|
||||
total: 1,
|
||||
page: 1,
|
||||
per_page: 10,
|
||||
aggregations: {
|
||||
apiKeyId: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] },
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -166,13 +174,114 @@ describe('Invalidate API Keys Task', () => {
|
|||
});
|
||||
});
|
||||
|
||||
test('should get decrypted api key pending invalidation saved object when some api keys are still in use', async () => {
|
||||
test('should get decrypted api key pending invalidation saved object when some api keys are still in use by AD_HOC_RUN_SAVED_OBJECT_TYPE', async () => {
|
||||
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(
|
||||
mockInvalidatePendingApiKeyObject1
|
||||
);
|
||||
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(
|
||||
mockInvalidatePendingApiKeyObject2
|
||||
);
|
||||
// first call to find aggregates any AD_HOC_RUN_SAVED_OBJECT_TYPE SOs by apiKeyId
|
||||
internalSavedObjectsRepository.find.mockResolvedValueOnce({
|
||||
saved_objects: [],
|
||||
total: 2,
|
||||
page: 1,
|
||||
per_page: 10,
|
||||
aggregations: {
|
||||
apiKeyId: {
|
||||
doc_count_error_upper_bound: 0,
|
||||
sum_other_doc_count: 0,
|
||||
buckets: [{ key: 'abcd====!', doc_count: 1 }],
|
||||
},
|
||||
},
|
||||
});
|
||||
// second call to find aggregates any ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE SOs by apiKeyId
|
||||
internalSavedObjectsRepository.find.mockResolvedValueOnce({
|
||||
saved_objects: [],
|
||||
total: 1,
|
||||
page: 1,
|
||||
per_page: 10,
|
||||
aggregations: {
|
||||
apiKeyId: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] },
|
||||
},
|
||||
});
|
||||
|
||||
const result = await getApiKeyIdsToInvalidate({
|
||||
apiKeySOsPendingInvalidation: {
|
||||
saved_objects: [
|
||||
{
|
||||
id: '1',
|
||||
type: API_KEY_PENDING_INVALIDATION_TYPE,
|
||||
score: 0,
|
||||
attributes: { apiKeyId: 'encryptedencrypted', createdAt: '2024-04-11T17:08:44.035Z' },
|
||||
references: [],
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: API_KEY_PENDING_INVALIDATION_TYPE,
|
||||
score: 0,
|
||||
attributes: { apiKeyId: 'encryptedencrypted', createdAt: '2024-04-11T17:08:44.035Z' },
|
||||
references: [],
|
||||
},
|
||||
],
|
||||
total: 2,
|
||||
per_page: 10,
|
||||
page: 1,
|
||||
},
|
||||
encryptedSavedObjectsClient,
|
||||
savedObjectsClient: internalSavedObjectsRepository,
|
||||
});
|
||||
|
||||
expect(encryptedSavedObjectsClient.getDecryptedAsInternalUser).toHaveBeenCalledTimes(2);
|
||||
expect(encryptedSavedObjectsClient.getDecryptedAsInternalUser).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
API_KEY_PENDING_INVALIDATION_TYPE,
|
||||
'1'
|
||||
);
|
||||
expect(encryptedSavedObjectsClient.getDecryptedAsInternalUser).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
API_KEY_PENDING_INVALIDATION_TYPE,
|
||||
'2'
|
||||
);
|
||||
expect(internalSavedObjectsRepository.find).toHaveBeenCalledWith({
|
||||
type: AD_HOC_RUN_SAVED_OBJECT_TYPE,
|
||||
perPage: 0,
|
||||
filter: `ad_hoc_run_params.attributes.apiKeyId: "abcd====!" OR ad_hoc_run_params.attributes.apiKeyId: "xyz!==!"`,
|
||||
namespaces: ['*'],
|
||||
aggs: {
|
||||
apiKeyId: {
|
||||
terms: {
|
||||
field: `ad_hoc_run_params.attributes.apiKeyId`,
|
||||
size: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(result).toEqual({
|
||||
apiKeyIdsToInvalidate: [{ id: '2', apiKeyId: 'xyz!==!' }],
|
||||
apiKeyIdsToExclude: [{ id: '1', apiKeyId: 'abcd====!' }],
|
||||
});
|
||||
});
|
||||
|
||||
test('should get decrypted api key pending invalidation saved object when some api keys are still in use by ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE', async () => {
|
||||
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(
|
||||
mockInvalidatePendingApiKeyObject1
|
||||
);
|
||||
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(
|
||||
mockInvalidatePendingApiKeyObject2
|
||||
);
|
||||
// first call to find aggregates any AD_HOC_RUN_SAVED_OBJECT_TYPE SOs by apiKeyId
|
||||
internalSavedObjectsRepository.find.mockResolvedValueOnce({
|
||||
saved_objects: [],
|
||||
total: 1,
|
||||
page: 1,
|
||||
per_page: 10,
|
||||
aggregations: {
|
||||
apiKeyId: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] },
|
||||
},
|
||||
});
|
||||
|
||||
// second call to find aggregates any ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE SOs by apiKeyId
|
||||
internalSavedObjectsRepository.find.mockResolvedValueOnce({
|
||||
saved_objects: [],
|
||||
total: 2,
|
||||
|
@ -299,6 +408,15 @@ describe('Invalidate API Keys Task', () => {
|
|||
per_page: 10,
|
||||
// missing aggregations
|
||||
});
|
||||
internalSavedObjectsRepository.find.mockResolvedValueOnce({
|
||||
saved_objects: [],
|
||||
total: 1,
|
||||
page: 1,
|
||||
per_page: 10,
|
||||
aggregations: {
|
||||
apiKeyId: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] },
|
||||
},
|
||||
});
|
||||
|
||||
const result = await getApiKeyIdsToInvalidate({
|
||||
apiKeySOsPendingInvalidation: {
|
||||
|
@ -547,19 +665,28 @@ describe('Invalidate API Keys Task', () => {
|
|||
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(
|
||||
mockInvalidatePendingApiKeyObject2
|
||||
);
|
||||
// first call to find aggregates any AD_HOC_RUN_SAVED_OBJECT_TYPE SOs by apiKeyId
|
||||
internalSavedObjectsRepository.find.mockResolvedValueOnce({
|
||||
saved_objects: [],
|
||||
total: 1,
|
||||
page: 1,
|
||||
per_page: 10,
|
||||
aggregations: {
|
||||
apiKeyId: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] },
|
||||
},
|
||||
});
|
||||
|
||||
// second call to find aggregates any ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE SOs by apiKeyId
|
||||
internalSavedObjectsRepository.find.mockResolvedValueOnce({
|
||||
saved_objects: [],
|
||||
total: 2,
|
||||
page: 1,
|
||||
per_page: 0,
|
||||
per_page: 10,
|
||||
aggregations: {
|
||||
apiKeyId: {
|
||||
doc_count_error_upper_bound: 0,
|
||||
sum_other_doc_count: 0,
|
||||
buckets: [],
|
||||
},
|
||||
apiKeyId: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] },
|
||||
},
|
||||
});
|
||||
|
||||
securityMockStart.authc.apiKeys.invalidateAsInternalUser.mockResolvedValue({
|
||||
invalidated_api_keys: ['1', '2'],
|
||||
previously_invalidated_api_keys: [],
|
||||
|
@ -576,7 +703,7 @@ describe('Invalidate API Keys Task', () => {
|
|||
});
|
||||
expect(result).toEqual(2);
|
||||
|
||||
expect(internalSavedObjectsRepository.find).toHaveBeenCalledTimes(2);
|
||||
expect(internalSavedObjectsRepository.find).toHaveBeenCalledTimes(3);
|
||||
expect(internalSavedObjectsRepository.find).toHaveBeenNthCalledWith(1, {
|
||||
type: API_KEY_PENDING_INVALIDATION_TYPE,
|
||||
filter: `api_key_pending_invalidation.attributes.createdAt <= "1969-12-31T23:00:00.000Z"`,
|
||||
|
@ -610,6 +737,20 @@ describe('Invalidate API Keys Task', () => {
|
|||
},
|
||||
},
|
||||
});
|
||||
expect(internalSavedObjectsRepository.find).toHaveBeenNthCalledWith(3, {
|
||||
type: ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE,
|
||||
perPage: 0,
|
||||
filter: `action_task_params.attributes.apiKeyId: "abcd====!" OR action_task_params.attributes.apiKeyId: "xyz!==!"`,
|
||||
namespaces: ['*'],
|
||||
aggs: {
|
||||
apiKeyId: {
|
||||
terms: {
|
||||
field: `action_task_params.attributes.apiKeyId`,
|
||||
size: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(securityMockStart.authc.apiKeys.invalidateAsInternalUser).toHaveBeenCalledTimes(1);
|
||||
expect(securityMockStart.authc.apiKeys.invalidateAsInternalUser).toHaveBeenCalledWith({
|
||||
ids: ['abcd====!', 'xyz!==!'],
|
||||
|
@ -628,7 +769,7 @@ describe('Invalidate API Keys Task', () => {
|
|||
expect(logger.debug).toHaveBeenCalledWith(`Total invalidated API keys "2"`);
|
||||
});
|
||||
|
||||
test('should succeed when there are API keys to invalidate and API keys to exclude', async () => {
|
||||
test('should succeed when there are API keys to invalidate and API keys to exclude (AD_HOC_RUN_SAVED_OBJECT_TYPE using apiKeyId)', async () => {
|
||||
internalSavedObjectsRepository.find.mockResolvedValueOnce({
|
||||
saved_objects: [
|
||||
{
|
||||
|
@ -656,6 +797,8 @@ describe('Invalidate API Keys Task', () => {
|
|||
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(
|
||||
mockInvalidatePendingApiKeyObject2
|
||||
);
|
||||
|
||||
// first call to find aggregates any AD_HOC_RUN_SAVED_OBJECT_TYPE SOs by apiKeyId
|
||||
internalSavedObjectsRepository.find.mockResolvedValueOnce({
|
||||
saved_objects: [],
|
||||
total: 2,
|
||||
|
@ -669,6 +812,18 @@ describe('Invalidate API Keys Task', () => {
|
|||
},
|
||||
},
|
||||
});
|
||||
|
||||
// second call to find aggregates any ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE SOs by apiKeyId
|
||||
internalSavedObjectsRepository.find.mockResolvedValueOnce({
|
||||
saved_objects: [],
|
||||
total: 2,
|
||||
page: 1,
|
||||
per_page: 10,
|
||||
aggregations: {
|
||||
apiKeyId: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] },
|
||||
},
|
||||
});
|
||||
|
||||
securityMockStart.authc.apiKeys.invalidateAsInternalUser.mockResolvedValue({
|
||||
invalidated_api_keys: ['1'],
|
||||
previously_invalidated_api_keys: [],
|
||||
|
@ -685,7 +840,7 @@ describe('Invalidate API Keys Task', () => {
|
|||
});
|
||||
expect(result).toEqual(1);
|
||||
|
||||
expect(internalSavedObjectsRepository.find).toHaveBeenCalledTimes(2);
|
||||
expect(internalSavedObjectsRepository.find).toHaveBeenCalledTimes(3);
|
||||
expect(internalSavedObjectsRepository.find).toHaveBeenNthCalledWith(1, {
|
||||
type: API_KEY_PENDING_INVALIDATION_TYPE,
|
||||
filter: `api_key_pending_invalidation.attributes.createdAt <= "1969-12-31T23:00:00.000Z"`,
|
||||
|
@ -719,6 +874,152 @@ describe('Invalidate API Keys Task', () => {
|
|||
},
|
||||
},
|
||||
});
|
||||
expect(internalSavedObjectsRepository.find).toHaveBeenNthCalledWith(3, {
|
||||
type: ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE,
|
||||
perPage: 0,
|
||||
filter: `action_task_params.attributes.apiKeyId: "abcd====!" OR action_task_params.attributes.apiKeyId: "xyz!==!"`,
|
||||
namespaces: ['*'],
|
||||
aggs: {
|
||||
apiKeyId: {
|
||||
terms: {
|
||||
field: `action_task_params.attributes.apiKeyId`,
|
||||
size: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(securityMockStart.authc.apiKeys.invalidateAsInternalUser).toHaveBeenCalledTimes(1);
|
||||
expect(securityMockStart.authc.apiKeys.invalidateAsInternalUser).toHaveBeenCalledWith({
|
||||
ids: ['xyz!==!'],
|
||||
});
|
||||
expect(internalSavedObjectsRepository.delete).toHaveBeenCalledTimes(1);
|
||||
expect(internalSavedObjectsRepository.delete).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
API_KEY_PENDING_INVALIDATION_TYPE,
|
||||
'2'
|
||||
);
|
||||
expect(logger.debug).toHaveBeenCalledWith(`Total invalidated API keys "1"`);
|
||||
});
|
||||
|
||||
test('should succeed when there are API keys to invalidate and API keys to exclude (ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE using apiKeyId)', async () => {
|
||||
internalSavedObjectsRepository.find.mockResolvedValueOnce({
|
||||
saved_objects: [
|
||||
{
|
||||
id: '1',
|
||||
type: API_KEY_PENDING_INVALIDATION_TYPE,
|
||||
score: 0,
|
||||
attributes: { apiKeyId: 'encryptedencrypted', createdAt: '2024-04-11T17:08:44.035Z' },
|
||||
references: [],
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: API_KEY_PENDING_INVALIDATION_TYPE,
|
||||
score: 0,
|
||||
attributes: { apiKeyId: 'encryptedencrypted', createdAt: '2024-04-11T17:08:44.035Z' },
|
||||
references: [],
|
||||
},
|
||||
],
|
||||
total: 2,
|
||||
per_page: 100,
|
||||
page: 1,
|
||||
});
|
||||
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(
|
||||
mockInvalidatePendingApiKeyObject1
|
||||
);
|
||||
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(
|
||||
mockInvalidatePendingApiKeyObject2
|
||||
);
|
||||
|
||||
// first call to find aggregates any AD_HOC_RUN_SAVED_OBJECT_TYPE SOs by apiKeyId
|
||||
internalSavedObjectsRepository.find.mockResolvedValueOnce({
|
||||
saved_objects: [],
|
||||
total: 2,
|
||||
page: 1,
|
||||
per_page: 10,
|
||||
aggregations: {
|
||||
apiKeyId: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] },
|
||||
},
|
||||
});
|
||||
|
||||
// second call to find aggregates any ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE SOs by apiKeyId
|
||||
internalSavedObjectsRepository.find.mockResolvedValueOnce({
|
||||
saved_objects: [],
|
||||
total: 2,
|
||||
page: 1,
|
||||
per_page: 0,
|
||||
aggregations: {
|
||||
apiKeyId: {
|
||||
doc_count_error_upper_bound: 0,
|
||||
sum_other_doc_count: 0,
|
||||
buckets: [{ key: 'abcd====!', doc_count: 1 }],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
securityMockStart.authc.apiKeys.invalidateAsInternalUser.mockResolvedValue({
|
||||
invalidated_api_keys: ['1'],
|
||||
previously_invalidated_api_keys: [],
|
||||
error_count: 0,
|
||||
});
|
||||
|
||||
const result = await runInvalidate({
|
||||
// @ts-expect-error
|
||||
config: { invalidateApiKeysTask: { interval: '1m', removalDelay: '1h' } },
|
||||
encryptedSavedObjectsClient,
|
||||
logger,
|
||||
savedObjectsClient: internalSavedObjectsRepository,
|
||||
security: securityMockStart,
|
||||
});
|
||||
expect(result).toEqual(1);
|
||||
|
||||
expect(internalSavedObjectsRepository.find).toHaveBeenCalledTimes(3);
|
||||
expect(internalSavedObjectsRepository.find).toHaveBeenNthCalledWith(1, {
|
||||
type: API_KEY_PENDING_INVALIDATION_TYPE,
|
||||
filter: `api_key_pending_invalidation.attributes.createdAt <= "1969-12-31T23:00:00.000Z"`,
|
||||
page: 1,
|
||||
sortField: 'createdAt',
|
||||
sortOrder: 'asc',
|
||||
perPage: 100,
|
||||
});
|
||||
expect(encryptedSavedObjectsClient.getDecryptedAsInternalUser).toHaveBeenCalledTimes(2);
|
||||
expect(encryptedSavedObjectsClient.getDecryptedAsInternalUser).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
API_KEY_PENDING_INVALIDATION_TYPE,
|
||||
'1'
|
||||
);
|
||||
expect(encryptedSavedObjectsClient.getDecryptedAsInternalUser).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
API_KEY_PENDING_INVALIDATION_TYPE,
|
||||
'2'
|
||||
);
|
||||
expect(internalSavedObjectsRepository.find).toHaveBeenNthCalledWith(2, {
|
||||
type: AD_HOC_RUN_SAVED_OBJECT_TYPE,
|
||||
perPage: 0,
|
||||
filter: `ad_hoc_run_params.attributes.apiKeyId: "abcd====!" OR ad_hoc_run_params.attributes.apiKeyId: "xyz!==!"`,
|
||||
namespaces: ['*'],
|
||||
aggs: {
|
||||
apiKeyId: {
|
||||
terms: {
|
||||
field: `ad_hoc_run_params.attributes.apiKeyId`,
|
||||
size: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(internalSavedObjectsRepository.find).toHaveBeenNthCalledWith(3, {
|
||||
type: ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE,
|
||||
perPage: 0,
|
||||
filter: `action_task_params.attributes.apiKeyId: "abcd====!" OR action_task_params.attributes.apiKeyId: "xyz!==!"`,
|
||||
namespaces: ['*'],
|
||||
aggs: {
|
||||
apiKeyId: {
|
||||
terms: {
|
||||
field: `action_task_params.attributes.apiKeyId`,
|
||||
size: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(securityMockStart.authc.apiKeys.invalidateAsInternalUser).toHaveBeenCalledTimes(1);
|
||||
expect(securityMockStart.authc.apiKeys.invalidateAsInternalUser).toHaveBeenCalledWith({
|
||||
ids: ['xyz!==!'],
|
||||
|
@ -760,6 +1061,8 @@ describe('Invalidate API Keys Task', () => {
|
|||
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(
|
||||
mockInvalidatePendingApiKeyObject2
|
||||
);
|
||||
|
||||
// first call to find aggregates any AD_HOC_RUN_SAVED_OBJECT_TYPE SOs by apiKeyId
|
||||
internalSavedObjectsRepository.find.mockResolvedValueOnce({
|
||||
saved_objects: [],
|
||||
total: 2,
|
||||
|
@ -777,6 +1080,21 @@ describe('Invalidate API Keys Task', () => {
|
|||
},
|
||||
});
|
||||
|
||||
// second call to find aggregates any ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE SOs by apiKeyId
|
||||
internalSavedObjectsRepository.find.mockResolvedValueOnce({
|
||||
saved_objects: [],
|
||||
total: 2,
|
||||
page: 1,
|
||||
per_page: 0,
|
||||
aggregations: {
|
||||
apiKeyId: {
|
||||
doc_count_error_upper_bound: 0,
|
||||
sum_other_doc_count: 0,
|
||||
buckets: [{ key: 'abcd====!', doc_count: 3 }],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await runInvalidate({
|
||||
// @ts-expect-error
|
||||
config: { invalidateApiKeysTask: { interval: '1m', removalDelay: '1h' } },
|
||||
|
@ -787,8 +1105,8 @@ describe('Invalidate API Keys Task', () => {
|
|||
});
|
||||
expect(result).toEqual(0);
|
||||
|
||||
expect(internalSavedObjectsRepository.find).toHaveBeenCalledTimes(2);
|
||||
expect(internalSavedObjectsRepository.find).toHaveBeenCalledWith({
|
||||
expect(internalSavedObjectsRepository.find).toHaveBeenCalledTimes(3);
|
||||
expect(internalSavedObjectsRepository.find).toHaveBeenNthCalledWith(1, {
|
||||
type: API_KEY_PENDING_INVALIDATION_TYPE,
|
||||
filter: `api_key_pending_invalidation.attributes.createdAt <= "1969-12-31T23:00:00.000Z"`,
|
||||
page: 1,
|
||||
|
@ -807,7 +1125,7 @@ describe('Invalidate API Keys Task', () => {
|
|||
API_KEY_PENDING_INVALIDATION_TYPE,
|
||||
'2'
|
||||
);
|
||||
expect(internalSavedObjectsRepository.find).toHaveBeenCalledWith({
|
||||
expect(internalSavedObjectsRepository.find).toHaveBeenNthCalledWith(2, {
|
||||
type: AD_HOC_RUN_SAVED_OBJECT_TYPE,
|
||||
perPage: 0,
|
||||
filter: `ad_hoc_run_params.attributes.apiKeyId: "abcd====!" OR ad_hoc_run_params.attributes.apiKeyId: "xyz!==!"`,
|
||||
|
@ -821,6 +1139,20 @@ describe('Invalidate API Keys Task', () => {
|
|||
},
|
||||
},
|
||||
});
|
||||
expect(internalSavedObjectsRepository.find).toHaveBeenNthCalledWith(3, {
|
||||
type: ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE,
|
||||
perPage: 0,
|
||||
filter: `action_task_params.attributes.apiKeyId: "abcd====!" OR action_task_params.attributes.apiKeyId: "xyz!==!"`,
|
||||
namespaces: ['*'],
|
||||
aggs: {
|
||||
apiKeyId: {
|
||||
terms: {
|
||||
field: `action_task_params.attributes.apiKeyId`,
|
||||
size: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(securityMockStart.authc.apiKeys.invalidateAsInternalUser).not.toHaveBeenCalled();
|
||||
expect(internalSavedObjectsRepository.delete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
@ -844,19 +1176,29 @@ describe('Invalidate API Keys Task', () => {
|
|||
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(
|
||||
mockInvalidatePendingApiKeyObject1
|
||||
);
|
||||
|
||||
// first call to find aggregates any AD_HOC_RUN_SAVED_OBJECT_TYPE SOs by apiKeyId
|
||||
internalSavedObjectsRepository.find.mockResolvedValueOnce({
|
||||
saved_objects: [],
|
||||
total: 2,
|
||||
page: 1,
|
||||
per_page: 0,
|
||||
aggregations: {
|
||||
apiKeyId: {
|
||||
doc_count_error_upper_bound: 0,
|
||||
sum_other_doc_count: 0,
|
||||
buckets: [],
|
||||
},
|
||||
apiKeyId: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] },
|
||||
},
|
||||
});
|
||||
|
||||
// second call to find aggregates any ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE SOs by apiKeyId
|
||||
internalSavedObjectsRepository.find.mockResolvedValueOnce({
|
||||
saved_objects: [],
|
||||
total: 2,
|
||||
page: 1,
|
||||
per_page: 0,
|
||||
aggregations: {
|
||||
apiKeyId: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] },
|
||||
},
|
||||
});
|
||||
|
||||
securityMockStart.authc.apiKeys.invalidateAsInternalUser.mockResolvedValue({
|
||||
invalidated_api_keys: ['1'],
|
||||
previously_invalidated_api_keys: [],
|
||||
|
@ -880,17 +1222,25 @@ describe('Invalidate API Keys Task', () => {
|
|||
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(
|
||||
mockInvalidatePendingApiKeyObject2
|
||||
);
|
||||
// first call to find aggregates any AD_HOC_RUN_SAVED_OBJECT_TYPE SOs by apiKeyId
|
||||
internalSavedObjectsRepository.find.mockResolvedValueOnce({
|
||||
saved_objects: [],
|
||||
total: 2,
|
||||
page: 1,
|
||||
per_page: 0,
|
||||
aggregations: {
|
||||
apiKeyId: {
|
||||
doc_count_error_upper_bound: 0,
|
||||
sum_other_doc_count: 0,
|
||||
buckets: [],
|
||||
},
|
||||
apiKeyId: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] },
|
||||
},
|
||||
});
|
||||
|
||||
// second call to find aggregates any ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE SOs by apiKeyId
|
||||
internalSavedObjectsRepository.find.mockResolvedValueOnce({
|
||||
saved_objects: [],
|
||||
total: 2,
|
||||
page: 1,
|
||||
per_page: 0,
|
||||
aggregations: {
|
||||
apiKeyId: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] },
|
||||
},
|
||||
});
|
||||
securityMockStart.authc.apiKeys.invalidateAsInternalUser.mockResolvedValue({
|
||||
|
@ -909,7 +1259,7 @@ describe('Invalidate API Keys Task', () => {
|
|||
});
|
||||
expect(result).toEqual(2);
|
||||
|
||||
expect(internalSavedObjectsRepository.find).toHaveBeenCalledTimes(4);
|
||||
expect(internalSavedObjectsRepository.find).toHaveBeenCalledTimes(6);
|
||||
expect(encryptedSavedObjectsClient.getDecryptedAsInternalUser).toHaveBeenCalledTimes(2);
|
||||
expect(securityMockStart.authc.apiKeys.invalidateAsInternalUser).toHaveBeenCalledTimes(2);
|
||||
expect(internalSavedObjectsRepository.delete).toHaveBeenCalledTimes(2);
|
||||
|
@ -943,6 +1293,20 @@ describe('Invalidate API Keys Task', () => {
|
|||
},
|
||||
},
|
||||
});
|
||||
expect(internalSavedObjectsRepository.find).toHaveBeenNthCalledWith(3, {
|
||||
type: ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE,
|
||||
perPage: 0,
|
||||
filter: `action_task_params.attributes.apiKeyId: "abcd====!"`,
|
||||
namespaces: ['*'],
|
||||
aggs: {
|
||||
apiKeyId: {
|
||||
terms: {
|
||||
field: `action_task_params.attributes.apiKeyId`,
|
||||
size: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(securityMockStart.authc.apiKeys.invalidateAsInternalUser).toHaveBeenNthCalledWith(1, {
|
||||
ids: ['abcd====!'],
|
||||
});
|
||||
|
@ -954,7 +1318,7 @@ describe('Invalidate API Keys Task', () => {
|
|||
expect(logger.debug).toHaveBeenNthCalledWith(1, `Total invalidated API keys "1"`);
|
||||
|
||||
// second iteration
|
||||
expect(internalSavedObjectsRepository.find).toHaveBeenNthCalledWith(3, {
|
||||
expect(internalSavedObjectsRepository.find).toHaveBeenNthCalledWith(4, {
|
||||
type: API_KEY_PENDING_INVALIDATION_TYPE,
|
||||
filter: `api_key_pending_invalidation.attributes.createdAt <= "1969-12-31T23:00:00.000Z"`,
|
||||
page: 1,
|
||||
|
@ -967,7 +1331,7 @@ describe('Invalidate API Keys Task', () => {
|
|||
API_KEY_PENDING_INVALIDATION_TYPE,
|
||||
'2'
|
||||
);
|
||||
expect(internalSavedObjectsRepository.find).toHaveBeenNthCalledWith(4, {
|
||||
expect(internalSavedObjectsRepository.find).toHaveBeenNthCalledWith(5, {
|
||||
type: AD_HOC_RUN_SAVED_OBJECT_TYPE,
|
||||
perPage: 0,
|
||||
filter: `ad_hoc_run_params.attributes.apiKeyId: "xyz!==!"`,
|
||||
|
@ -981,6 +1345,20 @@ describe('Invalidate API Keys Task', () => {
|
|||
},
|
||||
},
|
||||
});
|
||||
expect(internalSavedObjectsRepository.find).toHaveBeenNthCalledWith(6, {
|
||||
type: ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE,
|
||||
perPage: 0,
|
||||
filter: `action_task_params.attributes.apiKeyId: "xyz!==!"`,
|
||||
namespaces: ['*'],
|
||||
aggs: {
|
||||
apiKeyId: {
|
||||
terms: {
|
||||
field: `action_task_params.attributes.apiKeyId`,
|
||||
size: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(securityMockStart.authc.apiKeys.invalidateAsInternalUser).toHaveBeenNthCalledWith(2, {
|
||||
ids: ['xyz!==!'],
|
||||
});
|
||||
|
@ -1021,6 +1399,7 @@ describe('Invalidate API Keys Task', () => {
|
|||
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(
|
||||
mockInvalidatePendingApiKeyObject2
|
||||
);
|
||||
// first call to find aggregates any AD_HOC_RUN_SAVED_OBJECT_TYPE SOs by apiKeyId
|
||||
internalSavedObjectsRepository.find.mockResolvedValueOnce({
|
||||
saved_objects: [],
|
||||
total: 2,
|
||||
|
@ -1034,6 +1413,18 @@ describe('Invalidate API Keys Task', () => {
|
|||
},
|
||||
},
|
||||
});
|
||||
|
||||
// second call to find aggregates any ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE SOs by apiKeyId
|
||||
internalSavedObjectsRepository.find.mockResolvedValueOnce({
|
||||
saved_objects: [],
|
||||
total: 2,
|
||||
page: 1,
|
||||
per_page: 0,
|
||||
aggregations: {
|
||||
apiKeyId: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] },
|
||||
},
|
||||
});
|
||||
|
||||
securityMockStart.authc.apiKeys.invalidateAsInternalUser.mockResolvedValue({
|
||||
invalidated_api_keys: ['1'],
|
||||
previously_invalidated_api_keys: [],
|
||||
|
@ -1057,7 +1448,7 @@ describe('Invalidate API Keys Task', () => {
|
|||
});
|
||||
expect(result).toEqual(1);
|
||||
|
||||
expect(internalSavedObjectsRepository.find).toHaveBeenCalledTimes(3);
|
||||
expect(internalSavedObjectsRepository.find).toHaveBeenCalledTimes(4);
|
||||
expect(encryptedSavedObjectsClient.getDecryptedAsInternalUser).toHaveBeenCalledTimes(2);
|
||||
expect(securityMockStart.authc.apiKeys.invalidateAsInternalUser).toHaveBeenCalledTimes(1);
|
||||
expect(internalSavedObjectsRepository.delete).toHaveBeenCalledTimes(1);
|
||||
|
@ -1096,6 +1487,20 @@ describe('Invalidate API Keys Task', () => {
|
|||
},
|
||||
},
|
||||
});
|
||||
expect(internalSavedObjectsRepository.find).toHaveBeenNthCalledWith(3, {
|
||||
type: ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE,
|
||||
perPage: 0,
|
||||
filter: `action_task_params.attributes.apiKeyId: "abcd====!" OR action_task_params.attributes.apiKeyId: "xyz!==!"`,
|
||||
namespaces: ['*'],
|
||||
aggs: {
|
||||
apiKeyId: {
|
||||
terms: {
|
||||
field: `action_task_params.attributes.apiKeyId`,
|
||||
size: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(securityMockStart.authc.apiKeys.invalidateAsInternalUser).toHaveBeenNthCalledWith(1, {
|
||||
ids: ['xyz!==!'],
|
||||
});
|
||||
|
@ -1107,7 +1512,7 @@ describe('Invalidate API Keys Task', () => {
|
|||
expect(logger.debug).toHaveBeenNthCalledWith(1, `Total invalidated API keys "1"`);
|
||||
|
||||
// second iteration
|
||||
expect(internalSavedObjectsRepository.find).toHaveBeenNthCalledWith(3, {
|
||||
expect(internalSavedObjectsRepository.find).toHaveBeenNthCalledWith(4, {
|
||||
type: API_KEY_PENDING_INVALIDATION_TYPE,
|
||||
filter: `api_key_pending_invalidation.attributes.createdAt <= "1969-12-31T23:00:00.000Z" AND NOT api_key_pending_invalidation.id: "api_key_pending_invalidation:1"`,
|
||||
page: 1,
|
||||
|
|
|
@ -22,6 +22,7 @@ import {
|
|||
AggregationsStringTermsBucketKeys,
|
||||
AggregationsTermsAggregateBase,
|
||||
} from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import { ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE } from '@kbn/actions-plugin/server/constants/saved_objects';
|
||||
import { InvalidateAPIKeyResult } from '../rules_client';
|
||||
import { AlertingConfig } from '../config';
|
||||
import { timePeriodBeforeDate } from '../lib/get_cadence';
|
||||
|
@ -116,6 +117,7 @@ export function taskRunner(
|
|||
const savedObjectsClient = savedObjects.createInternalRepository([
|
||||
API_KEY_PENDING_INVALIDATION_TYPE,
|
||||
AD_HOC_RUN_SAVED_OBJECT_TYPE,
|
||||
ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE,
|
||||
]);
|
||||
const encryptedSavedObjectsClient = encryptedSavedObjects.getClient({
|
||||
includedHiddenTypes: [API_KEY_PENDING_INVALIDATION_TYPE],
|
||||
|
@ -248,29 +250,14 @@ export async function getApiKeyIdsToInvalidate({
|
|||
);
|
||||
|
||||
// Query saved objects index to see if any API keys are in use
|
||||
const filter = `${apiKeyIds
|
||||
.map(({ apiKeyId }) => `${AD_HOC_RUN_SAVED_OBJECT_TYPE}.attributes.apiKeyId: "${apiKeyId}"`)
|
||||
.join(' OR ')}`;
|
||||
const { aggregations } = await savedObjectsClient.find<
|
||||
AdHocRunSO,
|
||||
{ apiKeyId: AggregationsTermsAggregateBase<AggregationsStringTermsBucketKeys> }
|
||||
>({
|
||||
type: AD_HOC_RUN_SAVED_OBJECT_TYPE,
|
||||
filter,
|
||||
perPage: 0,
|
||||
namespaces: ['*'],
|
||||
aggs: {
|
||||
apiKeyId: {
|
||||
terms: {
|
||||
field: `${AD_HOC_RUN_SAVED_OBJECT_TYPE}.attributes.apiKeyId`,
|
||||
size: PAGE_SIZE,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const apiKeyIdStrings = apiKeyIds.map(({ apiKeyId }) => apiKeyId);
|
||||
let apiKeyIdsInUseBuckets: AggregationsStringTermsBucketKeys[] = [];
|
||||
|
||||
const apiKeyIdsInUseBuckets: AggregationsStringTermsBucketKeys[] =
|
||||
(aggregations?.apiKeyId?.buckets as AggregationsStringTermsBucketKeys[]) ?? [];
|
||||
for (const soType of [AD_HOC_RUN_SAVED_OBJECT_TYPE, ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE]) {
|
||||
apiKeyIdsInUseBuckets = apiKeyIdsInUseBuckets.concat(
|
||||
await queryForApiKeysInUse(apiKeyIdStrings, soType, savedObjectsClient)
|
||||
);
|
||||
}
|
||||
|
||||
const apiKeyIdsToInvalidate: ApiKeyIdAndSOId[] = [];
|
||||
const apiKeyIdsToExclude: ApiKeyIdAndSOId[] = [];
|
||||
|
@ -335,3 +322,33 @@ export async function invalidateApiKeysAndDeletePendingApiKeySavedObject({
|
|||
logger.debug(`Total invalidated API keys "${totalInvalidated}"`);
|
||||
return totalInvalidated;
|
||||
}
|
||||
|
||||
async function queryForApiKeysInUse(
|
||||
apiKeyIds: string[],
|
||||
savedObjectType: string,
|
||||
savedObjectsClient: SavedObjectsClientContract
|
||||
): Promise<AggregationsStringTermsBucketKeys[]> {
|
||||
const filter = `${apiKeyIds
|
||||
.map((apiKeyId) => `${savedObjectType}.attributes.apiKeyId: "${apiKeyId}"`)
|
||||
.join(' OR ')}`;
|
||||
|
||||
const { aggregations } = await savedObjectsClient.find<
|
||||
AdHocRunSO,
|
||||
{ apiKeyId: AggregationsTermsAggregateBase<AggregationsStringTermsBucketKeys> }
|
||||
>({
|
||||
type: savedObjectType,
|
||||
filter,
|
||||
perPage: 0,
|
||||
namespaces: ['*'],
|
||||
aggs: {
|
||||
apiKeyId: {
|
||||
terms: {
|
||||
field: `${savedObjectType}.attributes.apiKeyId`,
|
||||
size: PAGE_SIZE,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return (aggregations?.apiKeyId?.buckets as AggregationsStringTermsBucketKeys[]) ?? [];
|
||||
}
|
||||
|
|
|
@ -46,6 +46,7 @@ describe('findBackfillRoute', () => {
|
|||
tags: ['foo'],
|
||||
alertTypeId: 'myType',
|
||||
params: {},
|
||||
actions: [],
|
||||
apiKeyOwner: 'user',
|
||||
apiKeyCreatedByUser: false,
|
||||
consumer: 'myApp',
|
||||
|
@ -73,6 +74,7 @@ describe('findBackfillRoute', () => {
|
|||
tags: ['foo'],
|
||||
alertTypeId: 'myType',
|
||||
params: {},
|
||||
actions: [],
|
||||
apiKeyOwner: 'user',
|
||||
apiKeyCreatedByUser: false,
|
||||
consumer: 'myApp',
|
||||
|
|
|
@ -35,6 +35,7 @@ describe('getBackfillRoute', () => {
|
|||
tags: ['foo'],
|
||||
alertTypeId: 'myType',
|
||||
params: {},
|
||||
actions: [],
|
||||
apiKeyOwner: 'user',
|
||||
apiKeyCreatedByUser: false,
|
||||
consumer: 'myApp',
|
||||
|
|
|
@ -44,6 +44,7 @@ describe('scheduleBackfillRoute', () => {
|
|||
tags: ['foo'],
|
||||
alertTypeId: 'myType',
|
||||
params: {},
|
||||
actions: [],
|
||||
apiKeyOwner: 'user',
|
||||
apiKeyCreatedByUser: false,
|
||||
consumer: 'myApp',
|
||||
|
@ -71,6 +72,7 @@ describe('scheduleBackfillRoute', () => {
|
|||
tags: ['foo'],
|
||||
alertTypeId: 'myType',
|
||||
params: {},
|
||||
actions: [],
|
||||
apiKeyOwner: 'user',
|
||||
apiKeyCreatedByUser: false,
|
||||
consumer: 'myApp',
|
||||
|
|
|
@ -10,4 +10,9 @@ import { ScheduleBackfillRequestBodyV1 } from '../../../../../../../common/route
|
|||
import { ScheduleBackfillParams } from '../../../../../../application/backfill/methods/schedule/types';
|
||||
|
||||
export const transformRequest = (request: ScheduleBackfillRequestBodyV1): ScheduleBackfillParams =>
|
||||
request.map(({ rule_id, start, end }) => ({ ruleId: rule_id, start, end }));
|
||||
request.map(({ rule_id, start, end, run_actions }) => ({
|
||||
ruleId: rule_id,
|
||||
start,
|
||||
end,
|
||||
runActions: run_actions,
|
||||
}));
|
||||
|
|
|
@ -19,6 +19,7 @@ describe('transformBackfillToBackfillResponse', () => {
|
|||
tags: ['foo'],
|
||||
alertTypeId: 'myType',
|
||||
params: {},
|
||||
actions: [],
|
||||
apiKeyOwner: 'user',
|
||||
apiKeyCreatedByUser: false,
|
||||
consumer: 'myApp',
|
||||
|
@ -49,6 +50,7 @@ describe('transformBackfillToBackfillResponse', () => {
|
|||
name: 'my rule name',
|
||||
tags: ['foo'],
|
||||
rule_type_id: 'myType',
|
||||
actions: [],
|
||||
params: {},
|
||||
api_key_owner: 'user',
|
||||
api_key_created_by_user: false,
|
||||
|
|
|
@ -5,25 +5,21 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import { SavedObjectReference } from '@kbn/core/server';
|
||||
import { ActionsClient } from '@kbn/actions-plugin/server';
|
||||
import {
|
||||
preconfiguredConnectorActionRefPrefix,
|
||||
systemConnectorActionRefPrefix,
|
||||
} from '../common/constants';
|
||||
import {
|
||||
DenormalizedAction,
|
||||
NormalizedAlertActionWithGeneratedValues,
|
||||
RulesClientContext,
|
||||
} from '../types';
|
||||
import { DenormalizedAction, NormalizedAlertActionWithGeneratedValues } from '../types';
|
||||
|
||||
export async function denormalizeActions(
|
||||
context: RulesClientContext,
|
||||
actionsClient: ActionsClient,
|
||||
alertActions: NormalizedAlertActionWithGeneratedValues[]
|
||||
): Promise<{ actions: DenormalizedAction[]; references: SavedObjectReference[] }> {
|
||||
const references: SavedObjectReference[] = [];
|
||||
const actions: DenormalizedAction[] = [];
|
||||
|
||||
if (alertActions.length) {
|
||||
const actionsClient = await context.getActionsClient();
|
||||
const actionIds = [...new Set(alertActions.map((alertAction) => alertAction.id))];
|
||||
|
||||
const actionResults = await actionsClient.getBulk({
|
||||
|
|
|
@ -26,7 +26,11 @@ export async function extractReferences<
|
|||
params: ExtractedParams;
|
||||
references: SavedObjectReference[];
|
||||
}> {
|
||||
const { references: actionReferences, actions } = await denormalizeActions(context, ruleActions);
|
||||
const actionsClient = await context.getActionsClient();
|
||||
const { references: actionReferences, actions } = await denormalizeActions(
|
||||
actionsClient,
|
||||
ruleActions
|
||||
);
|
||||
|
||||
// Extracts any references using configured reference extractor if available
|
||||
const extractedRefsAndParams = ruleType?.useSavedObjectReferences?.extractReferences
|
||||
|
|
|
@ -18,7 +18,11 @@ import { InjectActionParamsOpts, injectActionParams } from '../inject_action_par
|
|||
import { RuleTypeParams, SanitizedRule, GetViewInAppRelativeUrlFnOpts } from '../../types';
|
||||
import { RuleRunMetricsStore } from '../../lib/rule_run_metrics_store';
|
||||
import { alertingEventLoggerMock } from '../../lib/alerting_event_logger/alerting_event_logger.mock';
|
||||
import { ConcreteTaskInstance, TaskErrorSource } from '@kbn/task-manager-plugin/server';
|
||||
import {
|
||||
ConcreteTaskInstance,
|
||||
TaskErrorSource,
|
||||
TaskPriority,
|
||||
} from '@kbn/task-manager-plugin/server';
|
||||
import { RuleNotifyWhen } from '../../../common';
|
||||
import { asSavedObjectExecutionSource } from '@kbn/actions-plugin/server';
|
||||
import sinon from 'sinon';
|
||||
|
@ -116,6 +120,7 @@ describe('Action Scheduler', () => {
|
|||
Object {
|
||||
"actionTypeId": "test",
|
||||
"apiKey": "MTIzOmFiYw==",
|
||||
"apiKeyId": undefined,
|
||||
"consumer": "rule-consumer",
|
||||
"executionId": "5f6aa57d-3e22-484e-bae8-cbed868f4d28",
|
||||
"id": "1",
|
||||
|
@ -125,6 +130,7 @@ describe('Action Scheduler', () => {
|
|||
"foo": true,
|
||||
"stateVal": "My goes here",
|
||||
},
|
||||
"priority": undefined,
|
||||
"relatedSavedObjects": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
|
@ -366,6 +372,7 @@ describe('Action Scheduler', () => {
|
|||
Object {
|
||||
"actionTypeId": "test",
|
||||
"apiKey": "MTIzOmFiYw==",
|
||||
"apiKeyId": undefined,
|
||||
"consumer": "rule-consumer",
|
||||
"executionId": "5f6aa57d-3e22-484e-bae8-cbed868f4d28",
|
||||
"id": "1",
|
||||
|
@ -375,6 +382,7 @@ describe('Action Scheduler', () => {
|
|||
"foo": true,
|
||||
"stateVal": "My goes here",
|
||||
},
|
||||
"priority": undefined,
|
||||
"relatedSavedObjects": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
|
@ -411,6 +419,7 @@ describe('Action Scheduler', () => {
|
|||
Object {
|
||||
"actionTypeId": "test",
|
||||
"apiKey": "MTIzOmFiYw==",
|
||||
"apiKeyId": undefined,
|
||||
"consumer": "rule-consumer",
|
||||
"executionId": "5f6aa57d-3e22-484e-bae8-cbed868f4d28",
|
||||
"id": "1",
|
||||
|
@ -420,6 +429,7 @@ describe('Action Scheduler', () => {
|
|||
"foo": true,
|
||||
"stateVal": "My state-val goes here",
|
||||
},
|
||||
"priority": undefined,
|
||||
"relatedSavedObjects": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
|
@ -769,6 +779,7 @@ describe('Action Scheduler', () => {
|
|||
Object {
|
||||
"actionTypeId": "test",
|
||||
"apiKey": "MTIzOmFiYw==",
|
||||
"apiKeyId": undefined,
|
||||
"consumer": "rule-consumer",
|
||||
"executionId": "5f6aa57d-3e22-484e-bae8-cbed868f4d28",
|
||||
"id": "action-2",
|
||||
|
@ -778,6 +789,7 @@ describe('Action Scheduler', () => {
|
|||
"foo": true,
|
||||
"stateVal": "My goes here",
|
||||
},
|
||||
"priority": undefined,
|
||||
"relatedSavedObjects": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
|
@ -1014,12 +1026,14 @@ describe('Action Scheduler', () => {
|
|||
Object {
|
||||
"actionTypeId": "testActionTypeId",
|
||||
"apiKey": "MTIzOmFiYw==",
|
||||
"apiKeyId": undefined,
|
||||
"consumer": "rule-consumer",
|
||||
"executionId": "5f6aa57d-3e22-484e-bae8-cbed868f4d28",
|
||||
"id": "1",
|
||||
"params": Object {
|
||||
"message": "New: 1 Ongoing: 0 Recovered: 0",
|
||||
},
|
||||
"priority": undefined,
|
||||
"relatedSavedObjects": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
|
@ -1161,12 +1175,14 @@ describe('Action Scheduler', () => {
|
|||
Object {
|
||||
"actionTypeId": "testActionTypeId",
|
||||
"apiKey": "MTIzOmFiYw==",
|
||||
"apiKeyId": undefined,
|
||||
"consumer": "rule-consumer",
|
||||
"executionId": "5f6aa57d-3e22-484e-bae8-cbed868f4d28",
|
||||
"id": "1",
|
||||
"params": Object {
|
||||
"message": "New: 1 Ongoing: 0 Recovered: 0",
|
||||
},
|
||||
"priority": undefined,
|
||||
"relatedSavedObjects": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
|
@ -1413,6 +1429,7 @@ describe('Action Scheduler', () => {
|
|||
Object {
|
||||
"actionTypeId": "test",
|
||||
"apiKey": "MTIzOmFiYw==",
|
||||
"apiKeyId": undefined,
|
||||
"consumer": "rule-consumer",
|
||||
"executionId": "5f6aa57d-3e22-484e-bae8-cbed868f4d28",
|
||||
"id": "1",
|
||||
|
@ -1422,6 +1439,7 @@ describe('Action Scheduler', () => {
|
|||
"foo": true,
|
||||
"stateVal": "My goes here",
|
||||
},
|
||||
"priority": undefined,
|
||||
"relatedSavedObjects": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
|
@ -1443,6 +1461,7 @@ describe('Action Scheduler', () => {
|
|||
Object {
|
||||
"actionTypeId": "test",
|
||||
"apiKey": "MTIzOmFiYw==",
|
||||
"apiKeyId": undefined,
|
||||
"consumer": "rule-consumer",
|
||||
"executionId": "5f6aa57d-3e22-484e-bae8-cbed868f4d28",
|
||||
"id": "2",
|
||||
|
@ -1452,6 +1471,7 @@ describe('Action Scheduler', () => {
|
|||
"foo": true,
|
||||
"stateVal": "My goes here",
|
||||
},
|
||||
"priority": undefined,
|
||||
"relatedSavedObjects": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
|
@ -1974,6 +1994,7 @@ describe('Action Scheduler', () => {
|
|||
Object {
|
||||
"actionTypeId": "test",
|
||||
"apiKey": "MTIzOmFiYw==",
|
||||
"apiKeyId": undefined,
|
||||
"consumer": "rule-consumer",
|
||||
"executionId": "5f6aa57d-3e22-484e-bae8-cbed868f4d28",
|
||||
"id": "1",
|
||||
|
@ -1983,6 +2004,7 @@ describe('Action Scheduler', () => {
|
|||
"foo": true,
|
||||
"stateVal": "My goes here",
|
||||
},
|
||||
"priority": undefined,
|
||||
"relatedSavedObjects": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
|
@ -2004,6 +2026,7 @@ describe('Action Scheduler', () => {
|
|||
Object {
|
||||
"actionTypeId": "test",
|
||||
"apiKey": "MTIzOmFiYw==",
|
||||
"apiKeyId": undefined,
|
||||
"consumer": "rule-consumer",
|
||||
"executionId": "5f6aa57d-3e22-484e-bae8-cbed868f4d28",
|
||||
"id": "1",
|
||||
|
@ -2013,6 +2036,7 @@ describe('Action Scheduler', () => {
|
|||
"foo": true,
|
||||
"stateVal": "My goes here",
|
||||
},
|
||||
"priority": undefined,
|
||||
"relatedSavedObjects": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
|
@ -2034,6 +2058,7 @@ describe('Action Scheduler', () => {
|
|||
Object {
|
||||
"actionTypeId": "test",
|
||||
"apiKey": "MTIzOmFiYw==",
|
||||
"apiKeyId": undefined,
|
||||
"consumer": "rule-consumer",
|
||||
"executionId": "5f6aa57d-3e22-484e-bae8-cbed868f4d28",
|
||||
"id": "1",
|
||||
|
@ -2043,6 +2068,154 @@ describe('Action Scheduler', () => {
|
|||
"foo": true,
|
||||
"stateVal": "My goes here",
|
||||
},
|
||||
"priority": undefined,
|
||||
"relatedSavedObjects": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"namespace": "test1",
|
||||
"type": "alert",
|
||||
"typeId": "test",
|
||||
},
|
||||
],
|
||||
"source": Object {
|
||||
"source": Object {
|
||||
"id": "1",
|
||||
"type": "alert",
|
||||
},
|
||||
"type": "SAVED_OBJECT",
|
||||
},
|
||||
"spaceId": "test1",
|
||||
"uuid": "111-111",
|
||||
},
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test('does schedule actions with priority when specified', async () => {
|
||||
const actionScheduler = new ActionScheduler(
|
||||
getSchedulerContext({
|
||||
...defaultSchedulerContext,
|
||||
priority: TaskPriority.Low,
|
||||
rule: {
|
||||
...defaultSchedulerContext.rule,
|
||||
actions: [
|
||||
{
|
||||
...defaultSchedulerContext.rule.actions[0],
|
||||
frequency: {
|
||||
summary: false,
|
||||
notifyWhen: RuleNotifyWhen.CHANGE,
|
||||
throttle: null,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
await actionScheduler.run({
|
||||
activeCurrentAlerts: {
|
||||
...generateAlert({
|
||||
id: 1,
|
||||
pendingRecoveredCount: 1,
|
||||
lastScheduledActionsGroup: 'recovered',
|
||||
}),
|
||||
...generateAlert({
|
||||
id: 2,
|
||||
pendingRecoveredCount: 1,
|
||||
lastScheduledActionsGroup: 'recovered',
|
||||
}),
|
||||
...generateAlert({
|
||||
id: 3,
|
||||
pendingRecoveredCount: 1,
|
||||
lastScheduledActionsGroup: 'recovered',
|
||||
}),
|
||||
},
|
||||
recoveredCurrentAlerts: {},
|
||||
});
|
||||
|
||||
expect(actionsClient.bulkEnqueueExecution).toHaveBeenCalledTimes(1);
|
||||
expect(actionsClient.bulkEnqueueExecution.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
Object {
|
||||
"actionTypeId": "test",
|
||||
"apiKey": "MTIzOmFiYw==",
|
||||
"apiKeyId": undefined,
|
||||
"consumer": "rule-consumer",
|
||||
"executionId": "5f6aa57d-3e22-484e-bae8-cbed868f4d28",
|
||||
"id": "1",
|
||||
"params": Object {
|
||||
"alertVal": "My 1 name-of-alert test1 tag-A,tag-B 1 goes here",
|
||||
"contextVal": "My goes here",
|
||||
"foo": true,
|
||||
"stateVal": "My goes here",
|
||||
},
|
||||
"priority": 1,
|
||||
"relatedSavedObjects": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"namespace": "test1",
|
||||
"type": "alert",
|
||||
"typeId": "test",
|
||||
},
|
||||
],
|
||||
"source": Object {
|
||||
"source": Object {
|
||||
"id": "1",
|
||||
"type": "alert",
|
||||
},
|
||||
"type": "SAVED_OBJECT",
|
||||
},
|
||||
"spaceId": "test1",
|
||||
"uuid": "111-111",
|
||||
},
|
||||
Object {
|
||||
"actionTypeId": "test",
|
||||
"apiKey": "MTIzOmFiYw==",
|
||||
"apiKeyId": undefined,
|
||||
"consumer": "rule-consumer",
|
||||
"executionId": "5f6aa57d-3e22-484e-bae8-cbed868f4d28",
|
||||
"id": "1",
|
||||
"params": Object {
|
||||
"alertVal": "My 1 name-of-alert test1 tag-A,tag-B 2 goes here",
|
||||
"contextVal": "My goes here",
|
||||
"foo": true,
|
||||
"stateVal": "My goes here",
|
||||
},
|
||||
"priority": 1,
|
||||
"relatedSavedObjects": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"namespace": "test1",
|
||||
"type": "alert",
|
||||
"typeId": "test",
|
||||
},
|
||||
],
|
||||
"source": Object {
|
||||
"source": Object {
|
||||
"id": "1",
|
||||
"type": "alert",
|
||||
},
|
||||
"type": "SAVED_OBJECT",
|
||||
},
|
||||
"spaceId": "test1",
|
||||
"uuid": "111-111",
|
||||
},
|
||||
Object {
|
||||
"actionTypeId": "test",
|
||||
"apiKey": "MTIzOmFiYw==",
|
||||
"apiKeyId": undefined,
|
||||
"consumer": "rule-consumer",
|
||||
"executionId": "5f6aa57d-3e22-484e-bae8-cbed868f4d28",
|
||||
"id": "1",
|
||||
"params": Object {
|
||||
"alertVal": "My 1 name-of-alert test1 tag-A,tag-B 3 goes here",
|
||||
"contextVal": "My goes here",
|
||||
"foo": true,
|
||||
"stateVal": "My goes here",
|
||||
},
|
||||
"priority": 1,
|
||||
"relatedSavedObjects": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
|
@ -2523,6 +2696,7 @@ describe('Action Scheduler', () => {
|
|||
Object {
|
||||
"actionTypeId": ".test-system-action",
|
||||
"apiKey": "MTIzOmFiYw==",
|
||||
"apiKeyId": undefined,
|
||||
"consumer": "rule-consumer",
|
||||
"executionId": "5f6aa57d-3e22-484e-bae8-cbed868f4d28",
|
||||
"id": "1",
|
||||
|
@ -2530,6 +2704,7 @@ describe('Action Scheduler', () => {
|
|||
"foo": "bar",
|
||||
"myParams": "test",
|
||||
},
|
||||
"priority": undefined,
|
||||
"relatedSavedObjects": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
|
|
|
@ -6,16 +6,17 @@
|
|||
*/
|
||||
|
||||
import { Logger } from '@kbn/logging';
|
||||
import { RuleTypeParams, SanitizedRule } from '@kbn/alerting-types';
|
||||
import { RuleTypeParams } from '@kbn/alerting-types';
|
||||
import { getRuleDetailsRoute, triggersActionsRoute } from '@kbn/rule-data-utils';
|
||||
import { GetViewInAppRelativeUrlFn } from '../../../types';
|
||||
import { ActionSchedulerRule } from '../types';
|
||||
|
||||
interface BuildRuleUrlOpts<Params extends RuleTypeParams> {
|
||||
end?: number;
|
||||
getViewInAppRelativeUrl?: GetViewInAppRelativeUrlFn<Params>;
|
||||
kibanaBaseUrl: string | undefined;
|
||||
logger: Logger;
|
||||
rule: SanitizedRule<Params>;
|
||||
rule: ActionSchedulerRule<Params>;
|
||||
spaceId: string;
|
||||
start?: number;
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { TaskPriority } from '@kbn/task-manager-plugin/server';
|
||||
import { RULE_SAVED_OBJECT_TYPE } from '../../..';
|
||||
import { formatActionToEnqueue } from './format_action_to_enqueue';
|
||||
|
||||
|
@ -65,6 +66,124 @@ describe('formatActionToEnqueue', () => {
|
|||
});
|
||||
});
|
||||
|
||||
test('should format a rule action with priority as expected', () => {
|
||||
expect(
|
||||
formatActionToEnqueue({
|
||||
action: {
|
||||
id: '1',
|
||||
group: 'default',
|
||||
actionTypeId: 'test',
|
||||
params: {
|
||||
foo: true,
|
||||
contextVal: 'My {{context.value}} goes here',
|
||||
stateVal: 'My {{state.value}} goes here',
|
||||
alertVal:
|
||||
'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here',
|
||||
},
|
||||
uuid: '111-111',
|
||||
},
|
||||
apiKey: 'MTIzOmFiYw==',
|
||||
executionId: '123',
|
||||
ruleConsumer: 'rule-consumer',
|
||||
ruleId: 'aaa',
|
||||
ruleTypeId: 'security-rule',
|
||||
spaceId: 'default',
|
||||
priority: TaskPriority.Low,
|
||||
})
|
||||
).toEqual({
|
||||
id: '1',
|
||||
uuid: '111-111',
|
||||
params: {
|
||||
foo: true,
|
||||
contextVal: 'My {{context.value}} goes here',
|
||||
stateVal: 'My {{state.value}} goes here',
|
||||
alertVal:
|
||||
'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here',
|
||||
},
|
||||
spaceId: 'default',
|
||||
apiKey: 'MTIzOmFiYw==',
|
||||
consumer: 'rule-consumer',
|
||||
source: {
|
||||
source: {
|
||||
id: 'aaa',
|
||||
type: RULE_SAVED_OBJECT_TYPE,
|
||||
},
|
||||
type: 'SAVED_OBJECT',
|
||||
},
|
||||
executionId: '123',
|
||||
relatedSavedObjects: [
|
||||
{
|
||||
id: 'aaa',
|
||||
type: RULE_SAVED_OBJECT_TYPE,
|
||||
namespace: undefined,
|
||||
typeId: 'security-rule',
|
||||
},
|
||||
],
|
||||
actionTypeId: 'test',
|
||||
priority: 1,
|
||||
});
|
||||
});
|
||||
|
||||
test('should format a rule action with apiKeyId as expected', () => {
|
||||
expect(
|
||||
formatActionToEnqueue({
|
||||
action: {
|
||||
id: '1',
|
||||
group: 'default',
|
||||
actionTypeId: 'test',
|
||||
params: {
|
||||
foo: true,
|
||||
contextVal: 'My {{context.value}} goes here',
|
||||
stateVal: 'My {{state.value}} goes here',
|
||||
alertVal:
|
||||
'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here',
|
||||
},
|
||||
uuid: '111-111',
|
||||
},
|
||||
apiKey: 'MTIzOmFiYw==',
|
||||
apiKeyId: '4534623462346',
|
||||
executionId: '123',
|
||||
ruleConsumer: 'rule-consumer',
|
||||
ruleId: 'aaa',
|
||||
ruleTypeId: 'security-rule',
|
||||
spaceId: 'default',
|
||||
priority: TaskPriority.Low,
|
||||
})
|
||||
).toEqual({
|
||||
id: '1',
|
||||
uuid: '111-111',
|
||||
params: {
|
||||
foo: true,
|
||||
contextVal: 'My {{context.value}} goes here',
|
||||
stateVal: 'My {{state.value}} goes here',
|
||||
alertVal:
|
||||
'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here',
|
||||
},
|
||||
spaceId: 'default',
|
||||
apiKey: 'MTIzOmFiYw==',
|
||||
consumer: 'rule-consumer',
|
||||
source: {
|
||||
source: {
|
||||
id: 'aaa',
|
||||
type: RULE_SAVED_OBJECT_TYPE,
|
||||
},
|
||||
type: 'SAVED_OBJECT',
|
||||
},
|
||||
executionId: '123',
|
||||
relatedSavedObjects: [
|
||||
{
|
||||
id: 'aaa',
|
||||
type: RULE_SAVED_OBJECT_TYPE,
|
||||
namespace: undefined,
|
||||
typeId: 'security-rule',
|
||||
},
|
||||
],
|
||||
actionTypeId: 'test',
|
||||
priority: 1,
|
||||
apiKeyId: '4534623462346',
|
||||
});
|
||||
});
|
||||
|
||||
test('should format a rule action with null apiKey as expected', () => {
|
||||
expect(
|
||||
formatActionToEnqueue({
|
||||
|
|
|
@ -7,12 +7,15 @@
|
|||
|
||||
import { RuleAction, RuleSystemAction } from '@kbn/alerting-types';
|
||||
import { asSavedObjectExecutionSource } from '@kbn/actions-plugin/server';
|
||||
import { TaskPriority } from '@kbn/task-manager-plugin/server';
|
||||
import { RULE_SAVED_OBJECT_TYPE } from '../../..';
|
||||
|
||||
interface FormatActionToEnqueueOpts {
|
||||
action: RuleAction | RuleSystemAction;
|
||||
apiKeyId?: string;
|
||||
apiKey: string | null;
|
||||
executionId: string;
|
||||
priority?: TaskPriority;
|
||||
ruleConsumer: string;
|
||||
ruleId: string;
|
||||
ruleTypeId: string;
|
||||
|
@ -20,7 +23,17 @@ interface FormatActionToEnqueueOpts {
|
|||
}
|
||||
|
||||
export const formatActionToEnqueue = (opts: FormatActionToEnqueueOpts) => {
|
||||
const { action, apiKey, executionId, ruleConsumer, ruleId, ruleTypeId, spaceId } = opts;
|
||||
const {
|
||||
action,
|
||||
apiKey,
|
||||
apiKeyId,
|
||||
executionId,
|
||||
priority,
|
||||
ruleConsumer,
|
||||
ruleId,
|
||||
ruleTypeId,
|
||||
spaceId,
|
||||
} = opts;
|
||||
|
||||
const namespace = spaceId === 'default' ? {} : { namespace: spaceId };
|
||||
return {
|
||||
|
@ -29,6 +42,7 @@ export const formatActionToEnqueue = (opts: FormatActionToEnqueueOpts) => {
|
|||
params: action.params,
|
||||
spaceId,
|
||||
apiKey: apiKey ?? null,
|
||||
apiKeyId,
|
||||
consumer: ruleConsumer,
|
||||
source: asSavedObjectExecutionSource({
|
||||
id: ruleId,
|
||||
|
@ -44,5 +58,6 @@ export const formatActionToEnqueue = (opts: FormatActionToEnqueueOpts) => {
|
|||
},
|
||||
],
|
||||
actionTypeId: action.actionTypeId,
|
||||
priority,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -22,6 +22,7 @@ import {
|
|||
AlertInstanceContext,
|
||||
AlertInstanceState,
|
||||
} from '@kbn/alerting-state-types';
|
||||
import { TaskPriority } from '@kbn/task-manager-plugin/server';
|
||||
|
||||
const alertingEventLogger = alertingEventLoggerMock.create();
|
||||
const actionsClient = actionsClientMock.create();
|
||||
|
@ -91,7 +92,13 @@ const getSchedulerContext = (params = {}) => {
|
|||
return { ...defaultSchedulerContext, rule, ...params, ruleRunMetricsStore };
|
||||
};
|
||||
|
||||
const getResult = (actionId: string, alertId: string, actionUuid: string) => ({
|
||||
const getResult = (
|
||||
actionId: string,
|
||||
alertId: string,
|
||||
actionUuid: string,
|
||||
priority?: number,
|
||||
apiKeyId?: string
|
||||
) => ({
|
||||
actionToEnqueue: {
|
||||
actionTypeId: 'test',
|
||||
apiKey: 'MTIzOmFiYw==',
|
||||
|
@ -102,6 +109,8 @@ const getResult = (actionId: string, alertId: string, actionUuid: string) => ({
|
|||
relatedSavedObjects: [{ id: 'rule-id-1', namespace: 'test1', type: 'alert', typeId: 'test' }],
|
||||
source: { source: { id: 'rule-id-1', type: 'alert' }, type: 'SAVED_OBJECT' },
|
||||
spaceId: 'test1',
|
||||
...(priority ? { priority } : {}),
|
||||
...(apiKeyId ? { apiKeyId } : {}),
|
||||
},
|
||||
actionToLog: { alertGroup: 'default', alertId, id: actionId, uuid: actionUuid, typeId: 'test' },
|
||||
});
|
||||
|
@ -236,6 +245,64 @@ describe('Per-Alert Action Scheduler', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
test('test should create action to schedule with priority if specified for each alert and each action', async () => {
|
||||
// 2 per-alert actions * 2 alerts = 4 actions to schedule
|
||||
const scheduler = new PerAlertActionScheduler({
|
||||
...getSchedulerContext(),
|
||||
priority: TaskPriority.Low,
|
||||
});
|
||||
const results = await scheduler.getActionsToSchedule({
|
||||
activeCurrentAlerts: alerts,
|
||||
});
|
||||
|
||||
expect(alertsClient.getSummarizedAlerts).not.toHaveBeenCalled();
|
||||
expect(logger.debug).not.toHaveBeenCalled();
|
||||
|
||||
expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(4);
|
||||
expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(4);
|
||||
expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({
|
||||
numberOfGeneratedActions: 4,
|
||||
numberOfTriggeredActions: 4,
|
||||
});
|
||||
|
||||
expect(results).toHaveLength(4);
|
||||
expect(results).toEqual([
|
||||
getResult('action-1', '1', '111-111', 1),
|
||||
getResult('action-1', '2', '111-111', 1),
|
||||
getResult('action-2', '1', '222-222', 1),
|
||||
getResult('action-2', '2', '222-222', 1),
|
||||
]);
|
||||
});
|
||||
|
||||
test('test should create action to schedule with apiKeyId if specified for each alert and each action', async () => {
|
||||
// 2 per-alert actions * 2 alerts = 4 actions to schedule
|
||||
const scheduler = new PerAlertActionScheduler({
|
||||
...getSchedulerContext(),
|
||||
apiKeyId: '23534ybfsdsnsdf',
|
||||
});
|
||||
const results = await scheduler.getActionsToSchedule({
|
||||
activeCurrentAlerts: alerts,
|
||||
});
|
||||
|
||||
expect(alertsClient.getSummarizedAlerts).not.toHaveBeenCalled();
|
||||
expect(logger.debug).not.toHaveBeenCalled();
|
||||
|
||||
expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(4);
|
||||
expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(4);
|
||||
expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({
|
||||
numberOfGeneratedActions: 4,
|
||||
numberOfTriggeredActions: 4,
|
||||
});
|
||||
|
||||
expect(results).toHaveLength(4);
|
||||
expect(results).toEqual([
|
||||
getResult('action-1', '1', '111-111', undefined, '23534ybfsdsnsdf'),
|
||||
getResult('action-1', '2', '111-111', undefined, '23534ybfsdsnsdf'),
|
||||
getResult('action-2', '1', '222-222', undefined, '23534ybfsdsnsdf'),
|
||||
getResult('action-2', '2', '222-222', undefined, '23534ybfsdsnsdf'),
|
||||
]);
|
||||
});
|
||||
|
||||
test('should skip creating actions to schedule when alert has maintenance window', async () => {
|
||||
// 2 per-alert actions * 2 alerts = 4 actions to schedule
|
||||
// but alert 1 has maintenance window, so only actions for alert 2 should be scheduled
|
||||
|
|
|
@ -240,7 +240,9 @@ export class PerAlertActionScheduler<
|
|||
actionToEnqueue: formatActionToEnqueue({
|
||||
action: actionToRun,
|
||||
apiKey: this.context.apiKey,
|
||||
apiKeyId: this.context.apiKeyId,
|
||||
executionId: this.context.executionId,
|
||||
priority: this.context.priority,
|
||||
ruleConsumer: this.context.ruleConsumer,
|
||||
ruleId: this.context.rule.id,
|
||||
ruleTypeId: this.context.ruleType.id,
|
||||
|
|
|
@ -28,6 +28,7 @@ import {
|
|||
} from '@kbn/task-manager-plugin/server/task_running/errors';
|
||||
import { CombinedSummarizedAlerts } from '../../../types';
|
||||
import { ActionsCompletion } from '@kbn/alerting-state-types';
|
||||
import { TaskPriority } from '@kbn/task-manager-plugin/server';
|
||||
|
||||
const alertingEventLogger = alertingEventLoggerMock.create();
|
||||
const actionsClient = actionsClientMock.create();
|
||||
|
@ -97,7 +98,13 @@ const getSchedulerContext = (params = {}) => {
|
|||
return { ...defaultSchedulerContext, rule, ...params, ruleRunMetricsStore };
|
||||
};
|
||||
|
||||
const getResult = (actionId: string, actionUuid: string, summary: CombinedSummarizedAlerts) => ({
|
||||
const getResult = (
|
||||
actionId: string,
|
||||
actionUuid: string,
|
||||
summary: CombinedSummarizedAlerts,
|
||||
priority?: number,
|
||||
apiKeyId?: string
|
||||
) => ({
|
||||
actionToEnqueue: {
|
||||
actionTypeId: 'test',
|
||||
apiKey: 'MTIzOmFiYw==',
|
||||
|
@ -108,6 +115,8 @@ const getResult = (actionId: string, actionUuid: string, summary: CombinedSummar
|
|||
relatedSavedObjects: [{ id: 'rule-id-1', namespace: 'test1', type: 'alert', typeId: 'test' }],
|
||||
source: { source: { id: 'rule-id-1', type: 'alert' }, type: 'SAVED_OBJECT' },
|
||||
spaceId: 'test1',
|
||||
...(priority && { priority }),
|
||||
...(apiKeyId && { apiKeyId }),
|
||||
},
|
||||
actionToLog: {
|
||||
alertSummary: {
|
||||
|
@ -261,6 +270,108 @@ describe('Summary Action Scheduler', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
test('should create action to schedule with priority if specified for summary action when summary action is per rule run', async () => {
|
||||
alertsClient.getProcessedAlerts.mockReturnValue(alerts);
|
||||
const summarizedAlerts = {
|
||||
new: { count: 2, data: [mockAAD, mockAAD] },
|
||||
ongoing: { count: 0, data: [] },
|
||||
recovered: { count: 0, data: [] },
|
||||
};
|
||||
alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts);
|
||||
|
||||
const throttledSummaryActions = {};
|
||||
const scheduler = new SummaryActionScheduler({
|
||||
...getSchedulerContext(),
|
||||
priority: TaskPriority.Low,
|
||||
});
|
||||
const results = await scheduler.getActionsToSchedule({
|
||||
activeCurrentAlerts: alerts,
|
||||
throttledSummaryActions,
|
||||
});
|
||||
|
||||
expect(throttledSummaryActions).toEqual({});
|
||||
expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(2);
|
||||
expect(alertsClient.getSummarizedAlerts).toHaveBeenNthCalledWith(1, {
|
||||
excludedAlertInstanceIds: [],
|
||||
executionUuid: defaultSchedulerContext.executionId,
|
||||
ruleId: 'rule-id-1',
|
||||
spaceId: 'test1',
|
||||
});
|
||||
expect(alertsClient.getSummarizedAlerts).toHaveBeenNthCalledWith(2, {
|
||||
excludedAlertInstanceIds: [],
|
||||
executionUuid: defaultSchedulerContext.executionId,
|
||||
ruleId: 'rule-id-1',
|
||||
spaceId: 'test1',
|
||||
});
|
||||
expect(logger.debug).not.toHaveBeenCalled();
|
||||
|
||||
expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(2);
|
||||
expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(2);
|
||||
expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({
|
||||
numberOfGeneratedActions: 2,
|
||||
numberOfTriggeredActions: 2,
|
||||
});
|
||||
|
||||
expect(results).toHaveLength(2);
|
||||
|
||||
const finalSummary = { ...summarizedAlerts, all: { count: 2, data: [mockAAD, mockAAD] } };
|
||||
expect(results).toEqual([
|
||||
getResult('action-2', '222-222', finalSummary, 1),
|
||||
getResult('action-3', '333-333', finalSummary, 1),
|
||||
]);
|
||||
});
|
||||
|
||||
test('should create action to schedule with apiKeyId if specified for summary action when summary action is per rule run', async () => {
|
||||
alertsClient.getProcessedAlerts.mockReturnValue(alerts);
|
||||
const summarizedAlerts = {
|
||||
new: { count: 2, data: [mockAAD, mockAAD] },
|
||||
ongoing: { count: 0, data: [] },
|
||||
recovered: { count: 0, data: [] },
|
||||
};
|
||||
alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts);
|
||||
|
||||
const throttledSummaryActions = {};
|
||||
const scheduler = new SummaryActionScheduler({
|
||||
...getSchedulerContext(),
|
||||
apiKeyId: '24534wy3wydfbs',
|
||||
});
|
||||
const results = await scheduler.getActionsToSchedule({
|
||||
activeCurrentAlerts: alerts,
|
||||
throttledSummaryActions,
|
||||
});
|
||||
|
||||
expect(throttledSummaryActions).toEqual({});
|
||||
expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(2);
|
||||
expect(alertsClient.getSummarizedAlerts).toHaveBeenNthCalledWith(1, {
|
||||
excludedAlertInstanceIds: [],
|
||||
executionUuid: defaultSchedulerContext.executionId,
|
||||
ruleId: 'rule-id-1',
|
||||
spaceId: 'test1',
|
||||
});
|
||||
expect(alertsClient.getSummarizedAlerts).toHaveBeenNthCalledWith(2, {
|
||||
excludedAlertInstanceIds: [],
|
||||
executionUuid: defaultSchedulerContext.executionId,
|
||||
ruleId: 'rule-id-1',
|
||||
spaceId: 'test1',
|
||||
});
|
||||
expect(logger.debug).not.toHaveBeenCalled();
|
||||
|
||||
expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(2);
|
||||
expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(2);
|
||||
expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({
|
||||
numberOfGeneratedActions: 2,
|
||||
numberOfTriggeredActions: 2,
|
||||
});
|
||||
|
||||
expect(results).toHaveLength(2);
|
||||
|
||||
const finalSummary = { ...summarizedAlerts, all: { count: 2, data: [mockAAD, mockAAD] } };
|
||||
expect(results).toEqual([
|
||||
getResult('action-2', '222-222', finalSummary, undefined, '24534wy3wydfbs'),
|
||||
getResult('action-3', '333-333', finalSummary, undefined, '24534wy3wydfbs'),
|
||||
]);
|
||||
});
|
||||
|
||||
test('should create actions to schedule for summary action when summary action has alertsFilter', async () => {
|
||||
alertsClient.getProcessedAlerts.mockReturnValue(alerts);
|
||||
const summarizedAlerts = {
|
||||
|
|
|
@ -206,7 +206,9 @@ export class SummaryActionScheduler<
|
|||
actionToEnqueue: formatActionToEnqueue({
|
||||
action: actionToRun,
|
||||
apiKey: this.context.apiKey,
|
||||
apiKeyId: this.context.apiKeyId,
|
||||
executionId: this.context.executionId,
|
||||
priority: this.context.priority,
|
||||
ruleConsumer: this.context.ruleConsumer,
|
||||
ruleId: this.context.rule.id,
|
||||
ruleTypeId: this.context.ruleType.id,
|
||||
|
|
|
@ -27,6 +27,7 @@ import {
|
|||
} from '@kbn/task-manager-plugin/server/task_running/errors';
|
||||
import { CombinedSummarizedAlerts } from '../../../types';
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { TaskPriority } from '@kbn/task-manager-plugin/server';
|
||||
|
||||
const alertingEventLogger = alertingEventLoggerMock.create();
|
||||
const actionsClient = actionsClientMock.create();
|
||||
|
@ -68,7 +69,13 @@ const getSchedulerContext = (params = {}) => {
|
|||
return { ...defaultSchedulerContext, rule, ...params, ruleRunMetricsStore };
|
||||
};
|
||||
|
||||
const getResult = (actionId: string, actionUuid: string, summary: CombinedSummarizedAlerts) => ({
|
||||
const getResult = (
|
||||
actionId: string,
|
||||
actionUuid: string,
|
||||
summary: CombinedSummarizedAlerts,
|
||||
priority?: number,
|
||||
apiKeyId?: string
|
||||
) => ({
|
||||
actionToEnqueue: {
|
||||
actionTypeId: '.test-system-action',
|
||||
apiKey: 'MTIzOmFiYw==',
|
||||
|
@ -79,6 +86,8 @@ const getResult = (actionId: string, actionUuid: string, summary: CombinedSummar
|
|||
relatedSavedObjects: [{ id: 'rule-id-1', namespace: 'test1', type: 'alert', typeId: 'test' }],
|
||||
source: { source: { id: 'rule-id-1', type: 'alert' }, type: 'SAVED_OBJECT' },
|
||||
spaceId: 'test1',
|
||||
...(priority && { priority }),
|
||||
...(apiKeyId && { apiKeyId }),
|
||||
},
|
||||
actionToLog: {
|
||||
alertSummary: {
|
||||
|
@ -183,6 +192,82 @@ describe('System Action Scheduler', () => {
|
|||
expect(results).toEqual([getResult('system-action-1', 'xxx-xxx', finalSummary)]);
|
||||
});
|
||||
|
||||
test('should create actions to schedule with priority if specified for each system action', async () => {
|
||||
alertsClient.getProcessedAlerts.mockReturnValue(alerts);
|
||||
|
||||
const summarizedAlerts = {
|
||||
new: { count: 2, data: [mockAAD, mockAAD] },
|
||||
ongoing: { count: 0, data: [] },
|
||||
recovered: { count: 0, data: [] },
|
||||
};
|
||||
alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts);
|
||||
|
||||
const scheduler = new SystemActionScheduler({
|
||||
...getSchedulerContext(),
|
||||
priority: TaskPriority.Low,
|
||||
});
|
||||
const results = await scheduler.getActionsToSchedule({});
|
||||
|
||||
expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(1);
|
||||
expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledWith({
|
||||
excludedAlertInstanceIds: [],
|
||||
executionUuid: defaultSchedulerContext.executionId,
|
||||
ruleId: 'rule-id-1',
|
||||
spaceId: 'test1',
|
||||
});
|
||||
|
||||
expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(1);
|
||||
expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(1);
|
||||
expect(ruleRunMetricsStore.getStatusByConnectorType('.test-system-action')).toEqual({
|
||||
numberOfGeneratedActions: 1,
|
||||
numberOfTriggeredActions: 1,
|
||||
});
|
||||
|
||||
expect(results).toHaveLength(1);
|
||||
|
||||
const finalSummary = { ...summarizedAlerts, all: { count: 2, data: [mockAAD, mockAAD] } };
|
||||
expect(results).toEqual([getResult('system-action-1', 'xxx-xxx', finalSummary, 1)]);
|
||||
});
|
||||
|
||||
test('should create actions to schedule with apiKeyId if specified for each system action', async () => {
|
||||
alertsClient.getProcessedAlerts.mockReturnValue(alerts);
|
||||
|
||||
const summarizedAlerts = {
|
||||
new: { count: 2, data: [mockAAD, mockAAD] },
|
||||
ongoing: { count: 0, data: [] },
|
||||
recovered: { count: 0, data: [] },
|
||||
};
|
||||
alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts);
|
||||
|
||||
const scheduler = new SystemActionScheduler({
|
||||
...getSchedulerContext(),
|
||||
apiKeyId: '464tfbwer5q43h',
|
||||
});
|
||||
const results = await scheduler.getActionsToSchedule({});
|
||||
|
||||
expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(1);
|
||||
expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledWith({
|
||||
excludedAlertInstanceIds: [],
|
||||
executionUuid: defaultSchedulerContext.executionId,
|
||||
ruleId: 'rule-id-1',
|
||||
spaceId: 'test1',
|
||||
});
|
||||
|
||||
expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(1);
|
||||
expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(1);
|
||||
expect(ruleRunMetricsStore.getStatusByConnectorType('.test-system-action')).toEqual({
|
||||
numberOfGeneratedActions: 1,
|
||||
numberOfTriggeredActions: 1,
|
||||
});
|
||||
|
||||
expect(results).toHaveLength(1);
|
||||
|
||||
const finalSummary = { ...summarizedAlerts, all: { count: 2, data: [mockAAD, mockAAD] } };
|
||||
expect(results).toEqual([
|
||||
getResult('system-action-1', 'xxx-xxx', finalSummary, undefined, '464tfbwer5q43h'),
|
||||
]);
|
||||
});
|
||||
|
||||
test('should remove new alerts from summary if suppressed by maintenance window', async () => {
|
||||
const newAlertWithMaintenanceWindow = generateAlert({
|
||||
id: 1,
|
||||
|
|
|
@ -156,7 +156,9 @@ export class SystemActionScheduler<
|
|||
actionToEnqueue: formatActionToEnqueue({
|
||||
action: actionToRun,
|
||||
apiKey: this.context.apiKey,
|
||||
apiKeyId: this.context.apiKeyId,
|
||||
executionId: this.context.executionId,
|
||||
priority: this.context.priority,
|
||||
ruleConsumer: this.context.ruleConsumer,
|
||||
ruleId: this.context.rule.id,
|
||||
ruleTypeId: this.context.ruleType.id,
|
||||
|
|
|
@ -9,6 +9,7 @@ import type { Logger } from '@kbn/core/server';
|
|||
import { PublicMethodsOf } from '@kbn/utility-types';
|
||||
import { ActionsClient } from '@kbn/actions-plugin/server/actions_client';
|
||||
import { ExecuteOptions as EnqueueExecutionOptions } from '@kbn/actions-plugin/server/create_execute_function';
|
||||
import { TaskPriority } from '@kbn/task-manager-plugin/server';
|
||||
import { IAlertsClient } from '../../alerts_client/types';
|
||||
import { Alert } from '../../alert';
|
||||
import {
|
||||
|
@ -31,6 +32,10 @@ import {
|
|||
} from '../../lib/alerting_event_logger/alerting_event_logger';
|
||||
import { RuleTaskInstance, TaskRunnerContext } from '../types';
|
||||
|
||||
export type ActionSchedulerRule<Params extends RuleTypeParams> = Omit<
|
||||
SanitizedRule<Params>,
|
||||
'executionStatus'
|
||||
>;
|
||||
export interface ActionSchedulerOptions<
|
||||
Params extends RuleTypeParams,
|
||||
ExtractedParams extends RuleTypeParams,
|
||||
|
@ -53,10 +58,11 @@ export interface ActionSchedulerOptions<
|
|||
>;
|
||||
logger: Logger;
|
||||
alertingEventLogger: PublicMethodsOf<AlertingEventLogger>;
|
||||
rule: SanitizedRule<Params>;
|
||||
rule: ActionSchedulerRule<Params>;
|
||||
taskRunnerContext: TaskRunnerContext;
|
||||
taskInstance: RuleTaskInstance;
|
||||
ruleRunMetricsStore: RuleRunMetricsStore;
|
||||
apiKeyId?: string;
|
||||
apiKey: RawRule['apiKey'];
|
||||
ruleConsumer: string;
|
||||
executionId: string;
|
||||
|
@ -64,6 +70,7 @@ export interface ActionSchedulerOptions<
|
|||
previousStartedAt: Date | null;
|
||||
actionsClient: PublicMethodsOf<ActionsClient>;
|
||||
alertsClient: IAlertsClient<AlertData, State, Context, ActionGroupIds, RecoveryActionGroupId>;
|
||||
priority?: TaskPriority;
|
||||
}
|
||||
|
||||
export type Executable<
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import sinon from 'sinon';
|
||||
import { PluginStartContract as ActionsPluginStart } from '@kbn/actions-plugin/server';
|
||||
import { actionsMock } from '@kbn/actions-plugin/server/mocks';
|
||||
import { actionsClientMock, actionsMock } from '@kbn/actions-plugin/server/mocks';
|
||||
import { SavedObject } from '@kbn/core/server';
|
||||
import {
|
||||
elasticsearchServiceMock,
|
||||
|
@ -25,7 +25,7 @@ import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/s
|
|||
import { IEventLogger } from '@kbn/event-log-plugin/server';
|
||||
import { eventLoggerMock } from '@kbn/event-log-plugin/server/mocks';
|
||||
import { SharePluginStart } from '@kbn/share-plugin/server';
|
||||
import { ConcreteTaskInstance, TaskStatus } from '@kbn/task-manager-plugin/server';
|
||||
import { ConcreteTaskInstance, TaskPriority, TaskStatus } from '@kbn/task-manager-plugin/server';
|
||||
import { usageCountersServiceMock } from '@kbn/usage-collection-plugin/server/usage_counters/usage_counters_service.mock';
|
||||
import { AdHocTaskRunner } from './ad_hoc_task_runner';
|
||||
import { TaskRunnerContext } from './types';
|
||||
|
@ -39,7 +39,7 @@ import {
|
|||
import { AdHocRunSchedule, AdHocRunSO } from '../data/ad_hoc_run/types';
|
||||
import { AD_HOC_RUN_SAVED_OBJECT_TYPE, RULE_SAVED_OBJECT_TYPE } from '../saved_objects';
|
||||
import { adHocRunStatus } from '../../common/constants';
|
||||
import { DATE_1970, ruleType } from './fixtures';
|
||||
import { DATE_1970, generateEnqueueFunctionInput, mockAAD, ruleType } from './fixtures';
|
||||
import { alertingEventLoggerMock } from '../lib/alerting_event_logger/alerting_event_logger.mock';
|
||||
import { alertsMock } from '../mocks';
|
||||
import { UntypedNormalizedRuleType } from '../rule_type_registry';
|
||||
|
@ -93,6 +93,8 @@ import { RuleRunMetricsStore } from '../lib/rule_run_metrics_store';
|
|||
import { ConnectorAdapterRegistry } from '../connector_adapters/connector_adapter_registry';
|
||||
import { rulesSettingsServiceMock } from '../rules_settings/rules_settings_service.mock';
|
||||
import { maintenanceWindowsServiceMock } from './maintenance_windows/maintenance_windows_service.mock';
|
||||
import { alertsClientMock } from '../alerts_client/alerts_client.mock';
|
||||
import { alertsServiceMock } from '../alerts_service/alerts_service.mock';
|
||||
|
||||
const UUID = '5f6aa57d-3e22-484e-bae8-cbed868f4d28';
|
||||
|
||||
|
@ -121,7 +123,7 @@ type TaskRunnerFactoryInitializerParamsType = jest.Mocked<TaskRunnerContext> & {
|
|||
executionContext: ReturnType<typeof executionContextServiceMock.createInternalStartContract>;
|
||||
};
|
||||
const clusterClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
|
||||
|
||||
const mockAlertsService = alertsServiceMock.create();
|
||||
const alertingEventLogger = alertingEventLoggerMock.create();
|
||||
const elasticsearchAndSOAvailability$ = of(true);
|
||||
const alertsService = new AlertsService({
|
||||
|
@ -133,6 +135,8 @@ const alertsService = new AlertsService({
|
|||
elasticsearchAndSOAvailability$,
|
||||
isServerless: false,
|
||||
});
|
||||
const alertsClient = alertsClientMock.create();
|
||||
const actionsClient = actionsClientMock.create();
|
||||
const backfillClient = backfillClientMock.create();
|
||||
const dataPlugin = dataPluginMock.createStartContract();
|
||||
const dataViewsMock = {
|
||||
|
@ -282,7 +286,7 @@ describe('Ad Hoc Task Runner', () => {
|
|||
name: 'test',
|
||||
tags: [],
|
||||
alertTypeId: 'siem.queryRule',
|
||||
// @ts-expect-error
|
||||
actions: [],
|
||||
params: {
|
||||
author: [],
|
||||
description: 'test',
|
||||
|
@ -369,10 +373,22 @@ describe('Ad Hoc Task Runner', () => {
|
|||
taskRunnerFactoryInitializerParams.executionContext.withContext.mockImplementation((ctx, fn) =>
|
||||
fn()
|
||||
);
|
||||
taskRunnerFactoryInitializerParams.actionsPlugin.getActionsClientWithRequest.mockResolvedValue(
|
||||
actionsClient
|
||||
);
|
||||
taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true);
|
||||
taskRunnerFactoryInitializerParams.actionsPlugin.renderActionParameterTemplates.mockImplementation(
|
||||
(_, __, params) => params
|
||||
);
|
||||
maintenanceWindowsService.getMaintenanceWindows.mockResolvedValue({
|
||||
maintenanceWindows: [],
|
||||
maintenanceWindowsWithoutScopedQueryIds: [],
|
||||
});
|
||||
ruleTypeRegistry.get.mockReturnValue(ruleTypeWithAlerts);
|
||||
ruleTypeWithAlerts.executor.mockResolvedValue({ state: {} });
|
||||
mockValidateRuleTypeParams.mockReturnValue(mockedAdHocRunSO.attributes.rule.params);
|
||||
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(mockedAdHocRunSO);
|
||||
actionsClient.bulkEnqueueExecution.mockResolvedValue({ errors: false, items: [] });
|
||||
});
|
||||
|
||||
afterAll(() => fakeTimer.restore());
|
||||
|
@ -543,6 +559,127 @@ describe('Ad Hoc Task Runner', () => {
|
|||
expect(logger.error).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should schedule actions for rule with actions', async () => {
|
||||
const mockedAdHocRunSOWithActions = {
|
||||
...mockedAdHocRunSO,
|
||||
attributes: {
|
||||
...mockedAdHocRunSO.attributes,
|
||||
rule: {
|
||||
...mockedAdHocRunSO.attributes.rule,
|
||||
id: '1',
|
||||
actions: [
|
||||
{
|
||||
uuid: '123abc',
|
||||
group: 'default',
|
||||
actionRef: 'action_0',
|
||||
actionTypeId: 'action',
|
||||
params: { foo: true },
|
||||
frequency: {
|
||||
notifyWhen: 'onActiveAlert',
|
||||
summary: true,
|
||||
throttle: null,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
references: [
|
||||
{ type: RULE_SAVED_OBJECT_TYPE, name: 'rule', id: '1' },
|
||||
{ id: '4', name: 'action_0', type: 'action' },
|
||||
],
|
||||
};
|
||||
alertsClient.getProcessedAlerts.mockReturnValue({});
|
||||
alertsClient.getSummarizedAlerts.mockResolvedValue({
|
||||
new: { count: 1, data: [mockAAD] },
|
||||
ongoing: { count: 0, data: [] },
|
||||
recovered: { count: 0, data: [] },
|
||||
});
|
||||
mockAlertsService.createAlertsClient.mockImplementation(() => alertsClient);
|
||||
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(
|
||||
mockedAdHocRunSOWithActions
|
||||
);
|
||||
|
||||
ruleTypeWithAlerts.executor.mockImplementation(
|
||||
async ({
|
||||
services: executorServices,
|
||||
}: RuleExecutorOptions<
|
||||
RuleTypeParams,
|
||||
RuleTypeState,
|
||||
AlertInstanceState,
|
||||
AlertInstanceContext,
|
||||
string,
|
||||
RuleAlertData
|
||||
>) => {
|
||||
executorServices.alertsClient?.report({
|
||||
id: '1',
|
||||
actionGroup: 'default',
|
||||
payload: { textField: 'foo', numericField: 27 },
|
||||
});
|
||||
return { state: {} };
|
||||
}
|
||||
);
|
||||
|
||||
const taskRunner = new AdHocTaskRunner({
|
||||
context: { ...taskRunnerFactoryInitializerParams, alertsService: mockAlertsService },
|
||||
internalSavedObjectsRepository,
|
||||
taskInstance: mockedTaskInstance,
|
||||
});
|
||||
expect(AlertingEventLogger).toHaveBeenCalledTimes(1);
|
||||
|
||||
const runnerResult = await taskRunner.run();
|
||||
expect(runnerResult).toEqual({ state: {}, runAt: new Date('1970-01-01T00:00:00.000Z') });
|
||||
await taskRunner.cleanup();
|
||||
|
||||
// Verify all the expected calls were made before calling the rule executor
|
||||
expect(encryptedSavedObjectsClient.getDecryptedAsInternalUser).toHaveBeenCalledWith(
|
||||
AD_HOC_RUN_SAVED_OBJECT_TYPE,
|
||||
'abc',
|
||||
{}
|
||||
);
|
||||
expect(ruleTypeRegistry.ensureRuleTypeEnabled).toHaveBeenCalledWith('siem.queryRule');
|
||||
expect(mockValidateRuleTypeParams).toHaveBeenCalledWith(
|
||||
mockedAdHocRunSO.attributes.rule.params,
|
||||
ruleTypeWithAlerts.validate.params
|
||||
);
|
||||
// @ts-ignore - accessing private variable
|
||||
// should run the first entry in the schedule
|
||||
expect(taskRunner.scheduleToRunIndex).toEqual(0);
|
||||
|
||||
// Verify all the expected calls were made while calling the rule executor
|
||||
expect(RuleRunMetricsStore).toHaveBeenCalledTimes(1);
|
||||
expect(ruleTypeWithAlerts.executor).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(internalSavedObjectsRepository.update).toHaveBeenCalledWith(
|
||||
AD_HOC_RUN_SAVED_OBJECT_TYPE,
|
||||
mockedAdHocRunSO.id,
|
||||
{
|
||||
schedule: [
|
||||
{ ...schedule1, status: adHocRunStatus.COMPLETE },
|
||||
schedule2,
|
||||
schedule3,
|
||||
schedule4,
|
||||
schedule5,
|
||||
],
|
||||
},
|
||||
{ namespace: undefined, refresh: false }
|
||||
);
|
||||
|
||||
expect(internalSavedObjectsRepository.delete).not.toHaveBeenCalled();
|
||||
|
||||
expect(actionsClient.bulkEnqueueExecution).toHaveBeenCalledTimes(1);
|
||||
expect(actionsClient.bulkEnqueueExecution).toHaveBeenCalledWith(
|
||||
generateEnqueueFunctionInput({
|
||||
isBulk: true,
|
||||
id: '4',
|
||||
foo: true,
|
||||
consumer: 'siem',
|
||||
uuid: '123abc',
|
||||
priority: TaskPriority.Low,
|
||||
apiKeyId: 'apiKeyId',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('should run with the next pending schedule', async () => {
|
||||
ruleTypeWithAlerts.executor.mockImplementation(
|
||||
async ({
|
||||
|
|
|
@ -20,7 +20,7 @@ import {
|
|||
TaskErrorSource,
|
||||
} from '@kbn/task-manager-plugin/server';
|
||||
import { nanosToMillis } from '@kbn/event-log-plugin/common';
|
||||
import { CancellableTask, RunResult } from '@kbn/task-manager-plugin/server/task';
|
||||
import { CancellableTask, RunResult, TaskPriority } from '@kbn/task-manager-plugin/server/task';
|
||||
import { AdHocRunStatus, adHocRunStatus } from '../../common/constants';
|
||||
import { RuleRunnerErrorStackTraceLog, RuleTaskStateAndMetrics, TaskRunnerContext } from './types';
|
||||
import { getExecutorServices } from './get_executor_services';
|
||||
|
@ -35,7 +35,7 @@ import {
|
|||
RuleTypeState,
|
||||
} from '../types';
|
||||
import { TaskRunnerTimer, TaskRunnerTimerSpan } from './task_runner_timer';
|
||||
import { AdHocRun, AdHocRunSchedule, AdHocRunSO } from '../data/ad_hoc_run/types';
|
||||
import { AdHocRun, AdHocRunSO, AdHocRunSchedule } from '../data/ad_hoc_run/types';
|
||||
import { AD_HOC_RUN_SAVED_OBJECT_TYPE } from '../saved_objects';
|
||||
import { RuleMonitoringService } from '../monitoring/rule_monitoring_service';
|
||||
import { AdHocTaskRunningHandler } from './ad_hoc_task_running_handler';
|
||||
|
@ -52,6 +52,8 @@ import {
|
|||
import { RuleRunMetrics, RuleRunMetricsStore } from '../lib/rule_run_metrics_store';
|
||||
import { getEsErrorMessage } from '../lib/errors';
|
||||
import { Result, isOk, asOk, asErr } from '../lib/result_type';
|
||||
import { ActionScheduler } from './action_scheduler';
|
||||
import { transformAdHocRunToAdHocRunData } from '../application/backfill/transforms/transform_ad_hoc_run_to_backfill_result';
|
||||
|
||||
interface ConstructorParams {
|
||||
context: TaskRunnerContext;
|
||||
|
@ -173,7 +175,7 @@ export class AdHocTaskRunner implements CancellableTask {
|
|||
return ruleRunMetricsStore.getMetrics();
|
||||
}
|
||||
|
||||
const { rule } = adHocRunData;
|
||||
const { rule, apiKeyToUse, apiKeyId } = adHocRunData;
|
||||
const ruleType = this.ruleTypeRegistry.get(rule.alertTypeId);
|
||||
|
||||
const ruleLabel = `${ruleType.id}:${rule.id}: '${rule.name}'`;
|
||||
|
@ -254,6 +256,36 @@ export class AdHocTaskRunner implements CancellableTask {
|
|||
throw error;
|
||||
}
|
||||
|
||||
const actionScheduler = new ActionScheduler({
|
||||
rule: {
|
||||
...rule,
|
||||
muteAll: false,
|
||||
mutedInstanceIds: [],
|
||||
createdAt: new Date(rule.createdAt),
|
||||
updatedAt: new Date(rule.updatedAt),
|
||||
},
|
||||
ruleType,
|
||||
logger: this.logger,
|
||||
taskRunnerContext: this.context,
|
||||
taskInstance: this.taskInstance,
|
||||
ruleRunMetricsStore,
|
||||
apiKey: apiKeyToUse,
|
||||
apiKeyId,
|
||||
ruleConsumer: rule.consumer,
|
||||
executionId: this.executionId,
|
||||
ruleLabel,
|
||||
previousStartedAt: null,
|
||||
alertingEventLogger: this.alertingEventLogger,
|
||||
actionsClient: await this.context.actionsPlugin.getActionsClientWithRequest(fakeRequest),
|
||||
alertsClient,
|
||||
priority: TaskPriority.Low,
|
||||
});
|
||||
|
||||
await actionScheduler.run({
|
||||
activeCurrentAlerts: alertsClient.getProcessedAlerts('activeCurrent'),
|
||||
recoveredCurrentAlerts: alertsClient.getProcessedAlerts('recoveredCurrent'),
|
||||
});
|
||||
|
||||
return ruleRunMetricsStore.getMetrics();
|
||||
}
|
||||
|
||||
|
@ -299,14 +331,12 @@ export class AdHocTaskRunner implements CancellableTask {
|
|||
{ namespace }
|
||||
);
|
||||
|
||||
adHocRunData = {
|
||||
id: adHocRunSO.id,
|
||||
...adHocRunSO.attributes,
|
||||
rule: {
|
||||
...adHocRunSO.attributes.rule,
|
||||
id: adHocRunSO.references[0].id,
|
||||
},
|
||||
};
|
||||
adHocRunData = transformAdHocRunToAdHocRunData({
|
||||
adHocRunSO,
|
||||
isSystemAction: (connectorId: string) =>
|
||||
this.context.actionsPlugin.isSystemActionConnector(connectorId),
|
||||
omitGeneratedActionValues: false,
|
||||
});
|
||||
} catch (err) {
|
||||
const errorSource = SavedObjectsErrorHelpers.isNotFoundError(err)
|
||||
? TaskErrorSource.USER
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { TaskStatus } from '@kbn/task-manager-plugin/server';
|
||||
import { TaskPriority, TaskStatus } from '@kbn/task-manager-plugin/server';
|
||||
import { SavedObject } from '@kbn/core/server';
|
||||
import { ALERTING_CASES_SAVED_OBJECT_INDEX } from '@kbn/core-saved-objects-server';
|
||||
import {
|
||||
|
@ -408,14 +408,20 @@ export const generateEnqueueFunctionInput = ({
|
|||
isBulk = false,
|
||||
isResolved,
|
||||
foo,
|
||||
consumer,
|
||||
actionTypeId,
|
||||
priority,
|
||||
apiKeyId,
|
||||
}: {
|
||||
uuid?: string;
|
||||
id: string;
|
||||
isBulk?: boolean;
|
||||
isResolved?: boolean;
|
||||
foo?: boolean;
|
||||
consumer?: string;
|
||||
actionTypeId?: string;
|
||||
priority?: TaskPriority;
|
||||
apiKeyId?: string;
|
||||
}) => {
|
||||
const input = {
|
||||
actionTypeId: actionTypeId || 'action',
|
||||
|
@ -427,7 +433,7 @@ export const generateEnqueueFunctionInput = ({
|
|||
...(isResolved !== undefined ? { isResolved } : {}),
|
||||
...(foo !== undefined ? { foo } : {}),
|
||||
},
|
||||
consumer: 'bar',
|
||||
consumer: consumer ?? 'bar',
|
||||
relatedSavedObjects: [
|
||||
{
|
||||
id: '1',
|
||||
|
@ -444,6 +450,8 @@ export const generateEnqueueFunctionInput = ({
|
|||
type: 'SAVED_OBJECT',
|
||||
},
|
||||
spaceId: 'default',
|
||||
...(priority && { priority }),
|
||||
...(apiKeyId && { apiKeyId }),
|
||||
};
|
||||
return isBulk ? [input] : input;
|
||||
};
|
||||
|
|
|
@ -14,8 +14,8 @@ import {
|
|||
AlertInstanceState,
|
||||
AlertInstanceContext,
|
||||
RuleTypeParams,
|
||||
SanitizedRule,
|
||||
} from '../types';
|
||||
import { ActionSchedulerRule } from './action_scheduler/types';
|
||||
|
||||
export interface TransformActionParamsOptions {
|
||||
actionsPlugin: ActionsPluginStartContract;
|
||||
|
@ -146,7 +146,7 @@ export function transformSummaryActionParams({
|
|||
kibanaBaseUrl,
|
||||
}: {
|
||||
alerts: SummarizedAlertsWithAll;
|
||||
rule: SanitizedRule<RuleTypeParams>;
|
||||
rule: ActionSchedulerRule<RuleTypeParams>;
|
||||
ruleTypeId: string;
|
||||
actionsPlugin: ActionsPluginStartContract;
|
||||
actionId: string;
|
||||
|
|
|
@ -128,7 +128,9 @@ function getSortByPriority(definitions: TaskTypeDictionary): estypes.SortCombina
|
|||
// TODO: we could do this locally as well, but they may starve
|
||||
source: `
|
||||
String taskType = doc['task.taskType'].value;
|
||||
if (params.priority_map.containsKey(taskType)) {
|
||||
if (doc['task.priority'].size() != 0) {
|
||||
return doc['task.priority'].value;
|
||||
} else if (params.priority_map.containsKey(taskType)) {
|
||||
return params.priority_map[taskType];
|
||||
} else {
|
||||
return ${TaskPriority.Normal};
|
||||
|
|
|
@ -351,6 +351,11 @@ export interface TaskInstance {
|
|||
* Used to break up tasks so each Kibana node can claim tasks on a subset of the partitions
|
||||
*/
|
||||
partition?: number;
|
||||
|
||||
/*
|
||||
* Optionally override the priority defined in the task type for this specific task instance
|
||||
*/
|
||||
priority?: TaskPriority;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -356,7 +356,9 @@ describe('TaskClaiming', () => {
|
|||
},
|
||||
source: `
|
||||
String taskType = doc['task.taskType'].value;
|
||||
if (params.priority_map.containsKey(taskType)) {
|
||||
if (doc['task.priority'].size() != 0) {
|
||||
return doc['task.priority'].value;
|
||||
} else if (params.priority_map.containsKey(taskType)) {
|
||||
return params.priority_map[taskType];
|
||||
} else {
|
||||
return 50;
|
||||
|
|
|
@ -17,5 +17,6 @@ export default function backfillTests({ loadTestFile }: FtrProviderContext) {
|
|||
loadTestFile(require.resolve('./delete'));
|
||||
loadTestFile(require.resolve('./delete_rule'));
|
||||
loadTestFile(require.resolve('./task_runner'));
|
||||
loadTestFile(require.resolve('./task_runner_with_actions'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -13,14 +13,9 @@ import { get } from 'lodash';
|
|||
import { AD_HOC_RUN_SAVED_OBJECT_TYPE } from '@kbn/alerting-plugin/server/saved_objects';
|
||||
import { asyncForEach } from '../../../../../../functional/services/transform/api';
|
||||
import { UserAtSpaceScenarios } from '../../../../scenarios';
|
||||
import {
|
||||
checkAAD,
|
||||
getTestRuleData,
|
||||
getUrlPrefix,
|
||||
ObjectRemover,
|
||||
TaskManagerDoc,
|
||||
} from '../../../../../common/lib';
|
||||
import { checkAAD, getTestRuleData, getUrlPrefix, ObjectRemover } from '../../../../../common/lib';
|
||||
import { FtrProviderContext } from '../../../../../common/ftr_provider_context';
|
||||
import { TEST_ACTIONS_INDEX, getScheduledTask } from './test_utils';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function scheduleBackfillTests({ getService }: FtrProviderContext) {
|
||||
|
@ -36,6 +31,7 @@ export default function scheduleBackfillTests({ getService }: FtrProviderContext
|
|||
await asyncForEach(backfillIds, async ({ id, spaceId }: { id: string; spaceId: string }) => {
|
||||
await supertest
|
||||
.delete(`${getUrlPrefix(spaceId)}/internal/alerting/rules/backfill/${id}`)
|
||||
.set('x-elastic-internal-origin', 'xxx')
|
||||
.set('kbn-xsrf', 'foo');
|
||||
});
|
||||
backfillIds = [];
|
||||
|
@ -50,16 +46,13 @@ export default function scheduleBackfillTests({ getService }: FtrProviderContext
|
|||
return result._source;
|
||||
}
|
||||
|
||||
async function getScheduledTask(id: string): Promise<TaskManagerDoc> {
|
||||
const scheduledTask = await es.get<TaskManagerDoc>({
|
||||
id: `task:${id}`,
|
||||
index: '.kibana_task_manager',
|
||||
});
|
||||
return scheduledTask._source!;
|
||||
}
|
||||
|
||||
function getRule(overwrites = {}) {
|
||||
return getTestRuleData({
|
||||
return {
|
||||
name: 'abc',
|
||||
enabled: true,
|
||||
tags: ['foo'],
|
||||
consumer: 'alertsFixture',
|
||||
actions: [],
|
||||
rule_type_id: 'test.patternFiringAutoRecoverFalse',
|
||||
params: {
|
||||
pattern: {
|
||||
|
@ -68,7 +61,7 @@ export default function scheduleBackfillTests({ getService }: FtrProviderContext
|
|||
},
|
||||
schedule: { interval: '12h' },
|
||||
...overwrites,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function getLifecycleRule(overwrites = {}) {
|
||||
|
@ -151,6 +144,7 @@ export default function scheduleBackfillTests({ getService }: FtrProviderContext
|
|||
// schedule backfill for both rules as current user
|
||||
const response = await supertestWithoutAuth
|
||||
.post(`${getUrlPrefix(apiOptions.spaceId)}/internal/alerting/rules/backfill/_schedule`)
|
||||
.set('x-elastic-internal-origin', 'xxx')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.auth(apiOptions.username, apiOptions.password)
|
||||
.send([
|
||||
|
@ -283,7 +277,7 @@ export default function scheduleBackfillTests({ getService }: FtrProviderContext
|
|||
expect(adHocRunSO2.references).to.eql([{ id: ruleId2, name: 'rule', type: 'alert' }]);
|
||||
|
||||
// check that the task was scheduled correctly
|
||||
const taskRecord1 = await getScheduledTask(result[0].id);
|
||||
const taskRecord1 = await getScheduledTask(es, result[0].id);
|
||||
expect(taskRecord1.type).to.eql('task');
|
||||
expect(taskRecord1.task.taskType).to.eql('ad_hoc_run-backfill');
|
||||
expect(taskRecord1.task.timeoutOverride).to.eql('10s');
|
||||
|
@ -292,7 +286,7 @@ export default function scheduleBackfillTests({ getService }: FtrProviderContext
|
|||
adHocRunParamsId: result[0].id,
|
||||
spaceId: space.id,
|
||||
});
|
||||
const taskRecord2 = await getScheduledTask(result[1].id);
|
||||
const taskRecord2 = await getScheduledTask(es, result[1].id);
|
||||
expect(taskRecord2.type).to.eql('task');
|
||||
expect(taskRecord2.task.taskType).to.eql('ad_hoc_run-backfill');
|
||||
expect(taskRecord2.task.timeoutOverride).to.eql('10s');
|
||||
|
@ -339,6 +333,7 @@ export default function scheduleBackfillTests({ getService }: FtrProviderContext
|
|||
// schedule 3 backfill jobs for rule as current user
|
||||
const response = await supertestWithoutAuth
|
||||
.post(`${getUrlPrefix(apiOptions.spaceId)}/internal/alerting/rules/backfill/_schedule`)
|
||||
.set('x-elastic-internal-origin', 'xxx')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.auth(apiOptions.username, apiOptions.password)
|
||||
.send([
|
||||
|
@ -514,7 +509,7 @@ export default function scheduleBackfillTests({ getService }: FtrProviderContext
|
|||
expect(adHocRunSO3.references).to.eql([{ id: ruleId, name: 'rule', type: 'alert' }]);
|
||||
|
||||
// check that the task was scheduled correctly
|
||||
const taskRecord1 = await getScheduledTask(result[0].id);
|
||||
const taskRecord1 = await getScheduledTask(es, result[0].id);
|
||||
expect(taskRecord1.type).to.eql('task');
|
||||
expect(taskRecord1.task.taskType).to.eql('ad_hoc_run-backfill');
|
||||
expect(taskRecord1.task.timeoutOverride).to.eql('10s');
|
||||
|
@ -523,7 +518,7 @@ export default function scheduleBackfillTests({ getService }: FtrProviderContext
|
|||
adHocRunParamsId: result[0].id,
|
||||
spaceId: space.id,
|
||||
});
|
||||
const taskRecord2 = await getScheduledTask(result[1].id);
|
||||
const taskRecord2 = await getScheduledTask(es, result[1].id);
|
||||
expect(taskRecord2.type).to.eql('task');
|
||||
expect(taskRecord2.task.taskType).to.eql('ad_hoc_run-backfill');
|
||||
expect(taskRecord2.task.timeoutOverride).to.eql('10s');
|
||||
|
@ -532,7 +527,7 @@ export default function scheduleBackfillTests({ getService }: FtrProviderContext
|
|||
adHocRunParamsId: result[1].id,
|
||||
spaceId: space.id,
|
||||
});
|
||||
const taskRecord3 = await getScheduledTask(result[2].id);
|
||||
const taskRecord3 = await getScheduledTask(es, result[2].id);
|
||||
expect(taskRecord3.type).to.eql('task');
|
||||
expect(taskRecord3.task.taskType).to.eql('ad_hoc_run-backfill');
|
||||
expect(taskRecord3.task.timeoutOverride).to.eql('10s');
|
||||
|
@ -572,6 +567,7 @@ export default function scheduleBackfillTests({ getService }: FtrProviderContext
|
|||
// invalid start time
|
||||
const response1 = await supertestWithoutAuth
|
||||
.post(`${getUrlPrefix(apiOptions.spaceId)}/internal/alerting/rules/backfill/_schedule`)
|
||||
.set('x-elastic-internal-origin', 'xxx')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.auth(apiOptions.username, apiOptions.password)
|
||||
.send([{ rule_id: 'abc', start: 'foo' }]);
|
||||
|
@ -579,6 +575,7 @@ export default function scheduleBackfillTests({ getService }: FtrProviderContext
|
|||
// invalid end time
|
||||
const response2 = await supertestWithoutAuth
|
||||
.post(`${getUrlPrefix(apiOptions.spaceId)}/internal/alerting/rules/backfill/_schedule`)
|
||||
.set('x-elastic-internal-origin', 'xxx')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.auth(apiOptions.username, apiOptions.password)
|
||||
.send([
|
||||
|
@ -593,6 +590,7 @@ export default function scheduleBackfillTests({ getService }: FtrProviderContext
|
|||
const time = moment().utc().startOf('day').subtract(7, 'days').toISOString();
|
||||
const response3 = await supertestWithoutAuth
|
||||
.post(`${getUrlPrefix(apiOptions.spaceId)}/internal/alerting/rules/backfill/_schedule`)
|
||||
.set('x-elastic-internal-origin', 'xxx')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.auth(apiOptions.username, apiOptions.password)
|
||||
.send([{ rule_id: 'abc', start: time, end: time }]);
|
||||
|
@ -600,6 +598,7 @@ export default function scheduleBackfillTests({ getService }: FtrProviderContext
|
|||
// end time is before start time
|
||||
const response4 = await supertestWithoutAuth
|
||||
.post(`${getUrlPrefix(apiOptions.spaceId)}/internal/alerting/rules/backfill/_schedule`)
|
||||
.set('x-elastic-internal-origin', 'xxx')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.auth(apiOptions.username, apiOptions.password)
|
||||
.send([
|
||||
|
@ -613,6 +612,7 @@ export default function scheduleBackfillTests({ getService }: FtrProviderContext
|
|||
// start time is too far in the past
|
||||
const response5 = await supertestWithoutAuth
|
||||
.post(`${getUrlPrefix(apiOptions.spaceId)}/internal/alerting/rules/backfill/_schedule`)
|
||||
.set('x-elastic-internal-origin', 'xxx')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.auth(apiOptions.username, apiOptions.password)
|
||||
.send([{ rule_id: 'abc', start: '2023-04-30T00:00:00.000Z' }]);
|
||||
|
@ -620,6 +620,7 @@ export default function scheduleBackfillTests({ getService }: FtrProviderContext
|
|||
// start time is in the future
|
||||
const response6 = await supertestWithoutAuth
|
||||
.post(`${getUrlPrefix(apiOptions.spaceId)}/internal/alerting/rules/backfill/_schedule`)
|
||||
.set('x-elastic-internal-origin', 'xxx')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.auth(apiOptions.username, apiOptions.password)
|
||||
.send([
|
||||
|
@ -629,6 +630,7 @@ export default function scheduleBackfillTests({ getService }: FtrProviderContext
|
|||
// end time is in the future
|
||||
const response7 = await supertestWithoutAuth
|
||||
.post(`${getUrlPrefix(apiOptions.spaceId)}/internal/alerting/rules/backfill/_schedule`)
|
||||
.set('x-elastic-internal-origin', 'xxx')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.auth(apiOptions.username, apiOptions.password)
|
||||
.send([
|
||||
|
@ -715,6 +717,7 @@ export default function scheduleBackfillTests({ getService }: FtrProviderContext
|
|||
// schedule backfill for non-existent rule
|
||||
const response = await supertestWithoutAuth
|
||||
.post(`${getUrlPrefix(apiOptions.spaceId)}/internal/alerting/rules/backfill/_schedule`)
|
||||
.set('x-elastic-internal-origin', 'xxx')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.auth(apiOptions.username, apiOptions.password)
|
||||
.send([
|
||||
|
@ -814,6 +817,7 @@ export default function scheduleBackfillTests({ getService }: FtrProviderContext
|
|||
// schedule backfill as current user
|
||||
const response = await supertestWithoutAuth
|
||||
.post(`${getUrlPrefix(apiOptions.spaceId)}/internal/alerting/rules/backfill/_schedule`)
|
||||
.set('x-elastic-internal-origin', 'xxx')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.auth(apiOptions.username, apiOptions.password)
|
||||
.send([
|
||||
|
@ -1019,7 +1023,7 @@ export default function scheduleBackfillTests({ getService }: FtrProviderContext
|
|||
expect(adHocRunSO3.references).to.eql([{ id: ruleId1, name: 'rule', type: 'alert' }]);
|
||||
|
||||
// check that the task was scheduled correctly
|
||||
const taskRecord1 = await getScheduledTask(result[0].id);
|
||||
const taskRecord1 = await getScheduledTask(es, result[0].id);
|
||||
expect(taskRecord1.type).to.eql('task');
|
||||
expect(taskRecord1.task.taskType).to.eql('ad_hoc_run-backfill');
|
||||
expect(taskRecord1.task.timeoutOverride).to.eql('10s');
|
||||
|
@ -1028,7 +1032,7 @@ export default function scheduleBackfillTests({ getService }: FtrProviderContext
|
|||
adHocRunParamsId: result[0].id,
|
||||
spaceId: space.id,
|
||||
});
|
||||
const taskRecord2 = await getScheduledTask(result[1].id);
|
||||
const taskRecord2 = await getScheduledTask(es, result[1].id);
|
||||
expect(taskRecord2.type).to.eql('task');
|
||||
expect(taskRecord2.task.taskType).to.eql('ad_hoc_run-backfill');
|
||||
expect(taskRecord2.task.timeoutOverride).to.eql('10s');
|
||||
|
@ -1037,7 +1041,7 @@ export default function scheduleBackfillTests({ getService }: FtrProviderContext
|
|||
adHocRunParamsId: result[1].id,
|
||||
spaceId: space.id,
|
||||
});
|
||||
const taskRecord3 = await getScheduledTask(result[5].id);
|
||||
const taskRecord3 = await getScheduledTask(es, result[5].id);
|
||||
expect(taskRecord3.type).to.eql('task');
|
||||
expect(taskRecord3.task.taskType).to.eql('ad_hoc_run-backfill');
|
||||
expect(taskRecord3.task.timeoutOverride).to.eql('10s');
|
||||
|
@ -1071,6 +1075,267 @@ export default function scheduleBackfillTests({ getService }: FtrProviderContext
|
|||
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle schedule request where rule has supported and unsupported actions', async () => {
|
||||
// create a connector
|
||||
const cresponse = await supertest
|
||||
.post(`${getUrlPrefix(apiOptions.spaceId)}/api/actions/connector`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: 'An index connector',
|
||||
connector_type_id: '.index',
|
||||
config: {
|
||||
index: TEST_ACTIONS_INDEX,
|
||||
refresh: true,
|
||||
},
|
||||
secrets: {},
|
||||
})
|
||||
.expect(200);
|
||||
const connectorId = cresponse.body.id;
|
||||
objectRemover.add(apiOptions.spaceId, connectorId, 'connector', 'actions');
|
||||
|
||||
const start = moment().utc().startOf('day').subtract(14, 'days').toISOString();
|
||||
const end = moment().utc().startOf('day').subtract(5, 'days').toISOString();
|
||||
// create 2 rules
|
||||
const rresponse1 = await supertest
|
||||
.post(`${getUrlPrefix(apiOptions.spaceId)}/api/alerting/rule`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(
|
||||
getRule({
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
id: connectorId,
|
||||
uuid: '111-111',
|
||||
params: { documents: [{ alertUuid: '{{alert.uuid}}' }] },
|
||||
frequency: { notify_when: 'onActiveAlert', throttle: null, summary: true },
|
||||
},
|
||||
{
|
||||
group: 'default',
|
||||
id: connectorId,
|
||||
uuid: '222-222',
|
||||
params: { documents: [{ alertUuid: '{{alert.uuid}}' }] },
|
||||
frequency: {
|
||||
notify_when: 'onActionGroupChange',
|
||||
throttle: null,
|
||||
summary: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
)
|
||||
.expect(200);
|
||||
const ruleId1 = rresponse1.body.id;
|
||||
objectRemover.add(apiOptions.spaceId, ruleId1, 'rule', 'alerting');
|
||||
|
||||
const rresponse2 = await supertest
|
||||
.post(`${getUrlPrefix(apiOptions.spaceId)}/api/alerting/rule`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(
|
||||
getRule({
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
id: connectorId,
|
||||
uuid: '333-333',
|
||||
params: { documents: [{ alertUuid: '{{alert.uuid}}' }] },
|
||||
frequency: { notify_when: 'onActiveAlert', throttle: null, summary: false },
|
||||
},
|
||||
],
|
||||
})
|
||||
)
|
||||
.expect(200);
|
||||
const ruleId2 = rresponse2.body.id;
|
||||
objectRemover.add(apiOptions.spaceId, ruleId2, 'rule', 'alerting');
|
||||
|
||||
// schedule backfill as current user
|
||||
const response = await supertestWithoutAuth
|
||||
.post(`${getUrlPrefix(apiOptions.spaceId)}/internal/alerting/rules/backfill/_schedule`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.set('x-elastic-internal-origin', 'xxx')
|
||||
.auth(apiOptions.username, apiOptions.password)
|
||||
.send([
|
||||
{ rule_id: ruleId1, start, end, run_actions: true },
|
||||
{ rule_id: ruleId2, start, end, run_actions: true },
|
||||
]);
|
||||
|
||||
switch (scenario.id) {
|
||||
// User can't do anything in this space
|
||||
case 'no_kibana_privileges at space1':
|
||||
// User has no privileges in this space
|
||||
case 'space_1_all at space2':
|
||||
expect(response.statusCode).to.eql(403);
|
||||
expect(response.body).to.eql({
|
||||
error: 'Forbidden',
|
||||
message: `Unauthorized to find rules for any rule types`,
|
||||
statusCode: 403,
|
||||
});
|
||||
break;
|
||||
// User has read privileges in this space
|
||||
case 'global_read at space1':
|
||||
expect(response.statusCode).to.eql(403);
|
||||
expect(response.body.error).to.eql('Forbidden');
|
||||
expect(response.body.message).to.match(
|
||||
/Unauthorized by "alertsFixture" to scheduleBackfill "[^"]+" rule/
|
||||
);
|
||||
break;
|
||||
// User doesn't have access to actions
|
||||
case 'space_1_all_alerts_none_actions at space1':
|
||||
expect(response.statusCode).to.eql(403);
|
||||
expect(response.body.error).to.eql('Forbidden');
|
||||
expect(response.body.message).to.eql('Unauthorized to get actions');
|
||||
break;
|
||||
// Superuser has access to everything
|
||||
case 'superuser at space1':
|
||||
// User has all privileges in this space
|
||||
case 'space_1_all at space1':
|
||||
// User has all privileges in this space
|
||||
case 'space_1_all_with_restricted_fixture at space1':
|
||||
expect(response.statusCode).to.eql(200);
|
||||
const result = response.body;
|
||||
|
||||
expect(result.length).to.eql(2);
|
||||
|
||||
// successful schedule with warning for unsupported action
|
||||
expect(typeof result[0].id).to.be('string');
|
||||
backfillIds.push({ id: result[0].id, spaceId: apiOptions.spaceId });
|
||||
expect(result[0].duration).to.eql('12h');
|
||||
expect(result[0].enabled).to.eql(true);
|
||||
expect(result[0].start).to.eql(start);
|
||||
expect(result[0].end).to.eql(end);
|
||||
expect(result[0].status).to.eql('pending');
|
||||
expect(result[0].space_id).to.eql(space.id);
|
||||
expect(typeof result[0].created_at).to.be('string');
|
||||
expect(result[0].rule.actions.length).to.eql(1);
|
||||
expect(result[0].rule.actions[0]).to.eql({
|
||||
actionTypeId: '.index',
|
||||
group: 'default',
|
||||
id: connectorId,
|
||||
uuid: '111-111',
|
||||
params: { documents: [{ alertUuid: '{{alert.uuid}}' }] },
|
||||
frequency: { notifyWhen: 'onActiveAlert', throttle: null, summary: true },
|
||||
});
|
||||
expect(result[0].warnings).to.eql([
|
||||
`Rule has actions that are not supported for backfill. Those actions will be skipped.`,
|
||||
]);
|
||||
|
||||
let currentStart = start;
|
||||
result[0].schedule.forEach((sched: any) => {
|
||||
expect(sched.interval).to.eql('12h');
|
||||
expect(sched.status).to.eql('pending');
|
||||
const runAt = moment(currentStart).add(12, 'hours').toISOString();
|
||||
expect(sched.run_at).to.eql(runAt);
|
||||
currentStart = runAt;
|
||||
});
|
||||
|
||||
// // successful schedule
|
||||
expect(typeof result[1].id).to.be('string');
|
||||
backfillIds.push({ id: result[1].id, spaceId: apiOptions.spaceId });
|
||||
expect(result[1].duration).to.eql('12h');
|
||||
expect(result[1].enabled).to.eql(true);
|
||||
expect(result[1].start).to.eql(start);
|
||||
expect(result[1].end).to.eql(end);
|
||||
expect(result[1].status).to.eql('pending');
|
||||
expect(result[1].space_id).to.eql(space.id);
|
||||
expect(typeof result[1].created_at).to.be('string');
|
||||
expect(result[1].rule.actions.length).to.eql(1);
|
||||
expect(result[1].rule.actions[0]).to.eql({
|
||||
actionTypeId: '.index',
|
||||
group: 'default',
|
||||
id: connectorId,
|
||||
uuid: '333-333',
|
||||
params: { documents: [{ alertUuid: '{{alert.uuid}}' }] },
|
||||
frequency: { notifyWhen: 'onActiveAlert', throttle: null, summary: false },
|
||||
});
|
||||
expect(result[1].warnings).to.be(undefined);
|
||||
|
||||
currentStart = start;
|
||||
result[1].schedule.forEach((sched: any) => {
|
||||
expect(sched.interval).to.eql('12h');
|
||||
expect(sched.status).to.eql('pending');
|
||||
const runAt = moment(currentStart).add(12, 'hours').toISOString();
|
||||
expect(sched.run_at).to.eql(runAt);
|
||||
currentStart = runAt;
|
||||
});
|
||||
|
||||
// check that the expected ad hoc run SOs were created
|
||||
const adHocRunSO1 = (await getAdHocRunSO(result[0].id)) as SavedObject<AdHocRunSO>;
|
||||
const adHocRun1: AdHocRunSO = get(adHocRunSO1, 'ad_hoc_run_params')!;
|
||||
const adHocRunSO2 = (await getAdHocRunSO(result[1].id)) as SavedObject<AdHocRunSO>;
|
||||
const adHocRun2: AdHocRunSO = get(adHocRunSO2, 'ad_hoc_run_params')!;
|
||||
|
||||
expect(typeof adHocRun1.apiKeyId).to.be('string');
|
||||
expect(typeof adHocRun1.apiKeyToUse).to.be('string');
|
||||
expect(typeof adHocRun1.createdAt).to.be('string');
|
||||
expect(adHocRun1.duration).to.eql('12h');
|
||||
expect(adHocRun1.enabled).to.eql(true);
|
||||
expect(adHocRun1.start).to.eql(start);
|
||||
expect(adHocRun1.end).to.eql(end);
|
||||
expect(adHocRun1.status).to.eql('pending');
|
||||
expect(adHocRun1.spaceId).to.eql(space.id);
|
||||
|
||||
currentStart = start;
|
||||
adHocRun1.schedule.forEach((sched: any) => {
|
||||
expect(sched.interval).to.eql('12h');
|
||||
expect(sched.status).to.eql('pending');
|
||||
const runAt = moment(currentStart).add(12, 'hours').toISOString();
|
||||
expect(sched.runAt).to.eql(runAt);
|
||||
currentStart = runAt;
|
||||
});
|
||||
|
||||
expect(typeof adHocRun2.apiKeyId).to.be('string');
|
||||
expect(typeof adHocRun2.apiKeyToUse).to.be('string');
|
||||
expect(typeof adHocRun2.createdAt).to.be('string');
|
||||
expect(adHocRun2.duration).to.eql('12h');
|
||||
expect(adHocRun2.enabled).to.eql(true);
|
||||
expect(adHocRun2.start).to.eql(start);
|
||||
expect(adHocRun2.end).to.eql(end);
|
||||
expect(adHocRun2.status).to.eql('pending');
|
||||
expect(adHocRun2.spaceId).to.eql(space.id);
|
||||
|
||||
currentStart = start;
|
||||
adHocRun2.schedule.forEach((sched: any) => {
|
||||
expect(sched.interval).to.eql('12h');
|
||||
expect(sched.status).to.eql('pending');
|
||||
const runAt = moment(currentStart).add(12, 'hours').toISOString();
|
||||
expect(sched.runAt).to.eql(runAt);
|
||||
currentStart = runAt;
|
||||
});
|
||||
|
||||
// check references are stored correctly
|
||||
expect(adHocRunSO1.references).to.eql([
|
||||
{ id: ruleId1, name: 'rule', type: 'alert' },
|
||||
{ id: connectorId, name: 'action_0', type: 'action' },
|
||||
]);
|
||||
expect(adHocRunSO2.references).to.eql([
|
||||
{ id: ruleId2, name: 'rule', type: 'alert' },
|
||||
{ id: connectorId, name: 'action_0', type: 'action' },
|
||||
]);
|
||||
|
||||
// check that the task was scheduled correctly
|
||||
const taskRecord1 = await getScheduledTask(es, result[0].id);
|
||||
expect(taskRecord1.type).to.eql('task');
|
||||
expect(taskRecord1.task.taskType).to.eql('ad_hoc_run-backfill');
|
||||
expect(taskRecord1.task.timeoutOverride).to.eql('10s');
|
||||
expect(taskRecord1.task.enabled).to.eql(true);
|
||||
expect(JSON.parse(taskRecord1.task.params)).to.eql({
|
||||
adHocRunParamsId: result[0].id,
|
||||
spaceId: space.id,
|
||||
});
|
||||
const taskRecord2 = await getScheduledTask(es, result[1].id);
|
||||
expect(taskRecord2.type).to.eql('task');
|
||||
expect(taskRecord2.task.taskType).to.eql('ad_hoc_run-backfill');
|
||||
expect(taskRecord2.task.timeoutOverride).to.eql('10s');
|
||||
expect(taskRecord2.task.enabled).to.eql(true);
|
||||
expect(JSON.parse(taskRecord2.task.params)).to.eql({
|
||||
adHocRunParamsId: result[1].id,
|
||||
spaceId: space.id,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
|
||||
import expect from '@kbn/expect';
|
||||
import moment from 'moment';
|
||||
import { SearchHit } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import type { SecurityAlert } from '@kbn/alerts-as-data-utils';
|
||||
import {
|
||||
ALERT_LAST_DETECTED,
|
||||
|
@ -33,27 +32,23 @@ import {
|
|||
RULE_SAVED_OBJECT_TYPE,
|
||||
} from '@kbn/alerting-plugin/server/saved_objects';
|
||||
import { ALERT_ORIGINAL_TIME } from '@kbn/security-solution-plugin/common/field_maps/field_names';
|
||||
import {
|
||||
createEsDocument,
|
||||
DOCUMENT_REFERENCE,
|
||||
DOCUMENT_SOURCE,
|
||||
} from '../../../../../spaces_only/tests/alerting/create_test_data';
|
||||
import { asyncForEach } from '../../../../../../functional/services/transform/api';
|
||||
import { DOCUMENT_SOURCE } from '../../../../../spaces_only/tests/alerting/create_test_data';
|
||||
import { FtrProviderContext } from '../../../../../common/ftr_provider_context';
|
||||
import { SuperuserAtSpace1 } from '../../../../scenarios';
|
||||
import { getTestRuleData, getUrlPrefix, ObjectRemover } from '../../../../../common/lib';
|
||||
import {
|
||||
getEventLog,
|
||||
getTestRuleData,
|
||||
getUrlPrefix,
|
||||
ObjectRemover,
|
||||
TaskManagerDoc,
|
||||
} from '../../../../../common/lib';
|
||||
getScheduledTask,
|
||||
indexTestDocs,
|
||||
queryForAlertDocs,
|
||||
searchScheduledTask,
|
||||
testDocTimestamps,
|
||||
waitForEventLogDocs,
|
||||
} from './test_utils';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function createBackfillTaskRunnerTests({ getService }: FtrProviderContext) {
|
||||
const es = getService('es');
|
||||
const retry = getService('retry');
|
||||
const log = getService('log');
|
||||
const esTestIndexTool = new ESTestIndexTool(es, retry);
|
||||
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
||||
const supertest = getService('supertest');
|
||||
|
@ -61,31 +56,6 @@ export default function createBackfillTaskRunnerTests({ getService }: FtrProvide
|
|||
|
||||
const alertsAsDataIndex = '.alerts-security.alerts-space1';
|
||||
const timestampPattern = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/;
|
||||
const originalDocTimestamps = [
|
||||
// before first backfill run
|
||||
moment().utc().subtract(14, 'days').toISOString(),
|
||||
|
||||
// backfill execution set 1
|
||||
moment().utc().startOf('day').subtract(13, 'days').add(10, 'minutes').toISOString(),
|
||||
moment().utc().startOf('day').subtract(13, 'days').add(11, 'minutes').toISOString(),
|
||||
moment().utc().startOf('day').subtract(13, 'days').add(12, 'minutes').toISOString(),
|
||||
|
||||
// backfill execution set 2
|
||||
moment().utc().startOf('day').subtract(12, 'days').add(20, 'minutes').toISOString(),
|
||||
|
||||
// backfill execution set 3
|
||||
moment().utc().startOf('day').subtract(11, 'days').add(30, 'minutes').toISOString(),
|
||||
moment().utc().startOf('day').subtract(11, 'days').add(31, 'minutes').toISOString(),
|
||||
moment().utc().startOf('day').subtract(11, 'days').add(32, 'minutes').toISOString(),
|
||||
moment().utc().startOf('day').subtract(11, 'days').add(33, 'minutes').toISOString(),
|
||||
moment().utc().startOf('day').subtract(11, 'days').add(34, 'minutes').toISOString(),
|
||||
|
||||
// backfill execution set 4 purposely left empty
|
||||
|
||||
// after last backfill
|
||||
moment().utc().startOf('day').subtract(9, 'days').add(40, 'minutes').toISOString(),
|
||||
moment().utc().startOf('day').subtract(9, 'days').add(41, 'minutes').toISOString(),
|
||||
];
|
||||
|
||||
describe('ad hoc backfill task', () => {
|
||||
beforeEach(async () => {
|
||||
|
@ -113,7 +83,7 @@ export default function createBackfillTaskRunnerTests({ getService }: FtrProvide
|
|||
const spaceId = SuperuserAtSpace1.space.id;
|
||||
|
||||
// Index documents
|
||||
await indexTestDocs();
|
||||
await indexTestDocs(es, esTestIndexTool);
|
||||
|
||||
// Create siem.queryRule
|
||||
const response1 = await supertestWithoutAuth
|
||||
|
@ -165,8 +135,8 @@ export default function createBackfillTaskRunnerTests({ getService }: FtrProvide
|
|||
const ruleId = response1.body.id;
|
||||
objectRemover.add(spaceId, ruleId, 'rule', 'alerting');
|
||||
|
||||
const start = moment(originalDocTimestamps[1]).utc().startOf('day').toISOString();
|
||||
const end = moment(originalDocTimestamps[11]).utc().startOf('day').toISOString();
|
||||
const start = moment(testDocTimestamps[1]).utc().startOf('day').toISOString();
|
||||
const end = moment(testDocTimestamps[11]).utc().startOf('day').toISOString();
|
||||
|
||||
// Schedule backfill for this rule
|
||||
const response2 = await supertestWithoutAuth
|
||||
|
@ -176,9 +146,6 @@ export default function createBackfillTaskRunnerTests({ getService }: FtrProvide
|
|||
.send([{ rule_id: ruleId, start, end }])
|
||||
.expect(200);
|
||||
|
||||
log.info(`originalDocTimestamps ${JSON.stringify(originalDocTimestamps)}`);
|
||||
log.info(`scheduledBackfill ${JSON.stringify(response2.body)}`);
|
||||
|
||||
const scheduleResult = response2.body;
|
||||
|
||||
expect(scheduleResult.length).to.eql(1);
|
||||
|
@ -196,7 +163,7 @@ export default function createBackfillTaskRunnerTests({ getService }: FtrProvide
|
|||
const backfillId = scheduleResult[0].id;
|
||||
|
||||
// check that the task was scheduled correctly
|
||||
const taskRecord = await getScheduledTask(backfillId);
|
||||
const taskRecord = await getScheduledTask(es, backfillId);
|
||||
expect(taskRecord.type).to.eql('task');
|
||||
expect(taskRecord.task.taskType).to.eql('ad_hoc_run-backfill');
|
||||
expect(taskRecord.task.timeoutOverride).to.eql('5m');
|
||||
|
@ -208,6 +175,8 @@ export default function createBackfillTaskRunnerTests({ getService }: FtrProvide
|
|||
|
||||
// get the execute-backfill events
|
||||
const events: IValidatedEvent[] = await waitForEventLogDocs(
|
||||
retry,
|
||||
getService,
|
||||
backfillId,
|
||||
spaceId,
|
||||
new Map([['execute-backfill', { equal: 4 }]])
|
||||
|
@ -284,7 +253,7 @@ export default function createBackfillTaskRunnerTests({ getService }: FtrProvide
|
|||
);
|
||||
|
||||
// query for alert docs
|
||||
const alertDocs = await queryForAlertDocs<SecurityAlert>();
|
||||
const alertDocs = await queryForAlertDocs<SecurityAlert>(es, alertsAsDataIndex);
|
||||
expect(alertDocs.length).to.eql(9);
|
||||
|
||||
// each alert doc should have these fields
|
||||
|
@ -320,9 +289,9 @@ export default function createBackfillTaskRunnerTests({ getService }: FtrProvide
|
|||
);
|
||||
}
|
||||
|
||||
expect(alertDocsBackfill1[0]._source![ALERT_ORIGINAL_TIME]).to.eql(originalDocTimestamps[1]);
|
||||
expect(alertDocsBackfill1[1]._source![ALERT_ORIGINAL_TIME]).to.eql(originalDocTimestamps[2]);
|
||||
expect(alertDocsBackfill1[2]._source![ALERT_ORIGINAL_TIME]).to.eql(originalDocTimestamps[3]);
|
||||
expect(alertDocsBackfill1[0]._source![ALERT_ORIGINAL_TIME]).to.eql(testDocTimestamps[1]);
|
||||
expect(alertDocsBackfill1[1]._source![ALERT_ORIGINAL_TIME]).to.eql(testDocTimestamps[2]);
|
||||
expect(alertDocsBackfill1[2]._source![ALERT_ORIGINAL_TIME]).to.eql(testDocTimestamps[3]);
|
||||
|
||||
// backfill run 2 alerts
|
||||
const alertDocsBackfill2 = alertDocs.filter(
|
||||
|
@ -342,7 +311,7 @@ export default function createBackfillTaskRunnerTests({ getService }: FtrProvide
|
|||
);
|
||||
}
|
||||
|
||||
expect(alertDocsBackfill2[0]._source![ALERT_ORIGINAL_TIME]).to.eql(originalDocTimestamps[4]);
|
||||
expect(alertDocsBackfill2[0]._source![ALERT_ORIGINAL_TIME]).to.eql(testDocTimestamps[4]);
|
||||
|
||||
// backfill run 3 alerts
|
||||
const alertDocsBackfill3 = alertDocs.filter(
|
||||
|
@ -362,11 +331,11 @@ export default function createBackfillTaskRunnerTests({ getService }: FtrProvide
|
|||
);
|
||||
}
|
||||
|
||||
expect(alertDocsBackfill3[0]._source![ALERT_ORIGINAL_TIME]).to.eql(originalDocTimestamps[5]);
|
||||
expect(alertDocsBackfill3[1]._source![ALERT_ORIGINAL_TIME]).to.eql(originalDocTimestamps[6]);
|
||||
expect(alertDocsBackfill3[2]._source![ALERT_ORIGINAL_TIME]).to.eql(originalDocTimestamps[7]);
|
||||
expect(alertDocsBackfill3[3]._source![ALERT_ORIGINAL_TIME]).to.eql(originalDocTimestamps[8]);
|
||||
expect(alertDocsBackfill3[4]._source![ALERT_ORIGINAL_TIME]).to.eql(originalDocTimestamps[9]);
|
||||
expect(alertDocsBackfill3[0]._source![ALERT_ORIGINAL_TIME]).to.eql(testDocTimestamps[5]);
|
||||
expect(alertDocsBackfill3[1]._source![ALERT_ORIGINAL_TIME]).to.eql(testDocTimestamps[6]);
|
||||
expect(alertDocsBackfill3[2]._source![ALERT_ORIGINAL_TIME]).to.eql(testDocTimestamps[7]);
|
||||
expect(alertDocsBackfill3[3]._source![ALERT_ORIGINAL_TIME]).to.eql(testDocTimestamps[8]);
|
||||
expect(alertDocsBackfill3[4]._source![ALERT_ORIGINAL_TIME]).to.eql(testDocTimestamps[9]);
|
||||
|
||||
// backfill run 4 alerts
|
||||
const alertDocsBackfill4 = alertDocs.filter(
|
||||
|
@ -375,7 +344,7 @@ export default function createBackfillTaskRunnerTests({ getService }: FtrProvide
|
|||
expect(alertDocsBackfill4.length).to.eql(0);
|
||||
|
||||
// task should have been deleted after backfill runs have finished
|
||||
const numHits = await searchScheduledTask(backfillId);
|
||||
const numHits = await searchScheduledTask(es, backfillId);
|
||||
expect(numHits).to.eql(0);
|
||||
});
|
||||
|
||||
|
@ -425,7 +394,7 @@ export default function createBackfillTaskRunnerTests({ getService }: FtrProvide
|
|||
const backfillId = scheduleResult[0].id;
|
||||
|
||||
// check that the task was scheduled correctly
|
||||
const taskRecord = await getScheduledTask(backfillId);
|
||||
const taskRecord = await getScheduledTask(es, backfillId);
|
||||
expect(taskRecord.type).to.eql('task');
|
||||
expect(taskRecord.task.taskType).to.eql('ad_hoc_run-backfill');
|
||||
expect(taskRecord.task.timeoutOverride).to.eql('10s');
|
||||
|
@ -437,6 +406,8 @@ export default function createBackfillTaskRunnerTests({ getService }: FtrProvide
|
|||
|
||||
// get the execute-timeout and execute-backfill events
|
||||
const events: IValidatedEvent[] = await waitForEventLogDocs(
|
||||
retry,
|
||||
getService,
|
||||
backfillId,
|
||||
spaceId,
|
||||
new Map([
|
||||
|
@ -515,7 +486,7 @@ export default function createBackfillTaskRunnerTests({ getService }: FtrProvide
|
|||
}
|
||||
|
||||
// task should have been deleted after backfill runs have finished
|
||||
const numHits = await searchScheduledTask(backfillId);
|
||||
const numHits = await searchScheduledTask(es, backfillId);
|
||||
expect(numHits).to.eql(0);
|
||||
});
|
||||
|
||||
|
@ -567,7 +538,7 @@ export default function createBackfillTaskRunnerTests({ getService }: FtrProvide
|
|||
const backfillId = scheduleResult[0].id;
|
||||
|
||||
// check that the task was scheduled correctly
|
||||
const taskRecord = await getScheduledTask(backfillId);
|
||||
const taskRecord = await getScheduledTask(es, backfillId);
|
||||
expect(taskRecord.type).to.eql('task');
|
||||
expect(taskRecord.task.taskType).to.eql('ad_hoc_run-backfill');
|
||||
expect(taskRecord.task.timeoutOverride).to.eql('10s');
|
||||
|
@ -579,6 +550,8 @@ export default function createBackfillTaskRunnerTests({ getService }: FtrProvide
|
|||
|
||||
// get the execute-backfill events
|
||||
const events: IValidatedEvent[] = await waitForEventLogDocs(
|
||||
retry,
|
||||
getService,
|
||||
backfillId,
|
||||
spaceId,
|
||||
new Map([['execute-backfill', { equal: 4 }]])
|
||||
|
@ -652,83 +625,8 @@ export default function createBackfillTaskRunnerTests({ getService }: FtrProvide
|
|||
);
|
||||
|
||||
// task should have been deleted after backfill runs have finished
|
||||
const numHits = await searchScheduledTask(backfillId);
|
||||
const numHits = await searchScheduledTask(es, backfillId);
|
||||
expect(numHits).to.eql(0);
|
||||
});
|
||||
|
||||
async function indexTestDocs() {
|
||||
await asyncForEach(originalDocTimestamps, async (timestamp: string) => {
|
||||
await createEsDocument(es, new Date(timestamp).valueOf(), 1, ES_TEST_INDEX_NAME);
|
||||
});
|
||||
|
||||
await esTestIndexTool.waitForDocs(
|
||||
DOCUMENT_SOURCE,
|
||||
DOCUMENT_REFERENCE,
|
||||
originalDocTimestamps.length
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
async function queryForAlertDocs<T>(): Promise<Array<SearchHit<T>>> {
|
||||
const searchResult = await es.search({
|
||||
index: alertsAsDataIndex,
|
||||
body: {
|
||||
sort: [{ [ALERT_ORIGINAL_TIME]: { order: 'asc' } }],
|
||||
query: { match_all: {} },
|
||||
},
|
||||
});
|
||||
return searchResult.hits.hits as Array<SearchHit<T>>;
|
||||
}
|
||||
|
||||
async function waitForEventLogDocs(
|
||||
id: string,
|
||||
spaceId: string,
|
||||
actions: Map<string, { gte: number } | { equal: number }>
|
||||
) {
|
||||
return await retry.try(async () => {
|
||||
return await getEventLog({
|
||||
getService,
|
||||
spaceId,
|
||||
type: AD_HOC_RUN_SAVED_OBJECT_TYPE,
|
||||
id,
|
||||
provider: 'alerting',
|
||||
actions,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function getScheduledTask(id: string): Promise<TaskManagerDoc> {
|
||||
const scheduledTask = await es.get<TaskManagerDoc>({
|
||||
id: `task:${id}`,
|
||||
index: '.kibana_task_manager',
|
||||
});
|
||||
return scheduledTask._source!;
|
||||
}
|
||||
|
||||
async function searchScheduledTask(id: string) {
|
||||
const searchResult = await es.search({
|
||||
index: '.kibana_task_manager',
|
||||
body: {
|
||||
query: {
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
term: {
|
||||
'task.id': `task:${id}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
terms: {
|
||||
'task.scope': ['alerting'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// @ts-expect-error
|
||||
return searchResult.hits.total.value;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,267 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
import moment from 'moment';
|
||||
import { ESTestIndexTool } from '@kbn/alerting-api-integration-helpers';
|
||||
import { asyncForEach } from '../../../../../../functional/services/transform/api';
|
||||
import { SuperuserAtSpace1 } from '../../../../scenarios';
|
||||
import { getUrlPrefix, ObjectRemover } from '../../../../../common/lib';
|
||||
import { FtrProviderContext } from '../../../../../common/ftr_provider_context';
|
||||
import {
|
||||
TEST_ACTIONS_INDEX,
|
||||
indexTestDocs,
|
||||
getSecurityRule,
|
||||
testDocTimestamps,
|
||||
waitForEventLogDocs,
|
||||
} from './test_utils';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function scheduleBackfillTests({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const es = getService('es');
|
||||
const retry = getService('retry');
|
||||
const esTestIndexTool = new ESTestIndexTool(es, retry);
|
||||
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
||||
|
||||
describe('ad hoc backfill with rule actions', () => {
|
||||
const spaceId = SuperuserAtSpace1.space.id;
|
||||
let backfillIds: string[] = [];
|
||||
const objectRemover = new ObjectRemover(supertest);
|
||||
let connectorId: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
await esTestIndexTool.destroy();
|
||||
await esTestIndexTool.setup();
|
||||
|
||||
// Index documents
|
||||
await indexTestDocs(es, esTestIndexTool);
|
||||
|
||||
// create a connector
|
||||
const cresponse = await supertest
|
||||
.post(`${getUrlPrefix(spaceId)}/api/actions/connector`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.auth(SuperuserAtSpace1.user.username, SuperuserAtSpace1.user.password)
|
||||
.send({
|
||||
name: 'An index connector',
|
||||
connector_type_id: '.index',
|
||||
config: {
|
||||
index: TEST_ACTIONS_INDEX,
|
||||
refresh: true,
|
||||
},
|
||||
secrets: {},
|
||||
})
|
||||
.expect(200);
|
||||
connectorId = cresponse.body.id;
|
||||
objectRemover.add(spaceId, connectorId, 'connector', 'actions');
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await es.deleteByQuery({
|
||||
index: TEST_ACTIONS_INDEX,
|
||||
query: { match_all: {} },
|
||||
conflicts: 'proceed',
|
||||
});
|
||||
await asyncForEach(backfillIds, async (id: string) => {
|
||||
await supertest
|
||||
.delete(`${getUrlPrefix(spaceId)}/internal/alerting/rules/backfill/${id}`)
|
||||
.set('kbn-xsrf', 'foo');
|
||||
});
|
||||
backfillIds = [];
|
||||
await objectRemover.removeAll();
|
||||
await esTestIndexTool.destroy();
|
||||
});
|
||||
|
||||
it('should run summary actions for backfill jobs when run_actions=true', async () => {
|
||||
// create a siem query rule with an action
|
||||
const rresponse = await supertest
|
||||
.post(`${getUrlPrefix(spaceId)}/api/alerting/rule`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.auth(SuperuserAtSpace1.user.username, SuperuserAtSpace1.user.password)
|
||||
.send(
|
||||
getSecurityRule({
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
id: connectorId,
|
||||
params: { documents: [{ alertUuid: '{{alert.uuid}}' }] },
|
||||
frequency: { notify_when: 'onActiveAlert', throttle: null, summary: true },
|
||||
},
|
||||
],
|
||||
})
|
||||
)
|
||||
.expect(200);
|
||||
|
||||
const ruleId = rresponse.body.id;
|
||||
objectRemover.add(spaceId, ruleId, 'rule', 'alerting');
|
||||
|
||||
const start = moment(testDocTimestamps[1]).utc().startOf('day').toISOString();
|
||||
const end = moment(testDocTimestamps[11]).utc().startOf('day').toISOString();
|
||||
|
||||
// schedule backfill for this rule
|
||||
const response = await supertestWithoutAuth
|
||||
.post(`${getUrlPrefix(spaceId)}/internal/alerting/rules/backfill/_schedule`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.auth(SuperuserAtSpace1.user.username, SuperuserAtSpace1.user.password)
|
||||
.send([{ rule_id: ruleId, start, end, run_actions: true }])
|
||||
.expect(200);
|
||||
|
||||
const scheduleResult = response.body;
|
||||
expect(scheduleResult.length).to.eql(1);
|
||||
expect(scheduleResult[0].schedule.length).to.eql(4);
|
||||
|
||||
const backfillId = scheduleResult[0].id;
|
||||
|
||||
// wait for backfills to run
|
||||
await waitForEventLogDocs(
|
||||
retry,
|
||||
getService,
|
||||
backfillId,
|
||||
spaceId,
|
||||
new Map([['execute-backfill', { equal: 4 }]])
|
||||
);
|
||||
|
||||
await retry.try(async () => {
|
||||
// verify that the correct number of actions were executed
|
||||
const actions = await es.search({
|
||||
index: TEST_ACTIONS_INDEX,
|
||||
body: { query: { match_all: {} } },
|
||||
});
|
||||
|
||||
// 3 backfill executions resulted in alerts so 3 notifications should have
|
||||
// been generated.
|
||||
expect(actions.hits.hits.length).to.eql(3);
|
||||
});
|
||||
});
|
||||
|
||||
it('should run per-alert actions for backfill jobs when run_actions=true', async () => {
|
||||
// create a siem query rule with an action
|
||||
const rresponse = await supertest
|
||||
.post(`${getUrlPrefix(spaceId)}/api/alerting/rule`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.auth(SuperuserAtSpace1.user.username, SuperuserAtSpace1.user.password)
|
||||
.send(
|
||||
getSecurityRule({
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
id: connectorId,
|
||||
params: { documents: [{ alertUuid: '{{alert.uuid}}' }] },
|
||||
frequency: { notify_when: 'onActiveAlert', throttle: null, summary: false },
|
||||
},
|
||||
],
|
||||
})
|
||||
)
|
||||
.expect(200);
|
||||
|
||||
const ruleId = rresponse.body.id;
|
||||
objectRemover.add(spaceId, ruleId, 'rule', 'alerting');
|
||||
|
||||
const start = moment(testDocTimestamps[1]).utc().startOf('day').toISOString();
|
||||
const end = moment(testDocTimestamps[11]).utc().startOf('day').toISOString();
|
||||
|
||||
// schedule backfill for this rule
|
||||
const response = await supertestWithoutAuth
|
||||
.post(`${getUrlPrefix(spaceId)}/internal/alerting/rules/backfill/_schedule`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.auth(SuperuserAtSpace1.user.username, SuperuserAtSpace1.user.password)
|
||||
.send([{ rule_id: ruleId, start, end, run_actions: true }])
|
||||
.expect(200);
|
||||
|
||||
const scheduleResult = response.body;
|
||||
expect(scheduleResult.length).to.eql(1);
|
||||
expect(scheduleResult[0].schedule.length).to.eql(4);
|
||||
|
||||
const backfillId = scheduleResult[0].id;
|
||||
|
||||
// wait for backfills to run
|
||||
await waitForEventLogDocs(
|
||||
retry,
|
||||
getService,
|
||||
backfillId,
|
||||
spaceId,
|
||||
new Map([['execute-backfill', { equal: 4 }]])
|
||||
);
|
||||
|
||||
await retry.try(async () => {
|
||||
// verify that the correct number of actions were executed
|
||||
const actions = await es.search({
|
||||
index: TEST_ACTIONS_INDEX,
|
||||
body: { query: { match_all: {} } },
|
||||
});
|
||||
|
||||
// 3 backfill executions resulted in 9 alerts so 9 notifications should have
|
||||
// been generated.
|
||||
expect(actions.hits.hits.length).to.eql(9);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not run actions for backfill jobs when run_actions=false', async () => {
|
||||
// create a siem query rule with an action
|
||||
const rresponse = await supertest
|
||||
.post(`${getUrlPrefix(spaceId)}/api/alerting/rule`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.auth(SuperuserAtSpace1.user.username, SuperuserAtSpace1.user.password)
|
||||
.send(
|
||||
getSecurityRule({
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
id: connectorId,
|
||||
params: { documents: [{ alertUuid: '{{alert.uuid}}' }] },
|
||||
frequency: { notify_when: 'onActiveAlert', throttle: null, summary: true },
|
||||
},
|
||||
],
|
||||
})
|
||||
)
|
||||
.expect(200);
|
||||
|
||||
const ruleId = rresponse.body.id;
|
||||
objectRemover.add(spaceId, ruleId, 'rule', 'alerting');
|
||||
|
||||
const start = moment(testDocTimestamps[1]).utc().startOf('day').toISOString();
|
||||
const end = moment(testDocTimestamps[11]).utc().startOf('day').toISOString();
|
||||
|
||||
// schedule backfill for this rule
|
||||
const response = await supertestWithoutAuth
|
||||
.post(`${getUrlPrefix(spaceId)}/internal/alerting/rules/backfill/_schedule`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.auth(SuperuserAtSpace1.user.username, SuperuserAtSpace1.user.password)
|
||||
.send([{ rule_id: ruleId, start, end, run_actions: false }])
|
||||
.expect(200);
|
||||
|
||||
const scheduleResult = response.body;
|
||||
expect(scheduleResult.length).to.eql(1);
|
||||
expect(scheduleResult[0].schedule.length).to.eql(4);
|
||||
expect(scheduleResult[0].rule.actions).to.eql([]);
|
||||
|
||||
const backfillId = scheduleResult[0].id;
|
||||
|
||||
// wait for backfills to run
|
||||
await waitForEventLogDocs(
|
||||
retry,
|
||||
getService,
|
||||
backfillId,
|
||||
spaceId,
|
||||
new Map([['execute-backfill', { equal: 4 }]])
|
||||
);
|
||||
|
||||
// since we want to check that no actions were executed and they might take a bit to run
|
||||
// add a small delay
|
||||
await new Promise((resolve) => setTimeout(resolve, 5000));
|
||||
|
||||
// verify that the correct number of actions were executed
|
||||
const actions = await es.search({
|
||||
index: TEST_ACTIONS_INDEX,
|
||||
body: { query: { match_all: {} } },
|
||||
});
|
||||
|
||||
// no actions should be generated
|
||||
expect(actions.hits.hits.length).to.eql(0);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -0,0 +1,166 @@
|
|||
/*
|
||||
* 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 { asyncForEach } from '@kbn/std';
|
||||
import { ESTestIndexTool, ES_TEST_INDEX_NAME } from '@kbn/alerting-api-integration-helpers';
|
||||
import type { Client } from '@elastic/elasticsearch';
|
||||
import moment from 'moment';
|
||||
import { FtrProviderContext, RetryService } from '@kbn/ftr-common-functional-services';
|
||||
import { AD_HOC_RUN_SAVED_OBJECT_TYPE } from '@kbn/alerting-plugin/server/saved_objects';
|
||||
import { ALERT_ORIGINAL_TIME } from '@kbn/security-solution-plugin/common/field_maps/field_names';
|
||||
import { SearchHit } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import { TaskManagerDoc, getEventLog } from '../../../../../common/lib';
|
||||
import {
|
||||
DOCUMENT_REFERENCE,
|
||||
DOCUMENT_SOURCE,
|
||||
createEsDocument,
|
||||
} from '../../../../../spaces_only/tests/alerting/create_test_data';
|
||||
|
||||
export const TEST_ACTIONS_INDEX = 'alerting-backfill-test-data';
|
||||
|
||||
export const testDocTimestamps = [
|
||||
// before first backfill run
|
||||
moment().utc().subtract(14, 'days').toISOString(),
|
||||
|
||||
// backfill execution set 1
|
||||
moment().utc().startOf('day').subtract(13, 'days').add(10, 'minutes').toISOString(),
|
||||
moment().utc().startOf('day').subtract(13, 'days').add(11, 'minutes').toISOString(),
|
||||
moment().utc().startOf('day').subtract(13, 'days').add(12, 'minutes').toISOString(),
|
||||
|
||||
// backfill execution set 2
|
||||
moment().utc().startOf('day').subtract(12, 'days').add(20, 'minutes').toISOString(),
|
||||
|
||||
// backfill execution set 3
|
||||
moment().utc().startOf('day').subtract(11, 'days').add(30, 'minutes').toISOString(),
|
||||
moment().utc().startOf('day').subtract(11, 'days').add(31, 'minutes').toISOString(),
|
||||
moment().utc().startOf('day').subtract(11, 'days').add(32, 'minutes').toISOString(),
|
||||
moment().utc().startOf('day').subtract(11, 'days').add(33, 'minutes').toISOString(),
|
||||
moment().utc().startOf('day').subtract(11, 'days').add(34, 'minutes').toISOString(),
|
||||
|
||||
// backfill execution set 4 purposely left empty
|
||||
|
||||
// after last backfill
|
||||
moment().utc().startOf('day').subtract(9, 'days').add(40, 'minutes').toISOString(),
|
||||
moment().utc().startOf('day').subtract(9, 'days').add(41, 'minutes').toISOString(),
|
||||
];
|
||||
|
||||
export async function indexTestDocs(es: Client, esTestIndexTool: ESTestIndexTool) {
|
||||
await asyncForEach(testDocTimestamps, async (timestamp: string) => {
|
||||
await createEsDocument(es, new Date(timestamp).valueOf(), 1, ES_TEST_INDEX_NAME);
|
||||
});
|
||||
|
||||
await esTestIndexTool.waitForDocs(DOCUMENT_SOURCE, DOCUMENT_REFERENCE, testDocTimestamps.length);
|
||||
}
|
||||
|
||||
export async function waitForEventLogDocs(
|
||||
retry: RetryService,
|
||||
getService: FtrProviderContext['getService'],
|
||||
id: string,
|
||||
spaceId: string,
|
||||
actions: Map<string, { gte: number } | { equal: number }>
|
||||
) {
|
||||
return await retry.try(async () => {
|
||||
return await getEventLog({
|
||||
getService,
|
||||
spaceId,
|
||||
type: AD_HOC_RUN_SAVED_OBJECT_TYPE,
|
||||
id,
|
||||
provider: 'alerting',
|
||||
actions,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function getScheduledTask(es: Client, id: string): Promise<TaskManagerDoc> {
|
||||
const scheduledTask = await es.get<TaskManagerDoc>({
|
||||
id: `task:${id}`,
|
||||
index: '.kibana_task_manager',
|
||||
});
|
||||
return scheduledTask._source!;
|
||||
}
|
||||
|
||||
export async function queryForAlertDocs<T>(
|
||||
es: Client,
|
||||
index: string
|
||||
): Promise<Array<SearchHit<T>>> {
|
||||
const searchResult = await es.search({
|
||||
index,
|
||||
body: {
|
||||
sort: [{ [ALERT_ORIGINAL_TIME]: { order: 'asc' } }],
|
||||
query: { match_all: {} },
|
||||
},
|
||||
});
|
||||
return searchResult.hits.hits as Array<SearchHit<T>>;
|
||||
}
|
||||
|
||||
export async function searchScheduledTask(es: Client, id: string) {
|
||||
const searchResult = await es.search({
|
||||
index: '.kibana_task_manager',
|
||||
body: {
|
||||
query: {
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
term: {
|
||||
'task.id': `task:${id}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
terms: {
|
||||
'task.scope': ['alerting'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// @ts-expect-error
|
||||
return searchResult.hits.total.value;
|
||||
}
|
||||
|
||||
export function getSecurityRule(overwrites = {}) {
|
||||
return {
|
||||
name: 'test siem query rule with actions',
|
||||
rule_type_id: 'siem.queryRule',
|
||||
consumer: 'siem',
|
||||
enabled: true,
|
||||
actions: [],
|
||||
schedule: { interval: '24h' },
|
||||
params: {
|
||||
author: [],
|
||||
description: 'test',
|
||||
falsePositives: [],
|
||||
from: 'now-86460s',
|
||||
ruleId: '31c54f10-9d3b-45a8-b064-b92e8c6fcbe7',
|
||||
immutable: false,
|
||||
license: '',
|
||||
outputIndex: '',
|
||||
meta: { from: '1m', kibana_siem_app_url: 'https://localhost:5601/app/security' },
|
||||
maxSignals: 20,
|
||||
riskScore: 21,
|
||||
riskScoreMapping: [],
|
||||
severity: 'low',
|
||||
severityMapping: [],
|
||||
threat: [],
|
||||
to: 'now',
|
||||
references: [],
|
||||
version: 1,
|
||||
exceptionsList: [],
|
||||
relatedIntegrations: [],
|
||||
requiredFields: [],
|
||||
setup: '',
|
||||
type: 'query',
|
||||
language: 'kuery',
|
||||
index: [ES_TEST_INDEX_NAME],
|
||||
query: `source:${DOCUMENT_SOURCE}`,
|
||||
filters: [],
|
||||
},
|
||||
...overwrites,
|
||||
};
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue