-
Notifications
You must be signed in to change notification settings - Fork 8.3k
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
Conversation
# Conflicts: # x-pack/plugins/security_solution/public/management/components/endpoint_responder/lib/console_commands_definition.ts
Pinging @elastic/security-defend-workflows (Team:Defend Workflows) |
import { RESPONSE_CONSOLE_ACTION_COMMANDS_TO_REQUIRED_AUTHZ } from '../service/response_actions/constants'; | ||
import type { EndpointPrivileges } from '../types'; | ||
|
||
export const getRbacControl = ({ |
There was a problem hiding this comment.
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 = ( |
There was a problem hiding this comment.
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:
kibana/x-pack/plugins/security_solution/common/endpoint/service/response_actions/constants.ts
Lines 87 to 98 in f9f4c1a
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 | ||
) => { |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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.
const isInvalid = !getRbacControl({ | ||
commandName: getUiCommand(action.params.command), | ||
privileges: { ...endpointAuthz, loading: false }, | ||
}); | ||
|
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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 @@ | |||
/* |
There was a problem hiding this comment.
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
if (command === 'unisolate') { | ||
return 'release'; | ||
} else if (command === 'running-processes') { | ||
return 'processes'; | ||
} else { | ||
return command; | ||
} | ||
}; |
There was a problem hiding this comment.
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
Thank you @paul-tavares, indeed the constants that you created recently made all my changes to |
There was a problem hiding this 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.
There was a problem hiding this 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>( |
There was a problem hiding this comment.
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
😅
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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)), |
There was a problem hiding this comment.
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 😅
There was a problem hiding this comment.
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.
…ndpoint-rule-validation
Thanks @maximpn, appreciate the feedback 👍 I've applied your comments, could you take another look please? :) |
@@ -384,3 +385,17 @@ export const convertAlertSuppressionToSnake = ( | |||
missing_fields_strategy: input.missingFieldsStrategy, | |||
} | |||
: undefined; | |||
|
|||
export const findDifferenceInArrays = <T1, T2>( |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 :)
There was a problem hiding this 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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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!
💚 Build Succeeded
Metrics [docs]Unknown metric groupsESLint disabled line counts
Total ESLint disabled count
History
To update your PR or re-run it, just comment with: cc @tomsonpl |
This PR adds a validation on rule creation/update checking if user has permissions to change response_actions.
AC:
endpoint
response_actions without specific permission per command, eg. if theisolate
command is changed, we validate forcanIsolateHost
endpointResponseActionsEnabled
flag is set to Trueresponse_actions
isolate
isolate
command provided (schema requirement)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!