diff --git a/packages/kbn-securitysolution-io-ts-types/src/import_query_schema/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/import_query_schema/index.test.ts
index d2f84b4ee3708..ab1f35c54b5db 100644
--- a/packages/kbn-securitysolution-io-ts-types/src/import_query_schema/index.test.ts
+++ b/packages/kbn-securitysolution-io-ts-types/src/import_query_schema/index.test.ts
@@ -16,6 +16,7 @@ describe('importQuerySchema', () => {
as_new_list: false,
overwrite: true,
overwrite_exceptions: true,
+ overwrite_action_connectors: true,
};
const decoded = importQuerySchema.decode(payload);
const checked = exactCheck(payload, decoded);
@@ -30,6 +31,7 @@ describe('importQuerySchema', () => {
as_new_list: false,
overwrite: 'wrong',
overwrite_exceptions: true,
+ overwrite_action_connectors: true,
};
const decoded = importQuerySchema.decode(payload);
const checked = exactCheck(payload, decoded);
@@ -48,6 +50,7 @@ describe('importQuerySchema', () => {
as_new_list: false,
overwrite: true,
overwrite_exceptions: 'wrong',
+ overwrite_action_connectors: true,
};
const decoded = importQuerySchema.decode(payload);
const checked = exactCheck(payload, decoded);
@@ -58,6 +61,24 @@ describe('importQuerySchema', () => {
]);
expect(message.schema).toEqual({});
});
+ test('it should NOT validate a non boolean value for "overwrite_action_connectors"', () => {
+ const payload: Omit & {
+ overwrite_action_connectors: string;
+ } = {
+ as_new_list: false,
+ overwrite: true,
+ overwrite_exceptions: true,
+ overwrite_action_connectors: 'wrong',
+ };
+ const decoded = importQuerySchema.decode(payload);
+ const checked = exactCheck(payload, decoded);
+ const message = foldLeftRight(checked);
+
+ expect(getPaths(left(message.errors))).toEqual([
+ 'Invalid value "wrong" supplied to "overwrite_action_connectors"',
+ ]);
+ expect(message.schema).toEqual({});
+ });
test('it should NOT allow an extra key to be sent in', () => {
const payload: ImportQuerySchema & {
diff --git a/packages/kbn-securitysolution-io-ts-types/src/import_query_schema/index.ts b/packages/kbn-securitysolution-io-ts-types/src/import_query_schema/index.ts
index ca0e35c4aa176..5b1ee38bb0855 100644
--- a/packages/kbn-securitysolution-io-ts-types/src/import_query_schema/index.ts
+++ b/packages/kbn-securitysolution-io-ts-types/src/import_query_schema/index.ts
@@ -14,6 +14,7 @@ export const importQuerySchema = t.exact(
t.partial({
overwrite: DefaultStringBooleanFalse,
overwrite_exceptions: DefaultStringBooleanFalse,
+ overwrite_action_connectors: DefaultStringBooleanFalse,
as_new_list: DefaultStringBooleanFalse,
})
);
@@ -21,9 +22,10 @@ export const importQuerySchema = t.exact(
export type ImportQuerySchema = t.TypeOf;
export type ImportQuerySchemaDecoded = Omit<
ImportQuerySchema,
- 'overwrite' | 'overwrite_exceptions' | 'as_new_list'
+ 'overwrite' | 'overwrite_exceptions' | 'as_new_list' | 'overwrite_action_connectors'
> & {
overwrite: boolean;
overwrite_exceptions: boolean;
+ overwrite_action_connectors: boolean;
as_new_list: boolean;
};
diff --git a/x-pack/plugins/alerting/server/rules_client/lib/validate_actions.ts b/x-pack/plugins/alerting/server/rules_client/lib/validate_actions.ts
index 3c97080e2d655..ea6145b8f6e99 100644
--- a/x-pack/plugins/alerting/server/rules_client/lib/validate_actions.ts
+++ b/x-pack/plugins/alerting/server/rules_client/lib/validate_actions.ts
@@ -17,7 +17,10 @@ import { parseDuration } from '../../lib';
export async function validateActions(
context: RulesClientContext,
alertType: UntypedNormalizedRuleType,
- data: Pick & { actions: NormalizedAlertAction[] }
+ data: Pick & {
+ actions: NormalizedAlertAction[];
+ },
+ allowMissingConnectorSecrets?: boolean
): Promise {
const { actions, notifyWhen, throttle } = data;
const hasRuleLevelNotifyWhen = typeof notifyWhen !== 'undefined';
@@ -35,20 +38,26 @@ export async function validateActions(
const actionsUsingConnectorsWithMissingSecrets = actionResults.filter(
(result) => result.isMissingSecrets
);
-
if (actionsUsingConnectorsWithMissingSecrets.length) {
- errors.push(
- i18n.translate('xpack.alerting.rulesClient.validateActions.misconfiguredConnector', {
- defaultMessage: 'Invalid connectors: {groups}',
- values: {
- groups: actionsUsingConnectorsWithMissingSecrets
- .map((connector) => connector.name)
- .join(', '),
- },
- })
- );
+ if (allowMissingConnectorSecrets) {
+ context.logger.error(
+ `Invalid connectors with "allowMissingConnectorSecrets": ${actionsUsingConnectorsWithMissingSecrets
+ .map((connector) => connector.name)
+ .join(', ')}`
+ );
+ } else {
+ errors.push(
+ i18n.translate('xpack.alerting.rulesClient.validateActions.misconfiguredConnector', {
+ defaultMessage: 'Invalid connectors: {groups}',
+ values: {
+ groups: actionsUsingConnectorsWithMissingSecrets
+ .map((connector) => connector.name)
+ .join(', '),
+ },
+ })
+ );
+ }
}
-
// check for actions with invalid action groups
const { actionGroups: alertTypeActionGroups } = alertType;
const usedAlertActionGroups = actions.map((action) => action.group);
diff --git a/x-pack/plugins/alerting/server/rules_client/methods/create.ts b/x-pack/plugins/alerting/server/rules_client/methods/create.ts
index e8dcefe4a9ef1..09dc8f62494b8 100644
--- a/x-pack/plugins/alerting/server/rules_client/methods/create.ts
+++ b/x-pack/plugins/alerting/server/rules_client/methods/create.ts
@@ -46,11 +46,12 @@ export interface CreateOptions {
| 'nextRun'
> & { actions: NormalizedAlertAction[] };
options?: SavedObjectOptions;
+ allowMissingConnectorSecrets?: boolean;
}
export async function create(
context: RulesClientContext,
- { data, options }: CreateOptions
+ { data, options, allowMissingConnectorSecrets }: CreateOptions
): Promise> {
const id = options?.id || SavedObjectsUtils.generateId();
@@ -104,10 +105,11 @@ export async function create(
}
}
- await validateActions(context, ruleType, data);
+ await validateActions(context, ruleType, data, allowMissingConnectorSecrets);
await withSpan({ name: 'validateActions', type: 'rules' }, () =>
- validateActions(context, ruleType, data)
+ validateActions(context, ruleType, data, allowMissingConnectorSecrets)
);
+
// Throw error if schedule interval is less than the minimum and we are enforcing it
const intervalInMs = parseDuration(data.schedule.interval);
if (
diff --git a/x-pack/plugins/alerting/server/rules_client/methods/update.ts b/x-pack/plugins/alerting/server/rules_client/methods/update.ts
index 8fa3855b95707..5e116f9f21b28 100644
--- a/x-pack/plugins/alerting/server/rules_client/methods/update.ts
+++ b/x-pack/plugins/alerting/server/rules_client/methods/update.ts
@@ -38,22 +38,23 @@ export interface UpdateOptions {
throttle?: string | null;
notifyWhen?: RuleNotifyWhenType | null;
};
+ allowMissingConnectorSecrets?: boolean;
}
export async function update(
context: RulesClientContext,
- { id, data }: UpdateOptions
+ { id, data, allowMissingConnectorSecrets }: UpdateOptions
): Promise> {
return await retryIfConflicts(
context.logger,
`rulesClient.update('${id}')`,
- async () => await updateWithOCC(context, { id, data })
+ async () => await updateWithOCC(context, { id, data, allowMissingConnectorSecrets })
);
}
async function updateWithOCC(
context: RulesClientContext,
- { id, data }: UpdateOptions
+ { id, data, allowMissingConnectorSecrets }: UpdateOptions
): Promise> {
let alertSavedObject: SavedObject;
@@ -99,7 +100,11 @@ async function updateWithOCC(
context.ruleTypeRegistry.ensureRuleTypeEnabled(alertSavedObject.attributes.alertTypeId);
- const updateResult = await updateAlert(context, { id, data }, alertSavedObject);
+ const updateResult = await updateAlert(
+ context,
+ { id, data, allowMissingConnectorSecrets },
+ alertSavedObject
+ );
await Promise.all([
alertSavedObject.attributes.apiKey
@@ -138,7 +143,7 @@ async function updateWithOCC(
async function updateAlert(
context: RulesClientContext,
- { id, data }: UpdateOptions,
+ { id, data, allowMissingConnectorSecrets }: UpdateOptions,
{ attributes, version }: SavedObject
): Promise> {
const ruleType = context.ruleTypeRegistry.get(attributes.alertTypeId);
@@ -156,7 +161,7 @@ async function updateAlert(
// Validate
const validatedAlertTypeParams = validateRuleTypeParams(data.params, ruleType.validate?.params);
- await validateActions(context, ruleType, data);
+ await validateActions(context, ruleType, data, allowMissingConnectorSecrets);
// Throw error if schedule interval is less than the minimum and we are enforcing it
const intervalInMs = parseDuration(data.schedule.interval);
diff --git a/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts
index c11dc8be21ca4..dddfbe85f9ee3 100644
--- a/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts
+++ b/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts
@@ -3047,4 +3047,120 @@ describe('create()', () => {
expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled();
expect(taskManager.schedule).not.toHaveBeenCalled();
});
+ test('should create a rule even if action is missing secret when allowMissingConnectorSecrets is true', async () => {
+ const data = getMockData({
+ actions: [
+ {
+ group: 'default',
+ id: '1',
+ params: {
+ foo: true,
+ },
+ },
+ {
+ group: 'default',
+ id: '1',
+ params: {
+ foo: true,
+ },
+ },
+ {
+ group: 'default',
+ id: '2',
+ params: {
+ foo: true,
+ },
+ },
+ ],
+ });
+ actionsClient.getBulk.mockReset();
+ actionsClient.getBulk.mockResolvedValue([
+ {
+ id: '1',
+ actionTypeId: '.slack',
+ config: {},
+ isMissingSecrets: true,
+ name: 'Slack connector',
+ isPreconfigured: false,
+ isDeprecated: false,
+ },
+ ]);
+ unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
+ id: '1',
+ type: 'alert',
+ attributes: {
+ alertTypeId: '123',
+ schedule: { interval: '1m' },
+ params: {
+ bar: true,
+ },
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
+ notifyWhen: null,
+ actions: [
+ {
+ group: 'default',
+ actionRef: 'action_0',
+ actionTypeId: '.slack',
+ params: {
+ foo: true,
+ },
+ },
+ ],
+ },
+ references: [
+ {
+ name: 'action_0',
+ type: 'action',
+ id: '1',
+ },
+ {
+ name: 'action_1',
+ type: 'action',
+ id: '1',
+ },
+ {
+ name: 'action_2',
+ type: 'action',
+ id: '2',
+ },
+ ],
+ });
+ unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
+ id: '1',
+ type: 'alert',
+ attributes: {
+ actions: [],
+ scheduledTaskId: 'task-123',
+ },
+ references: [],
+ });
+ const result = await rulesClient.create({ data, allowMissingConnectorSecrets: true });
+ expect(result).toMatchInlineSnapshot(`
+ Object {
+ "actions": Array [
+ Object {
+ "actionTypeId": ".slack",
+ "group": "default",
+ "id": "1",
+ "params": Object {
+ "foo": true,
+ },
+ },
+ ],
+ "alertTypeId": "123",
+ "createdAt": 2019-02-12T21:01:22.479Z,
+ "id": "1",
+ "notifyWhen": null,
+ "params": Object {
+ "bar": true,
+ },
+ "schedule": Object {
+ "interval": "1m",
+ },
+ "scheduledTaskId": "task-123",
+ "updatedAt": 2019-02-12T21:01:22.479Z,
+ }
+ `);
+ });
});
diff --git a/x-pack/plugins/alerting/server/rules_client/tests/update.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/update.test.ts
index 44c4eeb50fe27..4d16f3e5d66a0 100644
--- a/x-pack/plugins/alerting/server/rules_client/tests/update.test.ts
+++ b/x-pack/plugins/alerting/server/rules_client/tests/update.test.ts
@@ -2022,6 +2022,155 @@ describe('update()', () => {
expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled();
expect(taskManager.schedule).not.toHaveBeenCalled();
});
+ test('should update a rule even if action is missing secret when allowMissingConnectorSecrets is true', async () => {
+ // Reset from default behaviour
+ actionsClient.getBulk.mockReset();
+ actionsClient.getBulk.mockResolvedValue([
+ {
+ id: '1',
+ actionTypeId: '.slack',
+ config: {},
+ isMissingSecrets: true,
+ name: 'slack connector',
+ isPreconfigured: false,
+ isDeprecated: false,
+ },
+ ]);
+ actionsClient.isPreconfigured.mockReset();
+ actionsClient.isPreconfigured.mockReturnValueOnce(false);
+ actionsClient.isPreconfigured.mockReturnValueOnce(true);
+ actionsClient.isPreconfigured.mockReturnValueOnce(true);
+ unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
+ id: '1',
+ type: 'alert',
+ attributes: {
+ enabled: true,
+ schedule: { interval: '1m' },
+ params: {
+ bar: true,
+ },
+ actions: [
+ {
+ group: 'default',
+ actionRef: 'action_0',
+ actionTypeId: '.slack',
+ params: {
+ foo: true,
+ },
+ },
+ ],
+ notifyWhen: 'onActiveAlert',
+ scheduledTaskId: 'task-123',
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
+ },
+ references: [
+ {
+ name: 'action_0',
+ type: 'action',
+ id: '1',
+ },
+ {
+ name: 'param:soRef_0',
+ type: 'someSavedObjectType',
+ id: '9',
+ },
+ ],
+ });
+ const result = await rulesClient.update({
+ id: '1',
+ data: {
+ schedule: { interval: '1m' },
+ name: 'abc',
+ tags: ['foo'],
+ params: {
+ bar: true,
+ },
+ throttle: null,
+ notifyWhen: 'onActiveAlert',
+ actions: [
+ {
+ group: 'default',
+ id: '1',
+ params: {
+ foo: true,
+ },
+ },
+ ],
+ },
+ allowMissingConnectorSecrets: true,
+ });
+
+ expect(unsecuredSavedObjectsClient.create).toHaveBeenNthCalledWith(
+ 1,
+ 'alert',
+ {
+ actions: [
+ {
+ group: 'default',
+ actionRef: 'action_0',
+ actionTypeId: '.slack',
+ params: {
+ foo: true,
+ },
+ },
+ ],
+ alertTypeId: 'myType',
+ apiKey: null,
+ apiKeyOwner: null,
+ consumer: 'myApp',
+ enabled: true,
+ meta: { versionApiKeyLastmodified: 'v7.10.0' },
+ name: 'abc',
+ notifyWhen: 'onActiveAlert',
+ params: { bar: true },
+ schedule: { interval: '1m' },
+ scheduledTaskId: 'task-123',
+ tags: ['foo'],
+ throttle: null,
+ updatedAt: '2019-02-12T21:01:22.479Z',
+ updatedBy: 'elastic',
+ },
+ {
+ id: '1',
+ overwrite: true,
+ references: [{ id: '1', name: 'action_0', type: 'action' }],
+ version: '123',
+ }
+ );
+
+ expect(result).toMatchInlineSnapshot(`
+ Object {
+ "actions": Array [
+ Object {
+ "actionTypeId": ".slack",
+ "group": "default",
+ "id": "1",
+ "params": Object {
+ "foo": true,
+ },
+ },
+ ],
+ "createdAt": 2019-02-12T21:01:22.479Z,
+ "enabled": true,
+ "id": "1",
+ "notifyWhen": "onActiveAlert",
+ "params": Object {
+ "bar": true,
+ },
+ "schedule": Object {
+ "interval": "1m",
+ },
+ "scheduledTaskId": "task-123",
+ "updatedAt": 2019-02-12T21:01:22.479Z,
+ }
+ `);
+ expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', {
+ namespace: 'default',
+ });
+ expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled();
+ expect(actionsClient.isPreconfigured).toHaveBeenCalledTimes(1);
+ });
test('logs warning when creating with an interval less than the minimum configured one when enforce = false', async () => {
actionsClient.getBulk.mockReset();
diff --git a/x-pack/plugins/security_solution/common/detection_engine/rule_management/api/rules/import_rules/response_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/rule_management/api/rules/import_rules/response_schema.test.ts
index 2f11e1a9c120f..b2ff257ba2c4e 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/rule_management/api/rules/import_rules/response_schema.test.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/rule_management/api/rules/import_rules/response_schema.test.ts
@@ -24,6 +24,10 @@ describe('Import rules response schema', () => {
exceptions_errors: [],
exceptions_success: true,
exceptions_success_count: 0,
+ action_connectors_success: true,
+ action_connectors_success_count: 0,
+ action_connectors_errors: [],
+ action_connectors_warnings: [],
};
const decoded = ImportRulesResponse.decode(payload);
const checked = exactCheck(payload, decoded);
@@ -42,6 +46,10 @@ describe('Import rules response schema', () => {
exceptions_errors: [],
exceptions_success: true,
exceptions_success_count: 0,
+ action_connectors_success: true,
+ action_connectors_success_count: 0,
+ action_connectors_errors: [],
+ action_connectors_warnings: [],
};
const decoded = ImportRulesResponse.decode(payload);
const checked = exactCheck(payload, decoded);
@@ -60,6 +68,10 @@ describe('Import rules response schema', () => {
exceptions_errors: [{ error: { status_code: 400, message: 'some message' } }],
exceptions_success: true,
exceptions_success_count: 0,
+ action_connectors_success: true,
+ action_connectors_success_count: 0,
+ action_connectors_errors: [],
+ action_connectors_warnings: [],
};
const decoded = ImportRulesResponse.decode(payload);
const checked = exactCheck(payload, decoded);
@@ -81,6 +93,10 @@ describe('Import rules response schema', () => {
exceptions_errors: [],
exceptions_success: true,
exceptions_success_count: 0,
+ action_connectors_success: true,
+ action_connectors_success_count: 0,
+ action_connectors_errors: [],
+ action_connectors_warnings: [],
};
const decoded = ImportRulesResponse.decode(payload);
const checked = exactCheck(payload, decoded);
@@ -102,6 +118,10 @@ describe('Import rules response schema', () => {
],
exceptions_success: true,
exceptions_success_count: 0,
+ action_connectors_success: true,
+ action_connectors_success_count: 0,
+ action_connectors_errors: [],
+ action_connectors_warnings: [],
};
const decoded = ImportRulesResponse.decode(payload);
const checked = exactCheck(payload, decoded);
@@ -120,6 +140,10 @@ describe('Import rules response schema', () => {
exceptions_errors: [],
exceptions_success: true,
exceptions_success_count: 0,
+ action_connectors_success: true,
+ action_connectors_success_count: 0,
+ action_connectors_errors: [],
+ action_connectors_warnings: [],
};
const decoded = ImportRulesResponse.decode(payload);
const checked = exactCheck(payload, decoded);
@@ -140,6 +164,10 @@ describe('Import rules response schema', () => {
exceptions_errors: [],
exceptions_success: true,
exceptions_success_count: -1,
+ action_connectors_success: true,
+ action_connectors_success_count: 0,
+ action_connectors_errors: [],
+ action_connectors_warnings: [],
};
const decoded = ImportRulesResponse.decode(payload);
const checked = exactCheck(payload, decoded);
@@ -178,6 +206,10 @@ describe('Import rules response schema', () => {
exceptions_errors: [],
exceptions_success: true,
exceptions_success_count: 0,
+ action_connectors_success: true,
+ action_connectors_success_count: 0,
+ action_connectors_errors: [],
+ action_connectors_warnings: [],
};
const decoded = ImportRulesResponse.decode(payload);
const checked = exactCheck(payload, decoded as UnsafeCastForTest);
@@ -217,6 +249,10 @@ describe('Import rules response schema', () => {
exceptions_errors: [],
exceptions_success: 'hello',
exceptions_success_count: 0,
+ action_connectors_success: true,
+ action_connectors_success_count: 0,
+ action_connectors_errors: [],
+ action_connectors_warnings: [],
};
const decoded = ImportRulesResponse.decode(payload);
const checked = exactCheck(payload, decoded as UnsafeCastForTest);
@@ -238,6 +274,10 @@ describe('Import rules response schema', () => {
exceptions_errors: [],
exceptions_success: true,
exceptions_success_count: 0,
+ action_connectors_success: true,
+ action_connectors_success_count: 0,
+ action_connectors_errors: [],
+ action_connectors_warnings: [],
};
const decoded = ImportRulesResponse.decode(payload);
const checked = exactCheck(payload, decoded);
@@ -262,6 +302,10 @@ describe('Import rules response schema', () => {
exceptions_errors: [],
exceptions_success: true,
exceptions_success_count: 0,
+ action_connectors_success: true,
+ action_connectors_success_count: 0,
+ action_connectors_errors: [],
+ action_connectors_warnings: [],
};
const decoded = ImportRulesResponse.decode(payload);
const checked = exactCheck(payload, decoded);
@@ -270,4 +314,182 @@ describe('Import rules response schema', () => {
expect(getPaths(left(message.errors))).toEqual(['invalid keys "invalid_data"']);
expect(message.schema).toEqual({});
});
+
+ test('it should validate an empty import response with a single connectors error', () => {
+ const payload: ImportRulesResponse = {
+ success: false,
+ success_count: 0,
+ rules_count: 0,
+ errors: [],
+ exceptions_errors: [],
+ exceptions_success: true,
+ exceptions_success_count: 0,
+ action_connectors_success: true,
+ action_connectors_success_count: 0,
+ action_connectors_errors: [{ error: { status_code: 400, message: 'some message' } }],
+ action_connectors_warnings: [],
+ };
+ const decoded = ImportRulesResponse.decode(payload);
+ const checked = exactCheck(payload, decoded);
+ const message = pipe(checked, foldLeftRight);
+
+ expect(getPaths(left(message.errors))).toEqual([]);
+ expect(message.schema).toEqual(payload);
+ });
+ test('it should validate an empty import response with multiple errors', () => {
+ const payload: ImportRulesResponse = {
+ success: false,
+ success_count: 0,
+ rules_count: 0,
+ errors: [
+ { error: { status_code: 400, message: 'some message' } },
+ { error: { status_code: 500, message: 'some message' } },
+ ],
+ exceptions_errors: [],
+ exceptions_success: true,
+ exceptions_success_count: 0,
+ action_connectors_success: true,
+ action_connectors_success_count: 0,
+ action_connectors_errors: [{ error: { status_code: 400, message: 'some message' } }],
+ action_connectors_warnings: [],
+ };
+ const decoded = ImportRulesResponse.decode(payload);
+ const checked = exactCheck(payload, decoded);
+ const message = pipe(checked, foldLeftRight);
+
+ expect(getPaths(left(message.errors))).toEqual([]);
+ expect(message.schema).toEqual(payload);
+ });
+ test('it should NOT validate action_connectors_success that is not boolean', () => {
+ type UnsafeCastForTest = Either<
+ Errors,
+ {
+ success: boolean;
+ action_connectors_success: string;
+ success_count: number;
+ errors: Array<
+ {
+ id?: string | undefined;
+ rule_id?: string | undefined;
+ } & {
+ error: {
+ status_code: number;
+ message: string;
+ };
+ }
+ >;
+ }
+ >;
+ const payload: Omit & {
+ action_connectors_success: string;
+ } = {
+ success: true,
+ success_count: 0,
+ rules_count: 0,
+ errors: [],
+ exceptions_errors: [],
+ exceptions_success: true,
+ exceptions_success_count: 0,
+ action_connectors_success: 'invalid',
+ action_connectors_success_count: 0,
+ action_connectors_errors: [],
+ action_connectors_warnings: [],
+ };
+ const decoded = ImportRulesResponse.decode(payload);
+ const checked = exactCheck(payload, decoded as UnsafeCastForTest);
+ const message = pipe(checked, foldLeftRight);
+
+ expect(getPaths(left(message.errors))).toEqual([
+ 'Invalid value "invalid" supplied to "action_connectors_success"',
+ ]);
+ expect(message.schema).toEqual({});
+ });
+ test('it should NOT validate a action_connectors_success_count that is a negative number', () => {
+ const payload: ImportRulesResponse = {
+ success: false,
+ success_count: 0,
+ rules_count: 0,
+ errors: [],
+ exceptions_errors: [],
+ exceptions_success: true,
+ exceptions_success_count: 0,
+ action_connectors_success: true,
+ action_connectors_success_count: -1,
+ action_connectors_errors: [],
+ action_connectors_warnings: [],
+ };
+ const decoded = ImportRulesResponse.decode(payload);
+ const checked = exactCheck(payload, decoded);
+ const message = pipe(checked, foldLeftRight);
+
+ expect(getPaths(left(message.errors))).toEqual([
+ 'Invalid value "-1" supplied to "action_connectors_success_count"',
+ ]);
+ expect(message.schema).toEqual({});
+ });
+ test('it should validate a action_connectors_warnings after importing successfully', () => {
+ const payload: ImportRulesResponse = {
+ success: false,
+ success_count: 0,
+ rules_count: 0,
+ errors: [],
+ exceptions_errors: [],
+ exceptions_success: true,
+ exceptions_success_count: 0,
+ action_connectors_success: true,
+ action_connectors_success_count: 1,
+ action_connectors_errors: [],
+ action_connectors_warnings: [{ type: 'type', message: 'message', actionPath: 'actionPath' }],
+ };
+ const decoded = ImportRulesResponse.decode(payload);
+ const checked = exactCheck(payload, decoded);
+ const message = pipe(checked, foldLeftRight);
+
+ expect(getPaths(left(message.errors))).toEqual([]);
+ expect(message.schema).toEqual(payload);
+ });
+ test('it should NOT validate a action_connectors_warnings that is not WarningSchema', () => {
+ type UnsafeCastForTest = Either<
+ Errors,
+ {
+ success: boolean;
+ action_connectors_warnings: string;
+ success_count: number;
+ errors: Array<
+ {
+ id?: string | undefined;
+ rule_id?: string | undefined;
+ } & {
+ error: {
+ status_code: number;
+ message: string;
+ };
+ }
+ >;
+ }
+ >;
+ const payload: Omit & {
+ action_connectors_warnings: string;
+ } = {
+ success: true,
+ success_count: 0,
+ rules_count: 0,
+ errors: [],
+ exceptions_errors: [],
+ exceptions_success: true,
+ exceptions_success_count: 0,
+ action_connectors_success: true,
+ action_connectors_success_count: 0,
+ action_connectors_errors: [],
+ action_connectors_warnings: 'invalid',
+ };
+ const decoded = ImportRulesResponse.decode(payload);
+ const checked = exactCheck(payload, decoded as UnsafeCastForTest);
+ const message = pipe(checked, foldLeftRight);
+
+ expect(getPaths(left(message.errors))).toEqual([
+ 'Invalid value "invalid" supplied to "action_connectors_warnings"',
+ ]);
+ expect(message.schema).toEqual({});
+ });
});
diff --git a/x-pack/plugins/security_solution/common/detection_engine/rule_management/api/rules/import_rules/response_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/rule_management/api/rules/import_rules/response_schema.ts
index 77ccd0812c2c9..99212b902c4d3 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/rule_management/api/rules/import_rules/response_schema.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/rule_management/api/rules/import_rules/response_schema.ts
@@ -7,7 +7,7 @@
import * as t from 'io-ts';
import { PositiveInteger } from '@kbn/securitysolution-io-ts-types';
-import { errorSchema } from '../../../../schemas/response/error_schema';
+import { errorSchema, warningSchema } from '../../../../schemas/response';
export type ImportRulesResponse = t.TypeOf;
export const ImportRulesResponse = t.exact(
@@ -19,5 +19,9 @@ export const ImportRulesResponse = t.exact(
success: t.boolean,
success_count: PositiveInteger,
errors: t.array(errorSchema),
+ action_connectors_errors: t.array(errorSchema),
+ action_connectors_warnings: t.array(warningSchema),
+ action_connectors_success: t.boolean,
+ action_connectors_success_count: PositiveInteger,
})
);
diff --git a/x-pack/plugins/security_solution/common/detection_engine/rule_management/model/export/export_rules_details_schema.mock.ts b/x-pack/plugins/security_solution/common/detection_engine/rule_management/model/export/export_rules_details_schema.mock.ts
index 64b82abdb3755..9a26b1a09f066 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/rule_management/model/export/export_rules_details_schema.mock.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/rule_management/model/export/export_rules_details_schema.mock.ts
@@ -8,6 +8,7 @@
import { getExceptionExportDetailsMock } from '@kbn/lists-plugin/common/schemas/response/exception_export_details_schema.mock';
import type { ExportExceptionDetailsMock } from '@kbn/lists-plugin/common/schemas/response/exception_export_details_schema.mock';
import type { ExportRulesDetails } from './export_rules_details_schema';
+import type { DefaultActionConnectorDetails } from '../../../../../server/lib/detection_engine/rule_management/logic/export/get_export_rule_action_connectors';
interface RuleDetailsMock {
totalCount?: number;
@@ -16,6 +17,23 @@ interface RuleDetailsMock {
missingRules?: Array>;
}
+export const getActionConnectorDetailsMock = (): DefaultActionConnectorDetails => ({
+ exported_action_connector_count: 0,
+ missing_action_connection_count: 0,
+ missing_action_connections: [],
+ excluded_action_connection_count: 0,
+ excluded_action_connections: [],
+});
+export const getOutputDetailsSampleWithActionConnectors = (): ExportRulesDetails => ({
+ ...getOutputDetailsSample(),
+ ...getExceptionExportDetailsMock(),
+ exported_action_connector_count: 1,
+ missing_action_connection_count: 0,
+ missing_action_connections: [],
+ excluded_action_connection_count: 0,
+ excluded_action_connections: [],
+});
+
export const getOutputDetailsSample = (ruleDetails?: RuleDetailsMock): ExportRulesDetails => ({
exported_count: ruleDetails?.totalCount ?? 0,
exported_rules_count: ruleDetails?.rulesCount ?? 0,
@@ -29,6 +47,7 @@ export const getOutputDetailsSampleWithExceptions = (
): ExportRulesDetails => ({
...getOutputDetailsSample(ruleDetails),
...getExceptionExportDetailsMock(exceptionDetails),
+ ...getActionConnectorDetailsMock(),
});
export const getSampleDetailsAsNdjson = (sample: ExportRulesDetails): string => {
diff --git a/x-pack/plugins/security_solution/common/detection_engine/rule_management/model/export/export_rules_details_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/rule_management/model/export/export_rules_details_schema.test.ts
index 2c84a34b34928..a91f59924ef72 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/rule_management/model/export/export_rules_details_schema.test.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/rule_management/model/export/export_rules_details_schema.test.ts
@@ -18,15 +18,16 @@ import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts
import {
getOutputDetailsSample,
+ getOutputDetailsSampleWithActionConnectors,
getOutputDetailsSampleWithExceptions,
} from './export_rules_details_schema.mock';
import type { ExportRulesDetails } from './export_rules_details_schema';
-import { exportRulesDetailsWithExceptionsSchema } from './export_rules_details_schema';
+import { exportRulesDetailsWithExceptionsAndConnectorsSchema } from './export_rules_details_schema';
-describe('exportRulesDetailsWithExceptionsSchema', () => {
+describe('exportRulesDetailsWithExceptionsAndConnectorsSchema', () => {
test('it should validate export details response', () => {
const payload = getOutputDetailsSample();
- const decoded = exportRulesDetailsWithExceptionsSchema.decode(payload);
+ const decoded = exportRulesDetailsWithExceptionsAndConnectorsSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
@@ -36,7 +37,7 @@ describe('exportRulesDetailsWithExceptionsSchema', () => {
test('it should validate export details with exceptions details response', () => {
const payload = getOutputDetailsSampleWithExceptions();
- const decoded = exportRulesDetailsWithExceptionsSchema.decode(payload);
+ const decoded = exportRulesDetailsWithExceptionsAndConnectorsSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
@@ -44,12 +45,21 @@ describe('exportRulesDetailsWithExceptionsSchema', () => {
expect(message.schema).toEqual(payload);
});
+ test('it should validate export details with action connectors details response', () => {
+ const payload = getOutputDetailsSampleWithActionConnectors();
+ const decoded = exportRulesDetailsWithExceptionsAndConnectorsSchema.decode(payload);
+ const checked = exactCheck(payload, decoded);
+ const message = pipe(checked, foldLeftRight);
+
+ expect(getPaths(left(message.errors))).toEqual([]);
+ expect(message.schema).toEqual(payload);
+ });
test('it should strip out extra keys', () => {
const payload: ExportRulesDetails & {
extraKey?: string;
} = getOutputDetailsSample();
payload.extraKey = 'some extra key';
- const decoded = exportRulesDetailsWithExceptionsSchema.decode(payload);
+ const decoded = exportRulesDetailsWithExceptionsAndConnectorsSchema.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
diff --git a/x-pack/plugins/security_solution/common/detection_engine/rule_management/model/export/export_rules_details_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/rule_management/model/export/export_rules_details_schema.ts
index 85b423135566b..205861bdd42a7 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/rule_management/model/export/export_rules_details_schema.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/rule_management/model/export/export_rules_details_schema.ts
@@ -9,13 +9,6 @@ import * as t from 'io-ts';
import { exportExceptionDetails } from '@kbn/securitysolution-io-ts-list-types';
import { NonEmptyString } from '@kbn/securitysolution-io-ts-types';
-const createSchema = (
- requiredFields: Required,
- optionalFields: Optional
-) => {
- return t.intersection([t.exact(t.type(requiredFields)), t.exact(t.partial(optionalFields))]);
-};
-
const exportRulesDetails = {
exported_count: t.number,
exported_rules_count: t.number,
@@ -28,11 +21,38 @@ const exportRulesDetails = {
),
missing_rules_count: t.number,
};
+const excludedActionConnectors = t.intersection([
+ t.exact(
+ t.type({
+ id: NonEmptyString,
+ type: NonEmptyString,
+ })
+ ),
+ t.exact(t.partial({ reason: t.string })),
+]);
+
+const exportRuleActionConnectorsDetails = {
+ exported_action_connector_count: t.number,
+ missing_action_connection_count: t.number,
+ missing_action_connections: t.array(
+ t.exact(
+ t.type({
+ id: NonEmptyString,
+ type: NonEmptyString,
+ })
+ )
+ ),
+ excluded_action_connection_count: t.number,
+ excluded_action_connections: t.array(excludedActionConnectors),
+};
-// With exceptions
-export const exportRulesDetailsWithExceptionsSchema = createSchema(
- exportRulesDetails,
- exportExceptionDetails
-);
+// With exceptions and connectors
+export const exportRulesDetailsWithExceptionsAndConnectorsSchema = t.intersection([
+ t.exact(t.type(exportRulesDetails)),
+ t.exact(t.partial(exportExceptionDetails)),
+ t.exact(t.partial(exportRuleActionConnectorsDetails)),
+]);
-export type ExportRulesDetails = t.TypeOf;
+export type ExportRulesDetails = t.TypeOf<
+ typeof exportRulesDetailsWithExceptionsAndConnectorsSchema
+>;
diff --git a/x-pack/plugins/security_solution/common/detection_engine/rule_management/model/import/rule_to_import.mock.ts b/x-pack/plugins/security_solution/common/detection_engine/rule_management/model/import/rule_to_import.mock.ts
index d1dc9e8ac4663..cec58d1c5fdc6 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/rule_management/model/import/rule_to_import.mock.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/rule_management/model/import/rule_to_import.mock.ts
@@ -80,3 +80,41 @@ export const getImportThreatMatchRulesSchemaMock = (ruleId = 'rule-1'): RuleToIm
},
],
});
+
+export const webHookConnector = {
+ id: 'cabc78e0-9031-11ed-b076-53cc4d57aaf1',
+ type: 'action',
+ updated_at: '2023-01-25T14:35:52.852Z',
+ created_at: '2023-01-25T14:35:52.852Z',
+ version: 'WzUxNTksMV0=',
+ attributes: {
+ actionTypeId: '.webhook',
+ name: 'webhook',
+ isMissingSecrets: false,
+ config: {},
+ secrets: {},
+ },
+ references: [],
+ migrationVersion: { action: '8.3.0' },
+ coreMigrationVersion: '8.7.0',
+};
+
+export const ruleWithConnectorNdJSON = (): string => {
+ const items = [
+ {
+ ...getImportRulesSchemaMock(),
+ actions: [
+ {
+ group: 'default',
+ id: 'cabc78e0-9031-11ed-b076-53cc4d57aaf1',
+ action_type_id: '.webhook',
+ params: {},
+ },
+ ],
+ },
+ webHookConnector,
+ ];
+ const stringOfExceptions = items.map((item) => JSON.stringify(item));
+
+ return stringOfExceptions.join('\n');
+};
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/index.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/index.ts
index b20a956525e2e..76da687604028 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/index.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/index.ts
@@ -6,3 +6,4 @@
*/
export * from './error_schema';
+export * from './warning_schema';
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/warning_schema/index.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/warning_schema/index.ts
new file mode 100644
index 0000000000000..1a401d1941cb0
--- /dev/null
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/warning_schema/index.ts
@@ -0,0 +1,24 @@
+/*
+ * 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 * as t from 'io-ts';
+
+const partial = t.exact(
+ t.partial({
+ buttonLabel: t.string,
+ })
+);
+const required = t.exact(
+ t.type({
+ type: t.string,
+ message: t.string,
+ actionPath: t.string,
+ })
+);
+
+export const warningSchema = t.intersection([partial, required]);
+export type WarningSchema = t.TypeOf;
diff --git a/x-pack/plugins/security_solution/cypress/objects/rule.ts b/x-pack/plugins/security_solution/cypress/objects/rule.ts
index 20f41f938502b..a80c7099c31ff 100644
--- a/x-pack/plugins/security_solution/cypress/objects/rule.ts
+++ b/x-pack/plugins/security_solution/cypress/objects/rule.ts
@@ -573,6 +573,11 @@ export const expectedExportedRule = (ruleResponse: Cypress.Response {
.pipe(($el) => $el.trigger('click'))
.should('be.checked');
};
-
+const selectOverwriteConnectorsRulesImport = () => {
+ cy.get(RULE_IMPORT_OVERWRITE_CONNECTORS_CHECKBOX)
+ .pipe(($el) => $el.trigger('click'))
+ .should('be.checked');
+};
export const importRulesWithOverwriteAll = (rulesFile: string) => {
cy.get(RULE_IMPORT_MODAL).click();
cy.get(INPUT_FILE).should('exist');
cy.get(INPUT_FILE).trigger('click', { force: true }).attachFile(rulesFile).trigger('change');
selectOverwriteRulesImport();
selectOverwriteExceptionsRulesImport();
+ selectOverwriteConnectorsRulesImport();
cy.get(RULE_IMPORT_MODAL_BUTTON).last().click({ force: true });
cy.get(INPUT_FILE).should('not.exist');
};
diff --git a/x-pack/plugins/security_solution/public/common/components/import_data_modal/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/import_data_modal/__snapshots__/index.test.tsx.snap
index 47dd897f71ffb..3378225f6b602 100644
--- a/x-pack/plugins/security_solution/public/common/components/import_data_modal/__snapshots__/index.test.tsx.snap
+++ b/x-pack/plugins/security_solution/public/common/components/import_data_modal/__snapshots__/index.test.tsx.snap
@@ -90,13 +90,521 @@ Object {
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+