[Defend workflows] Fix action status on action list (#160758)

This commit is contained in:
Tomasz Ciecierski 2023-06-29 14:46:57 +02:00 committed by GitHub
parent 065ded6ff7
commit a43ee939b5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 161 additions and 39 deletions

View file

@ -30,7 +30,7 @@ export class FleetActionGenerator extends BaseDataGenerator {
expiration: this.randomFutureDate(timeStamp),
type: 'INPUT_ACTION',
input_type: 'endpoint',
agents: [this.seededUUIDv4()],
agents: overrides.agents ? overrides.agents : [this.seededUUIDv4()],
user_id: 'elastic',
data: {
command: this.randomResponseActionCommand(),

View file

@ -212,11 +212,12 @@ const getResponseActionListTableColumns = ({
},
},
{
field: 'status',
name: TABLE_COLUMN_NAMES.status,
width: !showHostNames ? '15%' : '10%',
render: (action: ActionListApiResponse['data'][number]) => {
const _status = action.errors?.length ? 'failed' : action.status;
render: (_status: ActionListApiResponse['data'][number]['status']) => {
const status = getActionStatus(_status);
return (
<EuiToolTip content={status} anchorClassName="eui-textTruncate">
<StatusBadge

View file

@ -118,7 +118,7 @@ export const getActionDetailsById = async (
});
const { isCompleted, completedAt, wasSuccessful, errors, outputs, agentState } =
getActionCompletionInfo(normalizedActionRequest.agents, actionResponses);
getActionCompletionInfo(normalizedActionRequest, actionResponses);
const { isExpired, status } = getActionStatus({
expirationDate: normalizedActionRequest.expiration,

View file

@ -276,7 +276,7 @@ const getActionDetailsList = async ({
// find the specific response's details using that set of matching responses
const { isCompleted, completedAt, wasSuccessful, errors, agentState, outputs } =
getActionCompletionInfo(action.agents, matchedResponses);
getActionCompletionInfo(action, matchedResponses);
const { isExpired, status } = getActionStatus({
expirationDate: action.expiration,

View file

@ -123,17 +123,42 @@ describe('When using Actions service utilities', () => {
});
it('should show complete `false` if no action ids', () => {
expect(getActionCompletionInfo([], [])).toEqual({ ...NOT_COMPLETED_OUTPUT, agentState: {} });
expect(
getActionCompletionInfo(
mapToNormalizedActionRequest(
fleetActionGenerator.generate({
agents: [],
})
),
[]
)
).toEqual({
...NOT_COMPLETED_OUTPUT,
agentState: {},
});
});
it('should show complete as `false` if no responses', () => {
expect(getActionCompletionInfo(['123'], [])).toEqual(NOT_COMPLETED_OUTPUT);
expect(
getActionCompletionInfo(
mapToNormalizedActionRequest(
fleetActionGenerator.generate({
agents: ['123'],
})
),
[]
)
).toEqual(NOT_COMPLETED_OUTPUT);
});
it('should show complete as `false` if no Endpoint response', () => {
expect(
getActionCompletionInfo(
['123'],
mapToNormalizedActionRequest(
fleetActionGenerator.generate({
agents: ['123'],
})
),
[
fleetActionGenerator.generateActivityLogActionResponse({
item: { data: { action_id: '123' } },
@ -156,7 +181,16 @@ describe('When using Actions service utilities', () => {
},
},
});
expect(getActionCompletionInfo(['123'], [endpointResponse])).toEqual({
expect(
getActionCompletionInfo(
mapToNormalizedActionRequest(
fleetActionGenerator.generate({
agents: ['123'],
})
),
[endpointResponse]
)
).toEqual({
isCompleted: true,
completedAt: COMPLETED_AT,
errors: undefined,
@ -201,7 +235,16 @@ describe('When using Actions service utilities', () => {
},
},
});
expect(getActionCompletionInfo(['123'], [endpointResponse])).toEqual({
expect(
getActionCompletionInfo(
mapToNormalizedActionRequest(
fleetActionGenerator.generate({
agents: ['123'],
})
),
[endpointResponse]
)
).toEqual({
isCompleted: true,
completedAt: COMPLETED_AT,
errors: undefined,
@ -255,7 +298,16 @@ describe('When using Actions service utilities', () => {
});
it('should show `wasSuccessful` as `false` if endpoint action response has error', () => {
expect(getActionCompletionInfo(['123'], [endpointResponseAtError])).toEqual({
expect(
getActionCompletionInfo(
mapToNormalizedActionRequest(
fleetActionGenerator.generate({
agents: ['123'],
})
),
[endpointResponseAtError]
)
).toEqual({
completedAt: endpointResponseAtError.item.data['@timestamp'],
errors: ['Endpoint action response error: endpoint failed to apply'],
isCompleted: true,
@ -273,7 +325,16 @@ describe('When using Actions service utilities', () => {
});
it('should show `wasSuccessful` as `false` if fleet action response has error (no endpoint response)', () => {
expect(getActionCompletionInfo(['123'], [fleetResponseAtError])).toEqual({
expect(
getActionCompletionInfo(
mapToNormalizedActionRequest(
fleetActionGenerator.generate({
agents: ['123'],
})
),
[fleetResponseAtError]
)
).toEqual({
completedAt: fleetResponseAtError.item.data.completed_at,
errors: ['Fleet action response error: agent failed to deliver'],
isCompleted: true,
@ -292,7 +353,14 @@ describe('When using Actions service utilities', () => {
it('should include both fleet and endpoint errors if both responses returned failure', () => {
expect(
getActionCompletionInfo(['123'], [fleetResponseAtError, endpointResponseAtError])
getActionCompletionInfo(
mapToNormalizedActionRequest(
fleetActionGenerator.generate({
agents: ['123'],
})
),
[fleetResponseAtError, endpointResponseAtError]
)
).toEqual({
completedAt: endpointResponseAtError.item.data['@timestamp'],
errors: [
@ -374,7 +442,16 @@ describe('When using Actions service utilities', () => {
});
it('should show complete as `false` if no responses', () => {
expect(getActionCompletionInfo(agentIds, [])).toEqual({
expect(
getActionCompletionInfo(
mapToNormalizedActionRequest(
fleetActionGenerator.generate({
agents: agentIds,
})
),
[]
)
).toEqual({
...NOT_COMPLETED_OUTPUT,
agentState: {
...NOT_COMPLETED_OUTPUT.agentState,
@ -396,14 +473,21 @@ describe('When using Actions service utilities', () => {
it('should complete as `false` if at least one agent id has not received a response', () => {
expect(
getActionCompletionInfo(agentIds, [
...action123Responses,
getActionCompletionInfo(
mapToNormalizedActionRequest(
fleetActionGenerator.generate({
agents: agentIds,
})
),
[
...action123Responses,
// Action id: 456 === Not complete (only fleet response)
action456Responses[0],
// Action id: 456 === Not complete (only fleet response)
action456Responses[0],
...action789Responses,
])
...action789Responses,
]
)
).toEqual({
...NOT_COMPLETED_OUTPUT,
outputs: expect.any(Object),
@ -432,11 +516,14 @@ describe('When using Actions service utilities', () => {
it('should show complete as `true` if all agent response were received', () => {
expect(
getActionCompletionInfo(agentIds, [
...action123Responses,
...action456Responses,
...action789Responses,
])
getActionCompletionInfo(
mapToNormalizedActionRequest(
fleetActionGenerator.generate({
agents: agentIds,
})
),
[...action123Responses, ...action456Responses, ...action789Responses]
)
).toEqual({
isCompleted: true,
completedAt: COMPLETED_AT,
@ -471,14 +558,21 @@ describe('When using Actions service utilities', () => {
action456Responses[0].item.data['@timestamp'] = '2022-05-06T12:50:19.747Z';
expect(
getActionCompletionInfo(agentIds, [
...action123Responses,
getActionCompletionInfo(
mapToNormalizedActionRequest(
fleetActionGenerator.generate({
agents: agentIds,
})
),
[
...action123Responses,
// Action id: 456 === is complete with only a fleet response that has `error`
action456Responses[0],
// Action id: 456 === is complete with only a fleet response that has `error`
action456Responses[0],
...action789Responses,
])
...action789Responses,
]
)
).toEqual({
completedAt: '2022-05-06T12:50:19.747Z',
errors: ['Fleet action response error: something is no good'],
@ -528,14 +622,21 @@ describe('When using Actions service utilities', () => {
};
expect(
getActionCompletionInfo(agentIds, [
...action123Responses,
getActionCompletionInfo(
mapToNormalizedActionRequest(
fleetActionGenerator.generate({
agents: agentIds,
})
),
[
...action123Responses,
// Action id: 456 === Not complete (only fleet response)
action456Responses[0],
// Action id: 456 === Not complete (only fleet response)
action456Responses[0],
...action789Responses,
])
...action789Responses,
]
)
).toEqual({
...NOT_COMPLETED_OUTPUT,
agentState: {

View file

@ -115,11 +115,12 @@ type ActionCompletionInfo = Pick<
>;
export const getActionCompletionInfo = (
/** List of agents that the action was sent to */
agentIds: string[],
/** The normalized action request */
action: NormalizedActionRequest,
/** List of action Log responses received for the action */
actionResponses: Array<ActivityLogActionResponse | EndpointActivityLogActionResponse>
): ActionCompletionInfo => {
const agentIds = action.agents;
const completedInfo: ActionCompletionInfo = {
completedAt: undefined,
errors: undefined,
@ -191,6 +192,25 @@ export const getActionCompletionInfo = (
}
}
// If the action request has an Error, then we'll never get actual response from all of the agents
// to which this action sent. In this case, we adjust the completion information to all be "complete"
// and un-successful
if (action.error?.message) {
const errorMessage = action.error.message;
completedInfo.completedAt = action.createdAt;
completedInfo.isCompleted = true;
completedInfo.wasSuccessful = false;
completedInfo.errors = [errorMessage];
Object.values(completedInfo.agentState).forEach((agentState) => {
agentState.completedAt = action.createdAt;
agentState.isCompleted = true;
agentState.wasSuccessful = false;
agentState.errors = [errorMessage];
});
}
return completedInfo;
};