Skip to content

Commit

Permalink
[Security Solution] [Endpoint] Add endpoint details activity log (#99795
Browse files Browse the repository at this point in the history
) (#101244)

* WIP

add tabs for endpoint details

* fetch activity log for endpoint

this is work in progress with dummy data

* refactor to hold host details and activity log within endpointDetails

* api for fetching actions log

* add a selector for getting selected agent id

* use the new api to show actions log

* review changes

* move util function to common/utils

in order to use it in endpoint_hosts as well as in trusted _apps

review suggestion

* use util function to get API path

review suggestion

* sync url params with details active tab

review suggestion

* fix types due to merge commit

refs 3722552

* use AsyncResourseState type

review suggestions

* sort entries chronologically with recent at the top

* adjust icon sizes within entries to match mocks

* remove endpoint list paging stuff (not for now)

* fix import after sync with master

* make the search bar work (sort of)

this needs to be fleshed out in a later PR

* add tests to middleware for now

* use snake case for naming routes

review changes

* rename and use own relative time function

review change

* use euiTheme tokens

review change

* add a comment

review changes

* log errors to kibana log and unwind stack

review changes

* use FleetActionGenerator for mocking data

review changes

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>

Co-authored-by: Ashokaditya <am.struktr@gmail.com>
  • Loading branch information
kibanamachine and ashokaditya committed Jun 3, 2021
1 parent b7070cb commit c42fbf7
Show file tree
Hide file tree
Showing 30 changed files with 868 additions and 149 deletions.
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({}),
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,
}) => {
// 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: {
activityLog: {
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

0 comments on commit c42fbf7

Please sign in to comment.