Skip to content
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

[Security Solution] [Endpoint] Add endpoint details activity log #99795

Merged
merged 34 commits into from
Jun 3, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
c26a7d4
WIP
ashokaditya May 11, 2021
b8fba34
fetch activity log for endpoint
ashokaditya May 12, 2021
a2eb777
refactor to hold host details and activity log within endpointDetails
ashokaditya May 14, 2021
24d392f
api for fetching actions log
ashokaditya May 17, 2021
62e38fe
add a selector for getting selected agent id
ashokaditya May 17, 2021
b6aa967
use the new api to show actions log
ashokaditya May 17, 2021
21fe79e
Merge branch 'master' into sec-team-1105/endpoint-details-activity-log
kibanamachine May 18, 2021
2dd22ed
review changes
ashokaditya May 19, 2021
567af29
Merge branch 'master' into sec-team-1105/endpoint-details-activity-log
kibanamachine May 19, 2021
13e8963
Merge branch 'master' into sec-team-1105/endpoint-details-activity-log
kibanamachine May 20, 2021
46c187d
move util function to common/utils
ashokaditya May 20, 2021
691b90a
use util function to get API path
ashokaditya May 20, 2021
3859af9
sync url params with details active tab
ashokaditya May 20, 2021
3722552
Merge branch 'master' into sec-team-1105/endpoint-details-activity-log
ashokaditya May 25, 2021
923a157
fix types due to merge commit
ashokaditya May 25, 2021
c5ade7b
use AsyncResourseState type
ashokaditya May 26, 2021
0f08908
sort entries chronologically with recent at the top
ashokaditya May 26, 2021
f760aca
adjust icon sizes within entries to match mocks
ashokaditya May 26, 2021
72c7b77
remove endpoint list paging stuff (not for now)
ashokaditya May 26, 2021
f2dd6cc
Merge branch 'master' into sec-team-1105/endpoint-details-activity-log
ashokaditya May 27, 2021
eb5f67e
fix import after sync with master
ashokaditya May 28, 2021
fd390c8
Merge branch 'master' into sec-team-1105/endpoint-details-activity-log
kibanamachine May 28, 2021
f9b95b9
make the search bar work (sort of)
ashokaditya May 28, 2021
299dc91
add tests to middleware for now
ashokaditya May 28, 2021
0d907f8
Merge branch 'master' into sec-team-1105/endpoint-details-activity-log
kibanamachine May 31, 2021
1c54aaa
use snake case for naming routes
ashokaditya May 31, 2021
e38e202
rename and use own relative time function
ashokaditya May 31, 2021
31cf7af
use euiTheme tokens
ashokaditya May 31, 2021
33f7d0b
add a comment
ashokaditya May 31, 2021
8d46a98
log errors to kibana log and unwind stack
ashokaditya May 31, 2021
a77204e
Merge branch 'master' into sec-team-1105/endpoint-details-activity-log
kibanamachine Jun 2, 2021
8fb56f5
Merge branch 'master' into sec-team-1105/endpoint-details-activity-log
kibanamachine Jun 2, 2021
6a2cace
use FleetActionGenerator for mocking data
ashokaditya Jun 2, 2021
453d034
Merge branch 'master' into sec-team-1105/endpoint-details-activity-log
kibanamachine Jun 2, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions x-pack/plugins/security_solution/common/endpoint/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,6 @@ export const AGENT_POLICY_SUMMARY_ROUTE = `${BASE_POLICY_ROUTE}/summaries`;
/** Host Isolation Routes */
export const ISOLATE_HOST_ROUTE = `/api/endpoint/isolate`;
export const UNISOLATE_HOST_ROUTE = `/api/endpoint/unisolate`;

/** Endpoint Actions Log Routes */
export const ENDPOINT_ACTION_LOG_ROUTE = `/api/endpoint/action_log/{agent_id}`;
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,11 @@ export const HostIsolationRequestSchema = {
comment: schema.maybe(schema.string()),
}),
};

export const EndpointActionLogRequestSchema = {
// TODO improve when using pagination with query params
query: schema.object({}),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can either leave this off this PR until we add the query definition, or at least add a comment saying this is for yet-to-come pagination query params

params: schema.object({
agent_id: schema.string(),
}),
};
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,14 @@
import React from 'react';
import { FormattedDate, FormattedTime, FormattedRelative } from '@kbn/i18n/react';

export const FormattedDateAndTime: React.FC<{ date: Date }> = ({ date }) => {
export const FormattedDateAndTime: React.FC<{ date: Date; showRelativeTime?: boolean }> = ({
date,
showRelativeTime = false,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why add the new prop? do you want relative time to always be shown? if so, you might just want to use <FormattedRelativeTime> directly in your component. But if that is the case, I would also talk to Bonnie about that - always showing relative time (IMO) is not a good after the first 1h.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I see what you mean. I guess I misunderstood you earlier. I assumed that we always want to show relative time. At least that's what it looked like in the mocks. This UX is still going to be worked on more in subsequent PRs. So we've time to make this slicker. 🙏

}) => {
// If date is greater than or equal to 1h (ago), then show it as a date
// and if showRelativeTime is false
// else, show it as relative to "now"
return Date.now() - date.getTime() >= 3.6e6 ? (
return Date.now() - date.getTime() >= 3.6e6 && !showRelativeTime ? (
<>
<FormattedDate value={date} year="numeric" month="short" day="2-digit" />
{' @'}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {
UpdateAlertStatusProps,
CasesFromAlertsResponse,
} from './types';
import { resolvePathVariables } from '../../../../management/pages/trusted_apps/service/utils';
import { resolvePathVariables } from '../../../../management/common/utils';
import { isolateHost, unIsolateHost } from '../../../../common/lib/host_isolation';

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* 2.0.
*/

import { parseQueryFilterToKQL } from './utils';
import { parseQueryFilterToKQL, resolvePathVariables } from './utils';

describe('utils', () => {
const searchableFields = [`name`, `description`, `entries.value`, `entries.entries.value`];
Expand Down Expand Up @@ -39,4 +39,39 @@ describe('utils', () => {
);
});
});

describe('resolvePathVariables', () => {
it('should resolve defined variables', () => {
expect(resolvePathVariables('/segment1/{var1}/segment2', { var1: 'value1' })).toBe(
'/segment1/value1/segment2'
);
});

it('should not resolve undefined variables', () => {
expect(resolvePathVariables('/segment1/{var1}/segment2', {})).toBe(
'/segment1/{var1}/segment2'
);
});

it('should ignore unused variables', () => {
expect(resolvePathVariables('/segment1/{var1}/segment2', { var2: 'value2' })).toBe(
'/segment1/{var1}/segment2'
);
});

it('should replace multiple variable occurences', () => {
expect(resolvePathVariables('/{var1}/segment1/{var1}', { var1: 'value1' })).toBe(
'/value1/segment1/value1'
);
});

it('should replace multiple variables', () => {
const path = resolvePathVariables('/{var1}/segment1/{var2}', {
var1: 'value1',
var2: 'value2',
});

expect(path).toBe('/value1/segment1/value2');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,8 @@ export const parseQueryFilterToKQL = (filter: string, fields: Readonly<string[]>

return kuery;
};

export const resolvePathVariables = (path: string, variables: { [K: string]: string | number }) =>
Object.keys(variables).reduce((acc, paramName) => {
return acc.replace(new RegExp(`\{${paramName}\}`, 'g'), String(variables[paramName]));
}, path);
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ export interface ServerFailedToReturnEndpointDetails {
type: 'serverFailedToReturnEndpointDetails';
payload: ServerApiError;
}

export interface ServerReturnedEndpointPolicyResponse {
type: 'serverReturnedEndpointPolicyResponse';
payload: GetHostPolicyResponse;
Expand Down Expand Up @@ -137,19 +136,24 @@ export interface ServerFailedToReturnEndpointsTotal {
payload: ServerApiError;
}

type EndpointIsolationRequest = Action<'endpointIsolationRequest'> & {
export type EndpointIsolationRequest = Action<'endpointIsolationRequest'> & {
payload: HostIsolationRequestBody;
};

type EndpointIsolationRequestStateChange = Action<'endpointIsolationRequestStateChange'> & {
export type EndpointIsolationRequestStateChange = Action<'endpointIsolationRequestStateChange'> & {
payload: EndpointState['isolationRequestState'];
};

export type EndpointDetailsActivityLogChanged = Action<'endpointDetailsActivityLogChanged'> & {
payload: EndpointState['endpointDetails']['activityLog'];
};

export type EndpointAction =
| ServerReturnedEndpointList
| ServerFailedToReturnEndpointList
| ServerReturnedEndpointDetails
| ServerFailedToReturnEndpointDetails
| EndpointDetailsActivityLogChanged
| ServerReturnedEndpointPolicyResponse
| ServerFailedToReturnEndpointPolicyResponse
| ServerReturnedPoliciesForOnboarding
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { Immutable } from '../../../../../common/endpoint/types';
import { DEFAULT_POLL_INTERVAL } from '../../../common/constants';
import { createUninitialisedResourceState } from '../../../state';
import { EndpointState } from '../types';

export const initialEndpointPageState = (): Immutable<EndpointState> => {
return {
hosts: [],
pageSize: 10,
pageIndex: 0,
total: 0,
loading: false,
error: undefined,
endpointDetails: {
activityLog: createUninitialisedResourceState(),
hostDetails: {
details: undefined,
detailsLoading: false,
detailsError: undefined,
},
},
policyResponse: undefined,
policyResponseLoading: false,
policyResponseError: undefined,
location: undefined,
policyItems: [],
selectedPolicyId: undefined,
policyItemsLoading: false,
endpointPackageInfo: undefined,
nonExistingPolicies: {},
agentPolicies: {},
endpointsExist: true,
patterns: [],
patternsError: undefined,
isAutoRefreshEnabled: true,
autoRefreshInterval: DEFAULT_POLL_INTERVAL,
agentsWithEndpointsTotal: 0,
agentsWithEndpointsTotalError: undefined,
endpointsTotal: 0,
endpointsTotalError: undefined,
queryStrategyVersion: undefined,
policyVersionInfo: undefined,
hostStatus: undefined,
isolationRequestState: createUninitialisedResourceState(),
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,16 @@ describe('EndpointList store concerns', () => {
total: 0,
loading: false,
error: undefined,
details: undefined,
detailsLoading: false,
detailsError: undefined,
endpointDetails: {
paul-tavares marked this conversation as resolved.
Show resolved Hide resolved
activityLog: {
ashokaditya marked this conversation as resolved.
Show resolved Hide resolved
type: 'UninitialisedResourceState',
},
hostDetails: {
details: undefined,
detailsLoading: false,
detailsError: undefined,
},
},
policyResponse: undefined,
policyResponseLoading: false,
policyResponseError: undefined,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,17 @@ import {
Immutable,
HostResultList,
HostIsolationResponse,
EndpointAction,
} from '../../../../../common/endpoint/types';
import { AppAction } from '../../../../common/store/actions';
import { mockEndpointResultList } from './mock_endpoint_result_list';
import { listData } from './selectors';
import { EndpointState } from '../types';
import { endpointListReducer } from './reducer';
import { endpointMiddlewareFactory } from './middleware';
import { getEndpointListPath } from '../../../common/routing';
import { getEndpointListPath, getEndpointDetailsPath } from '../../../common/routing';
import {
createLoadedResourceState,
FailedResourceState,
isFailedResourceState,
isLoadedResourceState,
Expand All @@ -39,6 +41,7 @@ import {
hostIsolationRequestBodyMock,
hostIsolationResponseMock,
} from '../../../../common/lib/host_isolation/mocks';
import { FleetActionGenerator } from '../../../../../common/endpoint/data_generators/fleet_action_generator';

jest.mock('../../policy/store/services/ingest', () => ({
sendGetAgentConfigList: () => Promise.resolve({ items: [] }),
Expand Down Expand Up @@ -192,4 +195,65 @@ describe('endpoint list middleware', () => {
expect(failedAction.error).toBe(apiError);
});
});

describe('handle ActivityLog State Change actions', () => {
const endpointList = getEndpointListApiResponse();
const search = getEndpointDetailsPath({
name: 'endpointDetails',
selected_endpoint: endpointList.hosts[0].metadata.agent.id,
});
const dispatchUserChangedUrl = () => {
dispatch({
type: 'userChangedUrl',
payload: {
...history.location,
pathname: '/endpoints',
search: `?${search.split('?').pop()}`,
},
});
};
const fleetActionGenerator = new FleetActionGenerator(Math.random().toString());
const activityLog = [
fleetActionGenerator.generate({
agents: [endpointList.hosts[0].metadata.agent.id],
}),
];
const dispatchGetActivityLog = () => {
dispatch({
type: 'endpointDetailsActivityLogChanged',
payload: createLoadedResourceState(activityLog),
});
};

it('should set ActivityLog state to loading', async () => {
dispatchUserChangedUrl();

const loadingDispatched = waitForAction('endpointDetailsActivityLogChanged', {
validate(action) {
return isLoadingResourceState(action.payload);
},
});

const loadingDispatchedResponse = await loadingDispatched;
expect(loadingDispatchedResponse.payload.type).toEqual('LoadingResourceState');
});

it('should set ActivityLog state to loaded when fetching activity log is successful', async () => {
dispatchUserChangedUrl();

const loadedDispatched = waitForAction('endpointDetailsActivityLogChanged', {
validate(action) {
return isLoadedResourceState(action.payload);
},
});

dispatchGetActivityLog();
const loadedDispatchedResponse = await loadedDispatched;
const activityLogData = (loadedDispatchedResponse.payload as LoadedResourceState<
EndpointAction[]
>).data;

expect(activityLogData).toEqual(activityLog);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import { HttpStart } from 'kibana/public';
import {
EndpointAction,
HostInfo,
HostIsolationRequestBody,
HostIsolationResponse,
Expand All @@ -18,6 +19,7 @@ import { ImmutableMiddlewareAPI, ImmutableMiddlewareFactory } from '../../../../
import {
isOnEndpointPage,
hasSelectedEndpoint,
selectedAgent,
uiQueryParams,
listData,
endpointPackageInfo,
Expand All @@ -27,6 +29,7 @@ import {
isTransformEnabled,
getIsIsolationRequestPending,
getCurrentIsolationRequestState,
getActivityLogData,
} from './selectors';
import { EndpointState, PolicyIds } from '../types';
import {
Expand All @@ -37,12 +40,13 @@ import {
} from '../../policy/store/services/ingest';
import { AGENT_POLICY_SAVED_OBJECT_TYPE } from '../../../../../../fleet/common';
import {
ENDPOINT_ACTION_LOG_ROUTE,
HOST_METADATA_GET_API,
HOST_METADATA_LIST_API,
metadataCurrentIndexPattern,
} from '../../../../../common/endpoint/constants';
import { IIndexPattern, Query } from '../../../../../../../../src/plugins/data/public';
import { resolvePathVariables } from '../../trusted_apps/service/utils';
import { resolvePathVariables } from '../../../common/utils';
import {
createFailedResourceState,
createLoadedResourceState,
Expand Down Expand Up @@ -336,6 +340,29 @@ export const endpointMiddlewareFactory: ImmutableMiddlewareFactory<EndpointState
});
}

// call the activity log api
dispatch({
type: 'endpointDetailsActivityLogChanged',
// ts error to be fixed when AsyncResourceState is refactored (#830)
// @ts-expect-error
payload: createLoadingResourceState<EndpointAction[]>(getActivityLogData(getState())),
});

try {
const activityLog = await coreStart.http.get<EndpointAction[]>(
resolvePathVariables(ENDPOINT_ACTION_LOG_ROUTE, { agent_id: selectedAgent(getState()) })
);
dispatch({
type: 'endpointDetailsActivityLogChanged',
payload: createLoadedResourceState<EndpointAction[]>(activityLog),
});
} catch (error) {
dispatch({
type: 'endpointDetailsActivityLogChanged',
payload: createFailedResourceState<EndpointAction[]>(error.body ?? error),
});
}

// call the policy response api
try {
const policyResponse = await coreStart.http.get(`/api/endpoint/policy_response`, {
Expand Down
Loading