* move activity log paging method close to call api method
refs 417d093a29
* add middleware additional activity log tests
* add a more specific server side test for activity log actions and responses
refs elastic/kibana/pull/101032
* remove obsolete server side audit log index mock method
refs elastic/kibana/pull/101032
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Ashokaditya <am.struktr@gmail.com>
This commit is contained in:
parent
a36bab718d
commit
0ff10856ac
|
@ -19,6 +19,7 @@ import {
|
|||
HostResultList,
|
||||
HostIsolationResponse,
|
||||
ISOLATION_ACTIONS,
|
||||
ActivityLog,
|
||||
} from '../../../../../common/endpoint/types';
|
||||
import { AppAction } from '../../../../common/store/actions';
|
||||
import { mockEndpointResultList } from './mock_endpoint_result_list';
|
||||
|
@ -244,6 +245,29 @@ describe('endpoint list middleware', () => {
|
|||
});
|
||||
};
|
||||
|
||||
const dispatchGetActivityLogPaging = ({ page = 1 }: { page: number }) => {
|
||||
dispatch({
|
||||
type: 'endpointDetailsActivityLogUpdatePaging',
|
||||
payload: {
|
||||
page,
|
||||
pageSize: 50,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const dispatchGetActivityLogUpdateInvalidDateRange = ({
|
||||
isInvalidDateRange = false,
|
||||
}: {
|
||||
isInvalidDateRange: boolean;
|
||||
}) => {
|
||||
dispatch({
|
||||
type: 'endpointDetailsActivityLogUpdateIsInvalidDateRange',
|
||||
payload: {
|
||||
isInvalidDateRange,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
it('should set ActivityLog state to loading', async () => {
|
||||
dispatchUserChangedUrl();
|
||||
dispatchGetActivityLogLoading();
|
||||
|
@ -284,6 +308,69 @@ describe('endpoint list middleware', () => {
|
|||
});
|
||||
expect(activityLogResponse.payload.type).toEqual('LoadedResourceState');
|
||||
});
|
||||
|
||||
it('should set ActivityLog to Failed if API call fails', async () => {
|
||||
dispatchUserChangedUrl();
|
||||
|
||||
const apiError = new Error('oh oh');
|
||||
const failedDispatched = waitForAction('endpointDetailsActivityLogChanged', {
|
||||
validate(action) {
|
||||
return isFailedResourceState(action.payload);
|
||||
},
|
||||
});
|
||||
|
||||
mockedApis.responseProvider.activityLogResponse.mockImplementation(() => {
|
||||
throw apiError;
|
||||
});
|
||||
|
||||
const failedAction = (await failedDispatched).payload as FailedResourceState<ActivityLog>;
|
||||
expect(failedAction.error).toBe(apiError);
|
||||
});
|
||||
|
||||
it('should not fetch Activity Log with invalid date ranges', async () => {
|
||||
dispatchUserChangedUrl();
|
||||
|
||||
const updateInvalidDateRangeDispatched = waitForAction(
|
||||
'endpointDetailsActivityLogUpdateIsInvalidDateRange'
|
||||
);
|
||||
dispatchGetActivityLogUpdateInvalidDateRange({ isInvalidDateRange: true });
|
||||
await updateInvalidDateRangeDispatched;
|
||||
|
||||
expect(mockedApis.responseProvider.activityLogResponse).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call get Activity Log API with valid date ranges', async () => {
|
||||
dispatchUserChangedUrl();
|
||||
|
||||
const updatePagingDispatched = waitForAction('endpointDetailsActivityLogUpdatePaging');
|
||||
dispatchGetActivityLogPaging({ page: 1 });
|
||||
|
||||
const updateInvalidDateRangeDispatched = waitForAction(
|
||||
'endpointDetailsActivityLogUpdateIsInvalidDateRange'
|
||||
);
|
||||
dispatchGetActivityLogUpdateInvalidDateRange({ isInvalidDateRange: false });
|
||||
await updateInvalidDateRangeDispatched;
|
||||
await updatePagingDispatched;
|
||||
|
||||
expect(mockedApis.responseProvider.activityLogResponse).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call get Activity Log API with correct paging options', async () => {
|
||||
dispatchUserChangedUrl();
|
||||
|
||||
const updatePagingDispatched = waitForAction('endpointDetailsActivityLogUpdatePaging');
|
||||
dispatchGetActivityLogPaging({ page: 3 });
|
||||
|
||||
await updatePagingDispatched;
|
||||
|
||||
expect(mockedApis.responseProvider.activityLogResponse).toHaveBeenCalledWith({
|
||||
path: expect.any(String),
|
||||
query: {
|
||||
page: 3,
|
||||
page_size: 50,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('handle Endpoint Pending Actions state actions', () => {
|
||||
|
|
|
@ -120,6 +120,7 @@ export const endpointMiddlewareFactory: ImmutableMiddlewareFactory<EndpointState
|
|||
await loadEndpointDetails({ store, coreStart, selectedEndpoint: action.payload.endpointId });
|
||||
}
|
||||
|
||||
// get activity log API
|
||||
if (
|
||||
action.type === 'userChangedUrl' &&
|
||||
hasSelectedEndpoint(getState()) === true &&
|
||||
|
@ -502,99 +503,6 @@ async function endpointDetailsListMiddleware({
|
|||
}
|
||||
}
|
||||
|
||||
async function endpointDetailsActivityLogPagingMiddleware({
|
||||
store,
|
||||
coreStart,
|
||||
}: {
|
||||
store: ImmutableMiddlewareAPI<EndpointState, AppAction>;
|
||||
coreStart: CoreStart;
|
||||
}) {
|
||||
const { getState, dispatch } = store;
|
||||
try {
|
||||
const { disabled, page, pageSize, startDate, endDate } = getActivityLogDataPaging(getState());
|
||||
// don't page when paging is disabled or when date ranges are invalid
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
if (getIsInvalidDateRange({ startDate, endDate })) {
|
||||
dispatch({
|
||||
type: 'endpointDetailsActivityLogUpdateIsInvalidDateRange',
|
||||
payload: {
|
||||
isInvalidDateRange: true,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: 'endpointDetailsActivityLogUpdateIsInvalidDateRange',
|
||||
payload: {
|
||||
isInvalidDateRange: false,
|
||||
},
|
||||
});
|
||||
dispatch({
|
||||
type: 'endpointDetailsActivityLogChanged',
|
||||
// ts error to be fixed when AsyncResourceState is refactored (#830)
|
||||
// @ts-expect-error
|
||||
payload: createLoadingResourceState<ActivityLog>(getActivityLogData(getState())),
|
||||
});
|
||||
const route = resolvePathVariables(ENDPOINT_ACTION_LOG_ROUTE, {
|
||||
agent_id: selectedAgent(getState()),
|
||||
});
|
||||
const activityLog = await coreStart.http.get<ActivityLog>(route, {
|
||||
query: {
|
||||
page,
|
||||
page_size: pageSize,
|
||||
start_date: startDate,
|
||||
end_date: endDate,
|
||||
},
|
||||
});
|
||||
|
||||
const lastLoadedLogData = getLastLoadedActivityLogData(getState());
|
||||
if (lastLoadedLogData !== undefined) {
|
||||
const updatedLogDataItems = ([
|
||||
...new Set([...lastLoadedLogData.data, ...activityLog.data]),
|
||||
] as ActivityLog['data']).sort((a, b) =>
|
||||
new Date(b.item.data['@timestamp']) > new Date(a.item.data['@timestamp']) ? 1 : -1
|
||||
);
|
||||
|
||||
const updatedLogData = {
|
||||
page: activityLog.page,
|
||||
pageSize: activityLog.pageSize,
|
||||
startDate: activityLog.startDate,
|
||||
endDate: activityLog.endDate,
|
||||
data: activityLog.page === 1 ? activityLog.data : updatedLogDataItems,
|
||||
};
|
||||
dispatch({
|
||||
type: 'endpointDetailsActivityLogChanged',
|
||||
payload: createLoadedResourceState<ActivityLog>(updatedLogData),
|
||||
});
|
||||
if (!activityLog.data.length) {
|
||||
dispatch({
|
||||
type: 'endpointDetailsActivityLogUpdatePaging',
|
||||
payload: {
|
||||
disabled: true,
|
||||
page: activityLog.page > 1 ? activityLog.page - 1 : 1,
|
||||
pageSize: activityLog.pageSize,
|
||||
startDate: activityLog.startDate,
|
||||
endDate: activityLog.endDate,
|
||||
},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
dispatch({
|
||||
type: 'endpointDetailsActivityLogChanged',
|
||||
payload: createLoadedResourceState<ActivityLog>(activityLog),
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
dispatch({
|
||||
type: 'endpointDetailsActivityLogChanged',
|
||||
payload: createFailedResourceState<ActivityLog>(error.body ?? error),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function loadEndpointDetails({
|
||||
selectedEndpoint,
|
||||
store,
|
||||
|
@ -720,7 +628,6 @@ async function endpointDetailsActivityLogChangedMiddleware({
|
|||
coreStart: CoreStart;
|
||||
}) {
|
||||
const { getState, dispatch } = store;
|
||||
// call the activity log api
|
||||
dispatch({
|
||||
type: 'endpointDetailsActivityLogChanged',
|
||||
// ts error to be fixed when AsyncResourceState is refactored (#830)
|
||||
|
@ -748,6 +655,99 @@ async function endpointDetailsActivityLogChangedMiddleware({
|
|||
}
|
||||
}
|
||||
|
||||
async function endpointDetailsActivityLogPagingMiddleware({
|
||||
store,
|
||||
coreStart,
|
||||
}: {
|
||||
store: ImmutableMiddlewareAPI<EndpointState, AppAction>;
|
||||
coreStart: CoreStart;
|
||||
}) {
|
||||
const { getState, dispatch } = store;
|
||||
try {
|
||||
const { disabled, page, pageSize, startDate, endDate } = getActivityLogDataPaging(getState());
|
||||
// don't page when paging is disabled or when date ranges are invalid
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
if (getIsInvalidDateRange({ startDate, endDate })) {
|
||||
dispatch({
|
||||
type: 'endpointDetailsActivityLogUpdateIsInvalidDateRange',
|
||||
payload: {
|
||||
isInvalidDateRange: true,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: 'endpointDetailsActivityLogUpdateIsInvalidDateRange',
|
||||
payload: {
|
||||
isInvalidDateRange: false,
|
||||
},
|
||||
});
|
||||
dispatch({
|
||||
type: 'endpointDetailsActivityLogChanged',
|
||||
// ts error to be fixed when AsyncResourceState is refactored (#830)
|
||||
// @ts-expect-error
|
||||
payload: createLoadingResourceState<ActivityLog>(getActivityLogData(getState())),
|
||||
});
|
||||
const route = resolvePathVariables(ENDPOINT_ACTION_LOG_ROUTE, {
|
||||
agent_id: selectedAgent(getState()),
|
||||
});
|
||||
const activityLog = await coreStart.http.get<ActivityLog>(route, {
|
||||
query: {
|
||||
page,
|
||||
page_size: pageSize,
|
||||
start_date: startDate,
|
||||
end_date: endDate,
|
||||
},
|
||||
});
|
||||
|
||||
const lastLoadedLogData = getLastLoadedActivityLogData(getState());
|
||||
if (lastLoadedLogData !== undefined) {
|
||||
const updatedLogDataItems = ([
|
||||
...new Set([...lastLoadedLogData.data, ...activityLog.data]),
|
||||
] as ActivityLog['data']).sort((a, b) =>
|
||||
new Date(b.item.data['@timestamp']) > new Date(a.item.data['@timestamp']) ? 1 : -1
|
||||
);
|
||||
|
||||
const updatedLogData = {
|
||||
page: activityLog.page,
|
||||
pageSize: activityLog.pageSize,
|
||||
startDate: activityLog.startDate,
|
||||
endDate: activityLog.endDate,
|
||||
data: activityLog.page === 1 ? activityLog.data : updatedLogDataItems,
|
||||
};
|
||||
dispatch({
|
||||
type: 'endpointDetailsActivityLogChanged',
|
||||
payload: createLoadedResourceState<ActivityLog>(updatedLogData),
|
||||
});
|
||||
if (!activityLog.data.length) {
|
||||
dispatch({
|
||||
type: 'endpointDetailsActivityLogUpdatePaging',
|
||||
payload: {
|
||||
disabled: true,
|
||||
page: activityLog.page > 1 ? activityLog.page - 1 : 1,
|
||||
pageSize: activityLog.pageSize,
|
||||
startDate: activityLog.startDate,
|
||||
endDate: activityLog.endDate,
|
||||
},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
dispatch({
|
||||
type: 'endpointDetailsActivityLogChanged',
|
||||
payload: createLoadedResourceState<ActivityLog>(activityLog),
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
dispatch({
|
||||
type: 'endpointDetailsActivityLogChanged',
|
||||
payload: createFailedResourceState<ActivityLog>(error.body ?? error),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleLoadMetadataTransformStats(http: HttpStart, store: EndpointPageStore) {
|
||||
const { getState, dispatch } = store;
|
||||
|
||||
|
|
|
@ -30,7 +30,7 @@ import {
|
|||
} from '../../mocks';
|
||||
import { registerActionAuditLogRoutes } from './audit_log';
|
||||
import uuid from 'uuid';
|
||||
import { aMockAction, aMockResponse, MockAction, mockAuditLog, MockResponse } from './mocks';
|
||||
import { aMockAction, aMockResponse, MockAction, mockSearchResult, MockResponse } from './mocks';
|
||||
import { SecuritySolutionRequestHandlerContext } from '../../../types';
|
||||
import { ActivityLog } from '../../../../common/endpoint/types';
|
||||
|
||||
|
@ -105,10 +105,11 @@ describe('Action Log API', () => {
|
|||
|
||||
// convenience for calling the route and handler for audit log
|
||||
let getActivityLog: (
|
||||
params: EndpointActionLogRequestParams,
|
||||
query?: EndpointActionLogRequestQuery
|
||||
) => Promise<jest.Mocked<KibanaResponseFactory>>;
|
||||
// convenience for injecting mock responses for actions index and responses
|
||||
let havingActionsAndResponses: (actions: MockAction[], responses: any[]) => void;
|
||||
let havingActionsAndResponses: (actions: MockAction[], responses: MockResponse[]) => void;
|
||||
|
||||
let havingErrors: () => void;
|
||||
|
||||
|
@ -125,9 +126,12 @@ describe('Action Log API', () => {
|
|||
experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental),
|
||||
});
|
||||
|
||||
getActivityLog = async (query?: any): Promise<jest.Mocked<KibanaResponseFactory>> => {
|
||||
getActivityLog = async (
|
||||
params: { agent_id: string },
|
||||
query?: { page: number; page_size: number; start_date?: string; end_date?: string }
|
||||
): Promise<jest.Mocked<KibanaResponseFactory>> => {
|
||||
const req = httpServerMock.createKibanaRequest({
|
||||
params: { agent_id: mockID },
|
||||
params,
|
||||
query,
|
||||
});
|
||||
const mockResponse = httpServerMock.createResponseFactory();
|
||||
|
@ -152,18 +156,12 @@ describe('Action Log API', () => {
|
|||
};
|
||||
|
||||
havingActionsAndResponses = (actions: MockAction[], responses: MockResponse[]) => {
|
||||
const actionsData = actions.map((a) => ({
|
||||
_index: '.fleet-actions-7',
|
||||
_source: a.build(),
|
||||
}));
|
||||
const responsesData = responses.map((r) => ({
|
||||
_index: '.ds-.fleet-actions-results-2021.06.09-000001',
|
||||
_source: r.build(),
|
||||
}));
|
||||
const mockResult = mockAuditLog([...actionsData, ...responsesData]);
|
||||
esClientMock.asCurrentUser.search = jest
|
||||
.fn()
|
||||
.mockImplementationOnce(() => Promise.resolve(mockResult));
|
||||
esClientMock.asCurrentUser.search = jest.fn().mockImplementation((req) => {
|
||||
const items: any[] =
|
||||
req.index === '.fleet-actions' ? actions.splice(0, 50) : responses.splice(0, 1000);
|
||||
|
||||
return Promise.resolve(mockSearchResult(items.map((x) => x.build())));
|
||||
});
|
||||
};
|
||||
|
||||
havingErrors = () => {
|
||||
|
@ -181,28 +179,33 @@ describe('Action Log API', () => {
|
|||
|
||||
it('should return an empty array when nothing in audit log', async () => {
|
||||
havingActionsAndResponses([], []);
|
||||
const response = await getActivityLog();
|
||||
const response = await getActivityLog({ agent_id: mockID });
|
||||
expect(response.ok).toBeCalled();
|
||||
expect((response.ok.mock.calls[0][0]?.body as ActivityLog).data).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should have actions and action responses', async () => {
|
||||
havingActionsAndResponses(
|
||||
[aMockAction().withAgent(mockID).withAction('isolate').withID(actionID)],
|
||||
[aMockResponse(actionID, mockID)]
|
||||
[
|
||||
aMockAction().withAgent(mockID).withAction('isolate').withID(actionID),
|
||||
aMockAction().withAgent(mockID).withAction('unisolate'),
|
||||
],
|
||||
[aMockResponse(actionID, mockID).forAction(actionID).forAgent(mockID)]
|
||||
);
|
||||
const response = await getActivityLog();
|
||||
const response = await getActivityLog({ agent_id: mockID });
|
||||
const responseBody = response.ok.mock.calls[0][0]?.body as ActivityLog;
|
||||
|
||||
expect(response.ok).toBeCalled();
|
||||
expect(responseBody.data).toHaveLength(2);
|
||||
expect(responseBody.data).toHaveLength(3);
|
||||
expect(responseBody.data.filter((e) => e.type === 'response')).toHaveLength(1);
|
||||
expect(responseBody.data.filter((e) => e.type === 'action')).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should throw errors when no results for some agentID', async () => {
|
||||
havingErrors();
|
||||
|
||||
try {
|
||||
await getActivityLog();
|
||||
await getActivityLog({ agent_id: mockID });
|
||||
} catch (error) {
|
||||
expect(error.message).toEqual(`Error fetching actions log for agent_id ${mockID}`);
|
||||
}
|
||||
|
@ -212,12 +215,15 @@ describe('Action Log API', () => {
|
|||
havingActionsAndResponses([], []);
|
||||
const startDate = new Date(new Date().setDate(new Date().getDate() - 1)).toISOString();
|
||||
const endDate = new Date().toISOString();
|
||||
const response = await getActivityLog({
|
||||
page: 1,
|
||||
page_size: 50,
|
||||
start_date: startDate,
|
||||
end_date: endDate,
|
||||
});
|
||||
const response = await getActivityLog(
|
||||
{ agent_id: mockID },
|
||||
{
|
||||
page: 1,
|
||||
page_size: 50,
|
||||
start_date: startDate,
|
||||
end_date: endDate,
|
||||
}
|
||||
);
|
||||
expect(response.ok).toBeCalled();
|
||||
expect((response.ok.mock.calls[0][0]?.body as ActivityLog).startDate).toEqual(startDate);
|
||||
expect((response.ok.mock.calls[0][0]?.body as ActivityLog).endDate).toEqual(endDate);
|
||||
|
|
|
@ -18,29 +18,6 @@ import {
|
|||
ISOLATION_ACTIONS,
|
||||
} from '../../../../common/endpoint/types';
|
||||
|
||||
export const mockAuditLog = (results: any = []): ApiResponse<any> => {
|
||||
return {
|
||||
body: {
|
||||
hits: {
|
||||
total: results.length,
|
||||
hits: results.map((a: any) => {
|
||||
const _index = a._index;
|
||||
delete a._index;
|
||||
const _source = a;
|
||||
return {
|
||||
_index,
|
||||
_source,
|
||||
};
|
||||
}),
|
||||
},
|
||||
},
|
||||
statusCode: 200,
|
||||
headers: {},
|
||||
warnings: [],
|
||||
meta: {} as any,
|
||||
};
|
||||
};
|
||||
|
||||
export const mockSearchResult = (results: any = []): ApiResponse<any> => {
|
||||
return {
|
||||
body: {
|
||||
|
|
Loading…
Reference in a new issue