Skip to content

Commit

Permalink
[Security Solution][Platform] - Add connectors to import/export API (#…
Browse files Browse the repository at this point in the history
…148703)

## Summary

- [x] Addresses #118774 
- [x] Enable Security Rule to be **imported** even if one of its
connectors has a missing secret
- [x] Shows **Warning Callout** in the Import Modal when missing secrets
connector is imported.
- [x] Added Link `connectors` to the connectors page in the same tab, so
that the user can fix imported connectors.
- [x] Added `Overwrite existing connectors with conflicting action "id"`
option to the Import Modal

## Cases:

> **Export:**
> - Export Rule(s) with connectors through `Export All` or `Bulk
Actions`
> 
> **Import:**
>     - Import Rule with correct connectors data 
> - Import Rule with missing secrets' connectors by showing a warning
callout
> - Re-Import connectors even if they were stored when overwrite is true
> 
> **Error:**
> - Showing an error message when the user has a Read Actions permission
and stops the importing => ` You may not have actions privileges
required to import rules with actions ...`
> - Showing an error message when the user has an old imported rule
missing all the connectors data **OR** these connectors were not in the
user's env => `X connector is missing. Connector id missing is: X`
> - Showing an error if the new connectors defined in the exported file
are not corresponding to the actions array under the rules param => `X
connector is missing. Connector id missing is: X`
> - **Showing a ` conflict` error in case of existing connectors and
re-importing again with an `overwrite` false => this won't happen in
case of implementing the `Skipping action-connectors importing if all
connectors have been imported/created before`**
> 
> **Skip importing:**  
> - Skipping action-connectors importing if the `actions` array is
empty, even if the user has exported-connectors in the file
> - Skipping action-connectors importing if all connectors have been
imported/created before
> 



### Screenshots
> 
>  **1. Importing Connectors successfully**
> <img width="1219" alt="image"
src="https://user-images.githubusercontent.com/12671903/216049657-a313033b-e45e-4c99-b6ca-ed3070f15a97.png">
> 
>  **2. Importing Connectors with warnings**
<img width="1208" alt="image"
src="https://user-images.githubusercontent.com/12671903/216980057-b5cdfe38-da1b-479b-8cfd-81f16037ff1d.png">

**3.Connector Page**

<img width="1701" alt="image"
src="https://user-images.githubusercontent.com/12671903/216049911-da29abc8-e20c-49d2-a507-ab382372b4f6.png">


## New text: @nastasha-solomon

**1. Warning message**

 title => could be ` 1 connector imported` or `x connectors imported`
message => ` 1 connector has sensitive information that requires
updates. review in connectors` or `x connectors have sensitive
information that requires updates. review in connectors`

<img width="588" alt="image"
src="https://user-images.githubusercontent.com/12671903/216103805-9946b080-07d3-4e8b-93aa-b5e1dcaa415d.png">

**2. New `Overwrite` checkbox**
<img width="431" alt="image"
src="https://user-images.githubusercontent.com/12671903/216106354-3d435d64-0fa5-467b-90f1-effb2c0aef2a.png">


**3. Success Toast message**

<img width="434" alt="image"
src="https://user-images.githubusercontent.com/12671903/216104454-2d83744b-efbc-40c1-9e69-7e8b0670dd19.png">

**4. Error messages**
   a. Missing import action privileges
<img width="438" alt="image"
src="https://user-images.githubusercontent.com/12671903/216116350-f306d744-eef4-4064-b4f8-e794db4ad78e.png">

   b. Missing connectors  
<img width="353" alt="image"
src="https://user-images.githubusercontent.com/12671903/216104979-370f6826-8150-45d5-8724-6ca50f99ad71.png">
<img width="357" alt="image"
src="https://user-images.githubusercontent.com/12671903/216106067-e6132a93-d36e-4bdf-b1bf-e6ddd1cf8a4e.png">

 


- [x] References: Use **getImporter** and **getExporter** from Saved
Object [Connectors SO import/export
implementation](#98802) ,
[Kibana-Core
confirmation](https://elastic.slack.com/archives/C5TQ33ND8/p1673275186013589
)
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

---------

Co-authored-by: kibanamachine <[email protected]>
  • Loading branch information
WafaaNasr and kibanamachine authored Feb 6, 2023
1 parent 45e5776 commit 8733774
Show file tree
Hide file tree
Showing 56 changed files with 4,475 additions and 182 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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<ImportQuerySchema, 'overwrite_action_connectors'> & {
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 & {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,18 @@ export const importQuerySchema = t.exact(
t.partial({
overwrite: DefaultStringBooleanFalse,
overwrite_exceptions: DefaultStringBooleanFalse,
overwrite_action_connectors: DefaultStringBooleanFalse,
as_new_list: DefaultStringBooleanFalse,
})
);

export type ImportQuerySchema = t.TypeOf<typeof importQuerySchema>;
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;
};
35 changes: 22 additions & 13 deletions x-pack/plugins/alerting/server/rules_client/lib/validate_actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ import { parseDuration } from '../../lib';
export async function validateActions(
context: RulesClientContext,
alertType: UntypedNormalizedRuleType,
data: Pick<RawRule, 'notifyWhen' | 'throttle' | 'schedule'> & { actions: NormalizedAlertAction[] }
data: Pick<RawRule, 'notifyWhen' | 'throttle' | 'schedule'> & {
actions: NormalizedAlertAction[];
},
allowMissingConnectorSecrets?: boolean
): Promise<void> {
const { actions, notifyWhen, throttle } = data;
const hasRuleLevelNotifyWhen = typeof notifyWhen !== 'undefined';
Expand All @@ -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);
Expand Down
8 changes: 5 additions & 3 deletions x-pack/plugins/alerting/server/rules_client/methods/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,12 @@ export interface CreateOptions<Params extends RuleTypeParams> {
| 'nextRun'
> & { actions: NormalizedAlertAction[] };
options?: SavedObjectOptions;
allowMissingConnectorSecrets?: boolean;
}

export async function create<Params extends RuleTypeParams = never>(
context: RulesClientContext,
{ data, options }: CreateOptions<Params>
{ data, options, allowMissingConnectorSecrets }: CreateOptions<Params>
): Promise<SanitizedRule<Params>> {
const id = options?.id || SavedObjectsUtils.generateId();

Expand Down Expand Up @@ -104,10 +105,11 @@ export async function create<Params extends RuleTypeParams = never>(
}
}

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 (
Expand Down
17 changes: 11 additions & 6 deletions x-pack/plugins/alerting/server/rules_client/methods/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,22 +38,23 @@ export interface UpdateOptions<Params extends RuleTypeParams> {
throttle?: string | null;
notifyWhen?: RuleNotifyWhenType | null;
};
allowMissingConnectorSecrets?: boolean;
}

export async function update<Params extends RuleTypeParams = never>(
context: RulesClientContext,
{ id, data }: UpdateOptions<Params>
{ id, data, allowMissingConnectorSecrets }: UpdateOptions<Params>
): Promise<PartialRule<Params>> {
return await retryIfConflicts(
context.logger,
`rulesClient.update('${id}')`,
async () => await updateWithOCC<Params>(context, { id, data })
async () => await updateWithOCC<Params>(context, { id, data, allowMissingConnectorSecrets })
);
}

async function updateWithOCC<Params extends RuleTypeParams>(
context: RulesClientContext,
{ id, data }: UpdateOptions<Params>
{ id, data, allowMissingConnectorSecrets }: UpdateOptions<Params>
): Promise<PartialRule<Params>> {
let alertSavedObject: SavedObject<RawRule>;

Expand Down Expand Up @@ -99,7 +100,11 @@ async function updateWithOCC<Params extends RuleTypeParams>(

context.ruleTypeRegistry.ensureRuleTypeEnabled(alertSavedObject.attributes.alertTypeId);

const updateResult = await updateAlert<Params>(context, { id, data }, alertSavedObject);
const updateResult = await updateAlert<Params>(
context,
{ id, data, allowMissingConnectorSecrets },
alertSavedObject
);

await Promise.all([
alertSavedObject.attributes.apiKey
Expand Down Expand Up @@ -138,7 +143,7 @@ async function updateWithOCC<Params extends RuleTypeParams>(

async function updateAlert<Params extends RuleTypeParams>(
context: RulesClientContext,
{ id, data }: UpdateOptions<Params>,
{ id, data, allowMissingConnectorSecrets }: UpdateOptions<Params>,
{ attributes, version }: SavedObject<RawRule>
): Promise<PartialRule<Params>> {
const ruleType = context.ruleTypeRegistry.get(attributes.alertTypeId);
Expand All @@ -156,7 +161,7 @@ async function updateAlert<Params extends RuleTypeParams>(

// 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);
Expand Down
116 changes: 116 additions & 0 deletions x-pack/plugins/alerting/server/rules_client/tests/create.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
`);
});
});
Loading

0 comments on commit 8733774

Please sign in to comment.