Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[Defend workflows] Validate Isolate RBAC on rule creation/update #157401

Merged
merged 22 commits into from
May 16, 2023

Conversation

tomsonpl
Copy link
Contributor

@tomsonpl tomsonpl commented May 11, 2023

This PR adds a validation on rule creation/update checking if user has permissions to change response_actions.

AC:

  • throws 401 error when user tries to add/change/delete any of the endpoint response_actions without specific permission per command, eg. if the isolate command is changed, we validate for canIsolateHost
  • validation happens only for queryTypeRule
  • validation happens only if endpointResponseActionsEnabled flag is set to True
  • validation happens only if either existing rule or the update rule contains response_actions
  • should be ready to validate more commands than just the current isolate
  • throws error when there is a different than isolate command provided (schema requirement)
  • basic unit tests provided

Rules Team: would you consider adding this validation to rule create/update flow? I was trying to make sure that this is as least problematic as possible, but would love to use a better approach if you have any suggestions.

Thanks!

@tomsonpl tomsonpl added release_note:skip Skip the PR/issue when compiling release notes Team:Defend Workflows “EDR Workflows” sub-team of Security Solution v8.9.0 labels May 11, 2023
@tomsonpl tomsonpl self-assigned this May 11, 2023
tomsonpl added 4 commits May 11, 2023 21:21
# Conflicts:
#	x-pack/plugins/security_solution/public/management/components/endpoint_responder/lib/console_commands_definition.ts
@tomsonpl tomsonpl marked this pull request as ready for review May 12, 2023 08:18
@tomsonpl tomsonpl requested review from a team as code owners May 12, 2023 08:18
@tomsonpl tomsonpl requested review from pzl and parkiino May 12, 2023 08:18
@elasticmachine
Copy link
Contributor

Pinging @elastic/security-defend-workflows (Team:Defend Workflows)

@tomsonpl tomsonpl requested a review from maximpn May 12, 2023 08:18
@tomsonpl tomsonpl requested a review from a team as a code owner May 12, 2023 11:26
@tomsonpl tomsonpl requested review from paul-tavares and removed request for parkiino May 12, 2023 11:32
@tomsonpl tomsonpl removed the request for review from a team May 12, 2023 12:02
import { RESPONSE_CONSOLE_ACTION_COMMANDS_TO_REQUIRED_AUTHZ } from '../service/response_actions/constants';
import type { EndpointPrivileges } from '../types';

export const getRbacControl = ({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a UI specific construct. EndpointPrivileges has some additional ui specific property (like loading). On the server side we use EndpointAuthz.

I would not recommend trying to make this reusable with the server side.

* unisolate -> release
* running-processes -> processes
*/
export const getUiCommand = (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have a const that does this mapping here:

export const RESPONSE_ACTION_API_COMMANDS_TO_CONSOLE_COMMAND_MAP = Object.freeze<
Record<ResponseActionsApiCommandNames, ConsoleResponseActionCommands>
>({
isolate: 'isolate',
unisolate: 'release',
execute: 'execute',
'get-file': 'get-file',
'running-processes': 'processes',
'kill-process': 'kill-process',
'suspend-process': 'suspend-process',
upload: 'upload',
});

That being said, and following from my earlier comment, I don't think you need this for the server side.

securitySolution: SecuritySolutionApiRequestHandlerContext,
ruleUpdate: RuleCreateProps | RuleUpdateProps,
existingRule?: RuleAlertType | null
) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you add a return type to this function.

);

difference.forEach((action) => {
if ('command' in action?.params) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see the API restricting this to only isolate. Did you forget to make that check here?

I know you are trying to build it in a way that it will just work with other aciton as they are introduced, but the API needs to be locked down to only the actions we support currently.

Comment on lines 95 to 99
const isInvalid = !getRbacControl({
commandName: getUiCommand(action.params.command),
privileges: { ...endpointAuthz, loading: false },
});

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would suggest not using getRbackControl() since that is working with a UI specific construct as I mentioned earlier.

If you keep this generic as is now, then I would suggest something like:

const authzPropName = RESPONSE_CONSOLE_ACTION_COMMANDS_TO_REQUIRED_AUTHZ[
  RESPONSE_ACTION_API_COMMANDS_TO_CONSOLE_COMMAND_MAP[action.params.command]
  ];

const isInvalid = endpiontAuthz[authzPropName];

You could also create a const that includes the mapping of RESPONSE_ACTION_API_COMMANDS_TO_REQUIRED_AUTHZ if that is clearer to understand.

(FYI: @ashokaditya and I have talked about refactoring all of const (there are many for actions) into a nested object that can be use by both the server and UI)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 Great hint, thank you!

@@ -0,0 +1,57 @@
/*
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you endup keeping these (see other comments below) I think they should go into common/endpoint/service/response_actions instead, since they are all actions specific

Comment on lines 33 to 40
if (command === 'unisolate') {
return 'release';
} else if (command === 'running-processes') {
return 'processes';
} else {
return command;
}
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we have a const that holds this mapping

@tomsonpl tomsonpl requested a review from a team as a code owner May 12, 2023 17:38
@tomsonpl tomsonpl requested a review from paul-tavares May 12, 2023 17:39
@tomsonpl
Copy link
Contributor Author

Thank you @paul-tavares, indeed the constants that you created recently made all my changes to management redundant 🚀 thanks!
I also added the schema restriction for isolate command as suggested :)

@tomsonpl tomsonpl removed the request for review from a team May 12, 2023 18:29
Copy link
Contributor

@paul-tavares paul-tavares left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good from an Endpoint management standpoint.

Copy link
Contributor

@maximpn maximpn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tomsonpl there are nice changes to validate permissions 👍

Rules Team: would you consider adding this validation to rule create/update flow? I was trying to make sure that this is as least problematic as possible, but would love to use a better approach if you have any suggestions.

It'd be nice to have this logic somewhere in validate section, so outside the route's business logic. Unfortunately it's impossible without refactoring and this definitely goes outside PR's scope. Having validateResponseActionsPermissions() next to the other validate functions is a good choice.

I left some comments, mostly related to the clarity of the changes.

if (payload.response_actions?.length || existingPayload?.params?.responseActions?.length) {
const endpointAuthz = await securitySolution.getEndpointAuthz();

const difference = findDifferenceInArrays<ResponseAction, RuleResponseAction>(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the point to define a wrapper function while it can be handled here right in place by

    const differences = _.differenceWith<ResponseAction, RuleResponseAction>(
      payload.response_actions,
      existingPayload?.params?.responseActions ?? [],
      _.isEqual
    );

nit: the result is rather differences than difference 😅

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, I had to support empty arrays, because it was not finding it as differences. However now the tests are still passing when I removed those So... I will have to give it another try and test it. :) Thanks!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I tested and it doesn't find differences correctly when we eg. had 1 response action in existingRule, and remove it to end up with empty array. I added a test to validate that.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point! I've checked the implementation in lodash and it seems considering comparing to an empty array as there are no differences. Looks like a bug IMHO.

const payload = ruleUpdate as QueryRule;
const existingPayload = existingRule as Rule<UnifiedQueryRuleParams>;

if (payload.response_actions?.length || existingPayload?.params?.responseActions?.length) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a good candidate for a type guard as ruleUpdate as QueryRule makes it harder to understand what's going on below as payload.response_actions isn't optional in the TS type.

Something like the following type guard should work for QueryRule

function isQueryRulePayload(rule: SharedResponseProps): rule is QueryRule {
  return 'response_actions' in rule;
}

And the same is applicable to existingRule. So the result code could be

if (!isQueryRulePayload(payload) && (!existingPayload || !isQueryRuleObject(existingPayload)) {
  return;
}

if (payload.response_actions.length === 0 && existingPayload.params?.responseActions.length === 0) {
  return;
}

);

difference.forEach((action) => {
if ('command' in action?.params) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: early exit would be nice here as well

@@ -180,4 +183,69 @@ describe('Update rule route', () => {
expect(result.badRequest).toHaveBeenCalledWith('Failed to parse "from" on rule param');
});
});
describe('rule containing response actions', () => {
beforeEach(() => {
// @ts-expect-error
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A comment here why @ts-expect-error is needed would be nice.

@@ -176,4 +176,69 @@ describe('Create rule route', () => {
expect(result.badRequest).toHaveBeenCalledWith('Failed to parse "from" on rule param');
});
});
describe('rule containing response actions', () => {
beforeEach(() => {
// @ts-expect-error
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A comment here why @ts-expect-error is needed would be nice.


// to enable using RESPONSE_ACTION_API_COMMANDS_NAMES as a type
function keyObject<T extends readonly string[]>(arr: T): { [K in T[number]]: null } {
return Object.fromEntries(arr.map((v) => [v, null])) as never;
}

export const EndpointParams = t.type({
command: t.keyof(keyObject(RESPONSE_ACTION_API_COMMANDS_NAMES)),
command: t.keyof(keyObject(ENABLED_AUTOMATED_RESPONSE_ACTION_COMMANDS)),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you clarify what's impact of this change? As far as I see it doesn't affect create/update rule endpoints or I just got lost in the files 😅

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey, that's a great question. What we agreed on within the team is that we want to support just isolate command so far. This change, makes sure we get a schema error when user wants to force using another command. I am saying force, because in the UI it's limited to just that one command.

@tomsonpl
Copy link
Contributor Author

Thanks @maximpn, appreciate the feedback 👍 I've applied your comments, could you take another look please? :)

@tomsonpl tomsonpl requested a review from maximpn May 16, 2023 06:24
@@ -384,3 +385,17 @@ export const convertAlertSuppressionToSnake = (
missing_fields_strategy: input.missingFieldsStrategy,
}
: undefined;

export const findDifferenceInArrays = <T1, T2>(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like you're finding elements that are not common in either array. If yes, then consider renaming this to findSymmetricDifferenceInArrays

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#157966 changed the approach in here, hope you like this one better @ashokaditya :)

@tomsonpl tomsonpl removed the request for review from pzl May 16, 2023 09:22
Copy link
Contributor

@maximpn maximpn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tomsonpl thank you for addressing all my comments 👍 There is one minor change left. Anyway it's not critical so approving in advance.

return;
}

const payload = ruleUpdate as QueryRule;
Copy link
Contributor

@maximpn maximpn May 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Type casting as QueryRule and as Rule<UnifiedQueryRuleParams isn't necessary anymore as type guards are used.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed as discussed offline, thank you!

@kibana-ci
Copy link
Collaborator

💚 Build Succeeded

Metrics [docs]

Unknown metric groups

ESLint disabled line counts

id before after diff
enterpriseSearch 19 21 +2
securitySolution 400 404 +4
total +6

Total ESLint disabled count

id before after diff
enterpriseSearch 20 22 +2
securitySolution 480 484 +4
total +6

History

To update your PR or re-run it, just comment with:
@elasticmachine merge upstream

cc @tomsonpl

@tomsonpl tomsonpl merged commit e607283 into elastic:main May 16, 2023
@kibanamachine kibanamachine added the backport:skip This commit does not require backporting label May 16, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
backport:skip This commit does not require backporting release_note:skip Skip the PR/issue when compiling release notes Team:Defend Workflows “EDR Workflows” sub-team of Security Solution v8.9.0
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants