[Security Solution][Endpoint] Additional Endpoint Activity log tests (#109776) (#110075)

* 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:
Kibana Machine 2021-08-25 13:17:29 -04:00 committed by GitHub
parent a36bab718d
commit 0ff10856ac
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 215 additions and 145 deletions

View file

@ -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', () => {

View file

@ -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;

View file

@ -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);

View file

@ -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: {