Skip to content

Commit adfc68f

Browse files
authored
[9.1] [Security Solution][Endpoint] Fix automated response actions when spaces is enabled and action can not be sent (#226043) (#226436)
# Backport This will backport the following commits from `main` to `9.1`: - [[Security Solution][Endpoint] Fix automated response actions when spaces is enabled and action can not be sent (#226043)](#226043) <!--- Backport version: 10.0.1 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sorenlouv/backport) <!--BACKPORT [{"author":{"name":"Paul Tavares","email":"[email protected]"},"sourceCommit":{"committedDate":"2025-07-03T13:37:09Z","message":"[Security Solution][Endpoint] Fix automated response actions when spaces is enabled and action can not be sent (#226043)\n\n## Summary\n\nPR fixes the following issue when spaces is enabled:\n\n- When automated response actions are processed, if unable to determine\nthe policy associated with the agent the action is being sent to, we\nshould still create the failed action request (as we did before spaces\nwas enabled)\n- Although this fix creates the action request in failed state, it will\nnot be visible in the UI - both under the alert that triggered it and\nthe action history log - because we are not able to determine access to\nthe action due to not having policy information for the agent.\n- Action failure will tagged as \"orphan\" and thus it will be displayed\nin the action history log in the space that is configured to show orphan\nactions","sha":"765376745e4988519b4ce2e1ef7d1a54c6bcd078","branchLabelMapping":{"^v9.2.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","Team:Defend Workflows","backport:version","v9.1.0","v9.2.0"],"title":"[Security Solution][Endpoint] Fix automated response actions when spaces is enabled and action can not be sent","number":226043,"url":"https://github.com/elastic/kibana/pull/226043","mergeCommit":{"message":"[Security Solution][Endpoint] Fix automated response actions when spaces is enabled and action can not be sent (#226043)\n\n## Summary\n\nPR fixes the following issue when spaces is enabled:\n\n- When automated response actions are processed, if unable to determine\nthe policy associated with the agent the action is being sent to, we\nshould still create the failed action request (as we did before spaces\nwas enabled)\n- Although this fix creates the action request in failed state, it will\nnot be visible in the UI - both under the alert that triggered it and\nthe action history log - because we are not able to determine access to\nthe action due to not having policy information for the agent.\n- Action failure will tagged as \"orphan\" and thus it will be displayed\nin the action history log in the space that is configured to show orphan\nactions","sha":"765376745e4988519b4ce2e1ef7d1a54c6bcd078"}},"sourceBranch":"main","suggestedTargetBranches":["9.1"],"targetPullRequestStates":[{"branch":"9.1","label":"v9.1.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v9.2.0","branchLabelMappingKey":"^v9.2.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/226043","number":226043,"mergeCommit":{"message":"[Security Solution][Endpoint] Fix automated response actions when spaces is enabled and action can not be sent (#226043)\n\n## Summary\n\nPR fixes the following issue when spaces is enabled:\n\n- When automated response actions are processed, if unable to determine\nthe policy associated with the agent the action is being sent to, we\nshould still create the failed action request (as we did before spaces\nwas enabled)\n- Although this fix creates the action request in failed state, it will\nnot be visible in the UI - both under the alert that triggered it and\nthe action history log - because we are not able to determine access to\nthe action due to not having policy information for the agent.\n- Action failure will tagged as \"orphan\" and thus it will be displayed\nin the action history log in the space that is configured to show orphan\nactions","sha":"765376745e4988519b4ce2e1ef7d1a54c6bcd078"}}]}] BACKPORT-->
1 parent e994f66 commit adfc68f

File tree

8 files changed

+128
-19
lines changed

8 files changed

+128
-19
lines changed

x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/action_details_by_id.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,4 +182,19 @@ describe('When using `getActionDetailsById()', () => {
182182
})
183183
);
184184
});
185+
186+
it('should not validate against spaces when `bypassSpaceValidation` is `true`', async () => {
187+
// @ts-expect-error
188+
endpointAppContextService.experimentalFeatures.endpointManagementSpaceAwarenessEnabled = true;
189+
(
190+
endpointAppContextService.getInternalFleetServices().ensureInCurrentSpace as jest.Mock
191+
).mockResolvedValue(undefined);
192+
await getActionDetailsById(endpointAppContextService, 'default', '123', {
193+
bypassSpaceValidation: true,
194+
});
195+
196+
expect(
197+
endpointAppContextService.getInternalFleetServices().ensureInCurrentSpace
198+
).not.toHaveBeenCalled();
199+
});
185200
});

x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/action_details_by_id.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,16 @@ import { NotFoundError } from '../../errors';
2727
export const getActionDetailsById = async <T extends ActionDetails = ActionDetails>(
2828
endpointService: EndpointAppContextService,
2929
spaceId: string,
30-
actionId: string
30+
actionId: string,
31+
{
32+
bypassSpaceValidation = false,
33+
}: Partial<{
34+
/**
35+
* if `true`, then no space validations will be done on the action retrieved. Default is `false`.
36+
* USE IT CAREFULLY!
37+
*/
38+
bypassSpaceValidation: boolean;
39+
}> = {}
3140
): Promise<T> => {
3241
let normalizedActionRequest: ReturnType<typeof mapToNormalizedActionRequest> | undefined;
3342
let actionResponses: FetchActionResponsesResult;
@@ -36,7 +45,7 @@ export const getActionDetailsById = async <T extends ActionDetails = ActionDetai
3645
// Get both the Action Request(s) and action Response(s)
3746
const [actionRequestEsDoc, actionResponseResult] = await Promise.all([
3847
// Get the action request(s)
39-
fetchActionRequestById(endpointService, spaceId, actionId),
48+
fetchActionRequestById(endpointService, spaceId, actionId, { bypassSpaceValidation }),
4049

4150
// Get all responses
4251
fetchActionResponses({

x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/endpoint/endpoint_actions_client.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { Readable } from 'stream';
2121
import { EndpointActionGenerator } from '../../../../../../common/endpoint/data_generators/endpoint_action_generator';
2222
import type { ResponseActionsRequestBody } from '../../../../../../common/api/endpoint';
2323
import { AgentNotFoundError } from '@kbn/fleet-plugin/server';
24+
import { ALLOWED_ACTION_REQUEST_TAGS } from '../../constants';
2425

2526
jest.mock('../../action_details_by_id', () => {
2627
const originalMod = jest.requireActual('../../action_details_by_id');
@@ -631,5 +632,52 @@ describe('EndpointActionsClient', () => {
631632
).not.toHaveBeenCalled();
632633
}
633634
);
635+
636+
it('should create failed action request for automated response actions', async () => {
637+
classConstructorOptions.isAutomated = true;
638+
// @ts-expect-error mocking this for testing purposes
639+
endpointActionsClient.checkAgentIds = jest.fn().mockResolvedValueOnce({
640+
isValid: false,
641+
valid: [],
642+
invalid: ['invalid-id'],
643+
hosts: [{ agent: { id: 'invalid-id', name: '' }, host: { hostname: '' } }],
644+
});
645+
646+
await endpointActionsClient.isolate(
647+
responseActionsClientMock.createIsolateOptions(getCommonResponseActionOptions())
648+
);
649+
650+
expect(classConstructorOptions.esClient.index).toHaveBeenCalledWith(
651+
expect.objectContaining({
652+
document: expect.objectContaining({
653+
agent: { id: [], policy: [] },
654+
tags: [ALLOWED_ACTION_REQUEST_TAGS.integrationPolicyDeleted],
655+
}),
656+
}),
657+
expect.anything()
658+
);
659+
});
660+
661+
it('should return action details for failed automated response actions even when no valid agents', async () => {
662+
classConstructorOptions.isAutomated = true;
663+
// @ts-expect-error mocking this for testing purposes
664+
endpointActionsClient.checkAgentIds = jest.fn().mockResolvedValueOnce({
665+
isValid: false,
666+
valid: [],
667+
invalid: ['invalid-id'],
668+
hosts: [{ agent: { id: 'invalid-id', name: '' }, host: { hostname: '' } }],
669+
});
670+
671+
await endpointActionsClient.isolate(
672+
responseActionsClientMock.createIsolateOptions(getCommonResponseActionOptions())
673+
);
674+
675+
expect(getActionDetailsByIdMock).toHaveBeenCalledWith(
676+
expect.anything(),
677+
expect.anything(),
678+
expect.anything(),
679+
{ bypassSpaceValidation: true }
680+
);
681+
});
634682
});
635683
});

x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/endpoint/endpoint_actions_client.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,10 @@ export class EndpointActionsClient extends ResponseActionsClientImpl {
195195
}),
196196
});
197197

198-
return this.fetchActionDetails<TResponse>(actionId);
198+
// We bypass space validation when retrieving the action details to ensure that if a failed
199+
// action was created, and it did not contain the agent policy information (and space is enabled)
200+
// we don't trigger an error.
201+
return this.fetchActionDetails<TResponse>(actionId, true);
199202
}
200203

201204
private async dispatchActionViaFleet({

x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/lib/base_response_actions_client.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,8 @@ describe('ResponseActionsClientImpl base class', () => {
293293
expect(getActionDetailsByIdMock).toHaveBeenCalledWith(
294294
expect.anything(),
295295
expect.anything(),
296-
'one'
296+
'one',
297+
{ bypassSpaceValidation: false }
297298
);
298299
});
299300
});

x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/lib/base_response_actions_client.ts

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -453,12 +453,20 @@ export abstract class ResponseActionsClientImpl implements ResponseActionsClient
453453
/**
454454
* Returns the action details for a given response action id
455455
* @param actionId
456+
* @param bypassSpaceValidation
456457
* @protected
457458
*/
458459
protected async fetchActionDetails<T extends ActionDetails = ActionDetails>(
459-
actionId: string
460+
actionId: string,
461+
/**
462+
* if `true`, then no space validations will be done on the action retrieved. Default is `false`.
463+
* USE IT CAREFULLY!
464+
*/
465+
bypassSpaceValidation: boolean = false
460466
): Promise<T> {
461-
return getActionDetailsById(this.options.endpointService, this.options.spaceId, actionId);
467+
return getActionDetailsById(this.options.endpointService, this.options.spaceId, actionId, {
468+
bypassSpaceValidation,
469+
});
462470
}
463471

464472
/**
@@ -612,18 +620,19 @@ export abstract class ResponseActionsClientImpl implements ResponseActionsClient
612620

613621
this.notifyUsage(actionRequest.command);
614622

615-
// It's possible with Automated Response actions that we could reach this point with
616-
// no endpoint IDs in the action request - case where they are no longer enrolled.
617-
// In these cases, we don't attempt to build the agent policy info and instead add
618-
// the `integration deleted` tag to the action request, which means these are only
619-
// visible in the space configured (via ref. data) show orphaned actions
620-
const agentPolicyInfo: LogsEndpointAction['agent']['policy'] =
621-
isSpacesEnabled && actionRequest.endpoint_ids.length > 0
623+
const actionId = actionRequest.actionId || uuidv4();
624+
const tags = actionRequest.tags ?? [];
625+
626+
// With automated response action, it's possible to reach this point and not have any `endpoint_ids`
627+
// defined in the action. That's because with automated response actions we always create an
628+
// action request, even when there is a failure - like if the agent was un-enrolled in between
629+
// the event sent and the detection engine processing that event.
630+
const agentPolicyInfo =
631+
isSpacesEnabled && actionRequest.endpoint_ids.length
622632
? await this.fetchAgentPolicyInfo(actionRequest.endpoint_ids)
623633
: [];
624-
const tags: LogsEndpointAction['tags'] = actionRequest.tags ?? [];
625634

626-
if (agentPolicyInfo.length === 0) {
635+
if (isSpacesEnabled && agentPolicyInfo.length === 0) {
627636
tags.push(ALLOWED_ACTION_REQUEST_TAGS.integrationPolicyDeleted);
628637
}
629638

@@ -645,7 +654,7 @@ export abstract class ResponseActionsClientImpl implements ResponseActionsClient
645654
...(isSpacesEnabled ? { policy: agentPolicyInfo } : {}),
646655
},
647656
EndpointActions: {
648-
action_id: actionRequest.actionId || uuidv4(),
657+
action_id: actionId,
649658
expiration: getActionRequestExpiration(),
650659
type: 'INPUT_ACTION',
651660
input_type: this.agentType,

x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/utils/fetch_action_request_by_id.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,5 +124,18 @@ describe('fetchActionRequestById() utility', () => {
124124
'Action [123] not found'
125125
);
126126
});
127+
128+
it('should not validate action against spaces if `bypassSpaceValidation` is true', async () => {
129+
(
130+
endpointServiceMock.getInternalFleetServices().ensureInCurrentSpace as jest.Mock
131+
).mockResolvedValue(undefined);
132+
await fetchActionRequestById(endpointServiceMock, 'default', '123', {
133+
bypassSpaceValidation: true,
134+
});
135+
136+
expect(
137+
endpointServiceMock.getInternalFleetServices().ensureInCurrentSpace as jest.Mock
138+
).not.toHaveBeenCalled();
139+
});
127140
});
128141
});

x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/utils/fetch_action_request_by_id.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import { REF_DATA_KEYS } from '../../../lib/reference_data';
2525
* @param endpointService
2626
* @param spaceId
2727
* @param actionId
28-
*
2928
* @throws
3029
*/
3130
export const fetchActionRequestById = async <
@@ -35,7 +34,16 @@ export const fetchActionRequestById = async <
3534
>(
3635
endpointService: EndpointAppContextService,
3736
spaceId: string,
38-
actionId: string
37+
actionId: string,
38+
{
39+
bypassSpaceValidation = false,
40+
}: Partial<{
41+
/**
42+
* if `true`, then no space validations will be done on the action retrieved. Default is `false`.
43+
* USE IT CAREFULLY!
44+
*/
45+
bypassSpaceValidation: boolean;
46+
}> = {}
3947
): Promise<LogsEndpointAction<TParameters, TOutputContent, TMeta>> => {
4048
const logger = endpointService.createLogger('fetchActionRequestById');
4149
const searchResponse = await endpointService
@@ -54,7 +62,10 @@ export const fetchActionRequestById = async <
5462

5563
if (!actionRequest) {
5664
throw new NotFoundError(`Action with id '${actionId}' not found.`);
57-
} else if (endpointService.experimentalFeatures.endpointManagementSpaceAwarenessEnabled) {
65+
} else if (
66+
endpointService.experimentalFeatures.endpointManagementSpaceAwarenessEnabled &&
67+
!bypassSpaceValidation
68+
) {
5869
if (!actionRequest.agent.policy || actionRequest.agent.policy.length === 0) {
5970
const message = `Response action [${actionId}] missing 'agent.policy' information - unable to determine if response action is accessible for space [${spaceId}]`;
6071

0 commit comments

Comments
 (0)