mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[Security Solution] [Endpoint] Add endpoint details activity log (#99795)
* 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 3722552f73
* 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>
This commit is contained in:
parent
f367deca48
commit
d4ecee6ba0
30 changed files with 868 additions and 149 deletions
|
@ -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}`;
|
||||
|
|
|
@ -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(),
|
||||
}),
|
||||
};
|
||||
|
|
|
@ -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" />
|
||||
{' @'}
|
||||
|
|
|
@ -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';
|
||||
|
||||
/**
|
||||
|
|
|
@ -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`];
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -37,7 +37,6 @@ export interface ServerFailedToReturnEndpointDetails {
|
|||
type: 'serverFailedToReturnEndpointDetails';
|
||||
payload: ServerApiError;
|
||||
}
|
||||
|
||||
export interface ServerReturnedEndpointPolicyResponse {
|
||||
type: 'serverReturnedEndpointPolicyResponse';
|
||||
payload: GetHostPolicyResponse;
|
||||
|
@ -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
|
||||
|
|
|
@ -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(),
|
||||
};
|
||||
};
|
|
@ -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,
|
||||
|
|
|
@ -18,6 +18,7 @@ import {
|
|||
Immutable,
|
||||
HostResultList,
|
||||
HostIsolationResponse,
|
||||
EndpointAction,
|
||||
} from '../../../../../common/endpoint/types';
|
||||
import { AppAction } from '../../../../common/store/actions';
|
||||
import { mockEndpointResultList } from './mock_endpoint_result_list';
|
||||
|
@ -25,8 +26,9 @@ 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,
|
||||
|
@ -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: [] }),
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { HttpStart } from 'kibana/public';
|
||||
import {
|
||||
EndpointAction,
|
||||
HostInfo,
|
||||
HostIsolationRequestBody,
|
||||
HostIsolationResponse,
|
||||
|
@ -18,6 +19,7 @@ import { ImmutableMiddlewareAPI, ImmutableMiddlewareFactory } from '../../../../
|
|||
import {
|
||||
isOnEndpointPage,
|
||||
hasSelectedEndpoint,
|
||||
selectedAgent,
|
||||
uiQueryParams,
|
||||
listData,
|
||||
endpointPackageInfo,
|
||||
|
@ -27,6 +29,7 @@ import {
|
|||
isTransformEnabled,
|
||||
getIsIsolationRequestPending,
|
||||
getCurrentIsolationRequestState,
|
||||
getActivityLogData,
|
||||
} from './selectors';
|
||||
import { EndpointState, PolicyIds } from '../types';
|
||||
import {
|
||||
|
@ -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,
|
||||
|
@ -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`, {
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EndpointDetailsActivityLogChanged } from './action';
|
||||
import {
|
||||
isOnEndpointPage,
|
||||
hasSelectedEndpoint,
|
||||
|
@ -12,52 +13,33 @@ import {
|
|||
getCurrentIsolationRequestState,
|
||||
} from './selectors';
|
||||
import { EndpointState } from '../types';
|
||||
import { initialEndpointPageState } from './builders';
|
||||
import { AppAction } from '../../../../common/store/actions';
|
||||
import { ImmutableReducer } from '../../../../common/store';
|
||||
import { Immutable } from '../../../../../common/endpoint/types';
|
||||
import { DEFAULT_POLL_INTERVAL } from '../../../common/constants';
|
||||
import { createUninitialisedResourceState, isUninitialisedResourceState } from '../../../state';
|
||||
|
||||
export const initialEndpointListState: Immutable<EndpointState> = {
|
||||
hosts: [],
|
||||
pageSize: 10,
|
||||
pageIndex: 0,
|
||||
total: 0,
|
||||
loading: false,
|
||||
error: undefined,
|
||||
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(),
|
||||
type StateReducer = ImmutableReducer<EndpointState, AppAction>;
|
||||
type CaseReducer<T extends AppAction> = (
|
||||
state: Immutable<EndpointState>,
|
||||
action: Immutable<T>
|
||||
) => Immutable<EndpointState>;
|
||||
|
||||
const handleEndpointDetailsActivityLogChanged: CaseReducer<EndpointDetailsActivityLogChanged> = (
|
||||
state,
|
||||
action
|
||||
) => {
|
||||
return {
|
||||
...state!,
|
||||
endpointDetails: {
|
||||
...state.endpointDetails!,
|
||||
activityLog: action.payload,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/* eslint-disable-next-line complexity */
|
||||
export const endpointListReducer: ImmutableReducer<EndpointState, AppAction> = (
|
||||
state = initialEndpointListState,
|
||||
action
|
||||
) => {
|
||||
export const endpointListReducer: StateReducer = (state = initialEndpointPageState(), action) => {
|
||||
if (action.type === 'serverReturnedEndpointList') {
|
||||
const {
|
||||
hosts,
|
||||
|
@ -115,18 +97,32 @@ export const endpointListReducer: ImmutableReducer<EndpointState, AppAction> = (
|
|||
} else if (action.type === 'serverReturnedEndpointDetails') {
|
||||
return {
|
||||
...state,
|
||||
details: action.payload.metadata,
|
||||
endpointDetails: {
|
||||
...state.endpointDetails,
|
||||
hostDetails: {
|
||||
...state.endpointDetails.hostDetails,
|
||||
details: action.payload.metadata,
|
||||
detailsLoading: false,
|
||||
detailsError: undefined,
|
||||
},
|
||||
},
|
||||
policyVersionInfo: action.payload.policy_info,
|
||||
hostStatus: action.payload.host_status,
|
||||
detailsLoading: false,
|
||||
detailsError: undefined,
|
||||
};
|
||||
} else if (action.type === 'serverFailedToReturnEndpointDetails') {
|
||||
return {
|
||||
...state,
|
||||
detailsError: action.payload,
|
||||
detailsLoading: false,
|
||||
endpointDetails: {
|
||||
...state.endpointDetails,
|
||||
hostDetails: {
|
||||
...state.endpointDetails.hostDetails,
|
||||
detailsError: action.payload,
|
||||
detailsLoading: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
} else if (action.type === 'endpointDetailsActivityLogChanged') {
|
||||
return handleEndpointDetailsActivityLogChanged(state, action);
|
||||
} else if (action.type === 'serverReturnedPoliciesForOnboarding') {
|
||||
return {
|
||||
...state,
|
||||
|
@ -221,7 +217,6 @@ export const endpointListReducer: ImmutableReducer<EndpointState, AppAction> = (
|
|||
const stateUpdates: Partial<EndpointState> = {
|
||||
location: action.payload,
|
||||
error: undefined,
|
||||
detailsError: undefined,
|
||||
policyResponseError: undefined,
|
||||
};
|
||||
|
||||
|
@ -239,6 +234,13 @@ export const endpointListReducer: ImmutableReducer<EndpointState, AppAction> = (
|
|||
return {
|
||||
...state,
|
||||
...stateUpdates,
|
||||
endpointDetails: {
|
||||
...state.endpointDetails,
|
||||
hostDetails: {
|
||||
...state.endpointDetails.hostDetails,
|
||||
detailsError: undefined,
|
||||
},
|
||||
},
|
||||
loading: true,
|
||||
policyItemsLoading: true,
|
||||
};
|
||||
|
@ -249,6 +251,14 @@ export const endpointListReducer: ImmutableReducer<EndpointState, AppAction> = (
|
|||
return {
|
||||
...state,
|
||||
...stateUpdates,
|
||||
endpointDetails: {
|
||||
...state.endpointDetails,
|
||||
hostDetails: {
|
||||
...state.endpointDetails.hostDetails,
|
||||
detailsLoading: true,
|
||||
detailsError: undefined,
|
||||
},
|
||||
},
|
||||
detailsLoading: true,
|
||||
policyResponseLoading: true,
|
||||
};
|
||||
|
@ -257,8 +267,15 @@ export const endpointListReducer: ImmutableReducer<EndpointState, AppAction> = (
|
|||
return {
|
||||
...state,
|
||||
...stateUpdates,
|
||||
endpointDetails: {
|
||||
...state.endpointDetails,
|
||||
hostDetails: {
|
||||
...state.endpointDetails.hostDetails,
|
||||
detailsLoading: true,
|
||||
detailsError: undefined,
|
||||
},
|
||||
},
|
||||
loading: true,
|
||||
detailsLoading: true,
|
||||
policyResponseLoading: true,
|
||||
policyItemsLoading: true,
|
||||
};
|
||||
|
@ -268,6 +285,13 @@ export const endpointListReducer: ImmutableReducer<EndpointState, AppAction> = (
|
|||
return {
|
||||
...state,
|
||||
...stateUpdates,
|
||||
endpointDetails: {
|
||||
...state.endpointDetails,
|
||||
hostDetails: {
|
||||
...state.endpointDetails.hostDetails,
|
||||
detailsError: undefined,
|
||||
},
|
||||
},
|
||||
endpointsExist: true,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -45,11 +45,16 @@ export const listLoading = (state: Immutable<EndpointState>): boolean => state.l
|
|||
|
||||
export const listError = (state: Immutable<EndpointState>) => state.error;
|
||||
|
||||
export const detailsData = (state: Immutable<EndpointState>) => state.details;
|
||||
export const detailsData = (state: Immutable<EndpointState>) =>
|
||||
state.endpointDetails.hostDetails.details;
|
||||
|
||||
export const detailsLoading = (state: Immutable<EndpointState>): boolean => state.detailsLoading;
|
||||
export const detailsLoading = (state: Immutable<EndpointState>): boolean =>
|
||||
state.endpointDetails.hostDetails.detailsLoading;
|
||||
|
||||
export const detailsError = (state: Immutable<EndpointState>) => state.detailsError;
|
||||
export const detailsError = (
|
||||
state: Immutable<EndpointState>
|
||||
): EndpointState['endpointDetails']['hostDetails']['detailsError'] =>
|
||||
state.endpointDetails.hostDetails.detailsError;
|
||||
|
||||
export const policyItems = (state: Immutable<EndpointState>) => state.policyItems;
|
||||
|
||||
|
@ -209,7 +214,12 @@ export const uiQueryParams: (
|
|||
|
||||
if (value !== undefined) {
|
||||
if (key === 'show') {
|
||||
if (value === 'policy_response' || value === 'details' || value === 'isolate') {
|
||||
if (
|
||||
value === 'policy_response' ||
|
||||
value === 'details' ||
|
||||
value === 'activity_log' ||
|
||||
value === 'isolate'
|
||||
) {
|
||||
data[key] = value;
|
||||
}
|
||||
} else {
|
||||
|
@ -240,6 +250,19 @@ export const showView: (
|
|||
return searchParams.show ?? 'details';
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns the selected endpoint's elastic agent Id
|
||||
* used for fetching endpoint actions log
|
||||
*/
|
||||
export const selectedAgent = (state: Immutable<EndpointState>): string => {
|
||||
const hostList = state.hosts;
|
||||
const { selected_endpoint: selectedEndpoint } = uiQueryParams(state);
|
||||
return (
|
||||
hostList.find((host) => host.metadata.agent.id === selectedEndpoint)?.metadata.elastic.agent
|
||||
.id || ''
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the Host Status which is connected the fleet agent
|
||||
*/
|
||||
|
@ -331,3 +354,27 @@ export const getIsolationRequestError: (
|
|||
return isolateHost.error;
|
||||
}
|
||||
});
|
||||
|
||||
export const getActivityLogData = (
|
||||
state: Immutable<EndpointState>
|
||||
): Immutable<EndpointState['endpointDetails']['activityLog']> => state.endpointDetails.activityLog;
|
||||
|
||||
export const getActivityLogRequestLoading: (
|
||||
state: Immutable<EndpointState>
|
||||
) => boolean = createSelector(getActivityLogData, (activityLog) =>
|
||||
isLoadingResourceState(activityLog)
|
||||
);
|
||||
|
||||
export const getActivityLogRequestLoaded: (
|
||||
state: Immutable<EndpointState>
|
||||
) => boolean = createSelector(getActivityLogData, (activityLog) =>
|
||||
isLoadedResourceState(activityLog)
|
||||
);
|
||||
|
||||
export const getActivityLogError: (
|
||||
state: Immutable<EndpointState>
|
||||
) => ServerApiError | undefined = createSelector(getActivityLogData, (activityLog) => {
|
||||
if (isFailedResourceState(activityLog)) {
|
||||
return activityLog.error;
|
||||
}
|
||||
});
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
PolicyData,
|
||||
MetadataQueryStrategyVersions,
|
||||
HostStatus,
|
||||
EndpointAction,
|
||||
HostIsolationResponse,
|
||||
} from '../../../../common/endpoint/types';
|
||||
import { ServerApiError } from '../../../common/types';
|
||||
|
@ -34,12 +35,17 @@ export interface EndpointState {
|
|||
loading: boolean;
|
||||
/** api error from retrieving host list */
|
||||
error?: ServerApiError;
|
||||
/** details data for a specific host */
|
||||
details?: Immutable<HostMetadata>;
|
||||
/** details page is retrieving data */
|
||||
detailsLoading: boolean;
|
||||
/** api error from retrieving host details */
|
||||
detailsError?: ServerApiError;
|
||||
endpointDetails: {
|
||||
activityLog: AsyncResourceState<EndpointAction[]>;
|
||||
hostDetails: {
|
||||
/** details data for a specific host */
|
||||
details?: Immutable<HostMetadata>;
|
||||
/** details page is retrieving data */
|
||||
detailsLoading: boolean;
|
||||
/** api error from retrieving host details */
|
||||
detailsError?: ServerApiError;
|
||||
};
|
||||
};
|
||||
/** Holds the Policy Response for the Host currently being displayed in the details */
|
||||
policyResponse?: HostPolicyResponse;
|
||||
/** policyResponse is being retrieved */
|
||||
|
@ -108,7 +114,7 @@ export interface EndpointIndexUIQueryParams {
|
|||
/** Which page to show */
|
||||
page_index?: string;
|
||||
/** show the policy response or host details */
|
||||
show?: 'policy_response' | 'details' | 'isolate';
|
||||
show?: 'policy_response' | 'activity_log' | 'details' | 'isolate';
|
||||
/** Query text from search bar*/
|
||||
admin_query?: string;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* 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 React, { memo, useCallback, useMemo, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { EuiTabbedContent, EuiTabbedContentTab } from '@elastic/eui';
|
||||
import { EndpointIndexUIQueryParams } from '../../../types';
|
||||
export enum EndpointDetailsTabsTypes {
|
||||
overview = 'overview',
|
||||
activityLog = 'activity_log',
|
||||
}
|
||||
|
||||
export type EndpointDetailsTabsId =
|
||||
| EndpointDetailsTabsTypes.overview
|
||||
| EndpointDetailsTabsTypes.activityLog;
|
||||
|
||||
interface EndpointDetailsTabs {
|
||||
id: string;
|
||||
name: string;
|
||||
content: JSX.Element;
|
||||
}
|
||||
|
||||
const StyledEuiTabbedContent = styled(EuiTabbedContent)`
|
||||
overflow: hidden;
|
||||
padding-bottom: ${(props) => props.theme.eui.paddingSizes.xl};
|
||||
|
||||
> [role='tabpanel'] {
|
||||
height: 100%;
|
||||
padding-right: 12px;
|
||||
overflow: hidden;
|
||||
overflow-y: auto;
|
||||
::-webkit-scrollbar {
|
||||
-webkit-appearance: none;
|
||||
width: 4px;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
border-radius: 2px;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
-webkit-box-shadow: 0 0 1px rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const EndpointDetailsFlyoutTabs = memo(
|
||||
({ show, tabs }: { show: EndpointIndexUIQueryParams['show']; tabs: EndpointDetailsTabs[] }) => {
|
||||
const [selectedTabId, setSelectedTabId] = useState<EndpointDetailsTabsId>(() => {
|
||||
return show === 'details'
|
||||
? EndpointDetailsTabsTypes.overview
|
||||
: EndpointDetailsTabsTypes.activityLog;
|
||||
});
|
||||
|
||||
const handleTabClick = useCallback(
|
||||
(tab: EuiTabbedContentTab) => setSelectedTabId(tab.id as EndpointDetailsTabsId),
|
||||
[setSelectedTabId]
|
||||
);
|
||||
|
||||
const selectedTab = useMemo(() => tabs.find((tab) => tab.id === selectedTabId), [
|
||||
tabs,
|
||||
selectedTabId,
|
||||
]);
|
||||
|
||||
return (
|
||||
<StyledEuiTabbedContent
|
||||
data-test-subj="endpointDetailsTabs"
|
||||
tabs={tabs}
|
||||
selectedTab={selectedTab}
|
||||
onTabClick={handleTabClick}
|
||||
key="endpoint-details-tabs"
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
EndpointDetailsFlyoutTabs.displayName = 'EndpointDetailsFlyoutTabs';
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* 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 React, { memo } from 'react';
|
||||
|
||||
import { EuiAvatar, EuiComment, EuiText } from '@elastic/eui';
|
||||
import { Immutable, EndpointAction } from '../../../../../../../common/endpoint/types';
|
||||
import { FormattedDateAndTime } from '../../../../../../common/components/endpoint/formatted_date_time';
|
||||
import { useEuiTheme } from '../../../../../../common/lib/theme/use_eui_theme';
|
||||
|
||||
export const LogEntry = memo(
|
||||
({ endpointAction }: { endpointAction: Immutable<EndpointAction> }) => {
|
||||
const euiTheme = useEuiTheme();
|
||||
const isIsolated = endpointAction?.data.command === 'isolate';
|
||||
|
||||
// do this better when we can distinguish between endpoint events vs user events
|
||||
const iconType = endpointAction.user_id === 'sys' ? 'dot' : isIsolated ? 'lock' : 'lockOpen';
|
||||
const commentType = endpointAction.user_id === 'sys' ? 'update' : 'regular';
|
||||
const timelineIcon = (
|
||||
<EuiAvatar
|
||||
name="Timeline Icon"
|
||||
size={endpointAction.user_id === 'sys' ? 's' : 'm'}
|
||||
color={euiTheme.euiColorLightestShade}
|
||||
iconColor="subdued"
|
||||
iconType={iconType}
|
||||
/>
|
||||
);
|
||||
const event = `${isIsolated ? 'isolated' : 'unisolated'} host`;
|
||||
const hasComment = !!endpointAction.data.comment;
|
||||
|
||||
return (
|
||||
<EuiComment
|
||||
type={commentType}
|
||||
username={endpointAction.user_id}
|
||||
timestamp={FormattedDateAndTime({
|
||||
date: new Date(endpointAction['@timestamp']),
|
||||
showRelativeTime: true,
|
||||
})}
|
||||
event={event}
|
||||
timelineIcon={timelineIcon}
|
||||
data-test-subj="timelineEntry"
|
||||
>
|
||||
{hasComment ? (
|
||||
<EuiText size="s">
|
||||
<p>{endpointAction.data.comment}</p>
|
||||
</EuiText>
|
||||
) : undefined}
|
||||
</EuiComment>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
LogEntry.displayName = 'LogEntry';
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* 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 React, { memo, useCallback } from 'react';
|
||||
|
||||
import { EuiEmptyPrompt, EuiSpacer } from '@elastic/eui';
|
||||
import { LogEntry } from './components/log_entry';
|
||||
import * as i18 from '../translations';
|
||||
import { SearchBar } from '../../../../components/search_bar';
|
||||
import { Immutable, EndpointAction } from '../../../../../../common/endpoint/types';
|
||||
import { AsyncResourceState } from '../../../../state';
|
||||
|
||||
export const EndpointActivityLog = memo(
|
||||
({ endpointActions }: { endpointActions: AsyncResourceState<Immutable<EndpointAction[]>> }) => {
|
||||
// TODO
|
||||
const onSearch = useCallback(() => {}, []);
|
||||
return (
|
||||
<>
|
||||
<EuiSpacer size="l" />
|
||||
{endpointActions.type !== 'LoadedResourceState' || !endpointActions.data.length ? (
|
||||
<EuiEmptyPrompt
|
||||
iconType="editorUnorderedList"
|
||||
titleSize="s"
|
||||
title={<h2>{'No logged actions'}</h2>}
|
||||
body={<p>{'No actions have been logged for this endpoint.'}</p>}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<SearchBar onSearch={onSearch} placeholder={i18.SEARCH_ACTIVITY_LOG} />
|
||||
<EuiSpacer size="l" />
|
||||
{endpointActions.data.map((endpointAction) => (
|
||||
<LogEntry key={endpointAction.action_id} endpointAction={endpointAction} />
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
EndpointActivityLog.displayName = 'EndpointActivityLog';
|
|
@ -258,6 +258,7 @@ export const EndpointDetails = memo(
|
|||
|
||||
return (
|
||||
<>
|
||||
<EuiSpacer size="l" />
|
||||
<EuiDescriptionList
|
||||
type="column"
|
||||
listItems={detailsResultsUpper}
|
||||
|
|
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
* 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 React, { ComponentType } from 'react';
|
||||
import moment from 'moment';
|
||||
|
||||
import { EndpointAction, Immutable } from '../../../../../../common/endpoint/types';
|
||||
import { EndpointDetailsFlyoutTabs } from './components/endpoint_details_tabs';
|
||||
import { EndpointActivityLog } from './endpoint_activity_log';
|
||||
import { EndpointDetailsFlyout } from '.';
|
||||
import { EuiThemeProvider } from '../../../../../../../../../src/plugins/kibana_react/common';
|
||||
import { AsyncResourceState } from '../../../../state';
|
||||
|
||||
export const dummyEndpointActivityLog = (
|
||||
selectedEndpoint: string = ''
|
||||
): AsyncResourceState<Immutable<EndpointAction[]>> => ({
|
||||
type: 'LoadedResourceState',
|
||||
data: [
|
||||
{
|
||||
action_id: '1',
|
||||
'@timestamp': moment().subtract(1, 'hours').fromNow().toString(),
|
||||
expiration: moment().add(3, 'day').fromNow().toString(),
|
||||
type: 'INPUT_ACTION',
|
||||
input_type: 'endpoint',
|
||||
agents: [`${selectedEndpoint}`],
|
||||
user_id: 'sys',
|
||||
data: {
|
||||
command: 'isolate',
|
||||
},
|
||||
},
|
||||
{
|
||||
action_id: '2',
|
||||
'@timestamp': moment().subtract(2, 'hours').fromNow().toString(),
|
||||
expiration: moment().add(1, 'day').fromNow().toString(),
|
||||
type: 'INPUT_ACTION',
|
||||
input_type: 'endpoint',
|
||||
agents: [`${selectedEndpoint}`],
|
||||
user_id: 'ash',
|
||||
data: {
|
||||
command: 'isolate',
|
||||
comment: 'Sem et tortor consequat id porta nibh venenatis cras sed.',
|
||||
},
|
||||
},
|
||||
{
|
||||
action_id: '3',
|
||||
'@timestamp': moment().subtract(4, 'hours').fromNow().toString(),
|
||||
expiration: moment().add(1, 'day').fromNow().toString(),
|
||||
type: 'INPUT_ACTION',
|
||||
input_type: 'endpoint',
|
||||
agents: [`${selectedEndpoint}`],
|
||||
user_id: 'someone',
|
||||
data: {
|
||||
command: 'unisolate',
|
||||
comment: 'Turpis egestas pretium aenean pharetra.',
|
||||
},
|
||||
},
|
||||
{
|
||||
action_id: '4',
|
||||
'@timestamp': moment().subtract(1, 'day').fromNow().toString(),
|
||||
expiration: moment().add(3, 'day').fromNow().toString(),
|
||||
type: 'INPUT_ACTION',
|
||||
input_type: 'endpoint',
|
||||
agents: [`${selectedEndpoint}`],
|
||||
user_id: 'ash',
|
||||
data: {
|
||||
command: 'isolate',
|
||||
comment:
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, \
|
||||
sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export default {
|
||||
title: 'Endpoints/Endpoint Details',
|
||||
component: EndpointDetailsFlyout,
|
||||
decorators: [
|
||||
(Story: ComponentType) => (
|
||||
<EuiThemeProvider>
|
||||
<Story />
|
||||
</EuiThemeProvider>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export const Tabs = () => (
|
||||
<EndpointDetailsFlyoutTabs
|
||||
show="details"
|
||||
tabs={[
|
||||
{
|
||||
id: 'overview',
|
||||
name: 'Overview',
|
||||
content: <>{'Endpoint Details'}</>,
|
||||
},
|
||||
{
|
||||
id: 'activity_log',
|
||||
name: 'Activity Log',
|
||||
content: ActivityLog(),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
|
||||
export const ActivityLog = () => (
|
||||
<EndpointActivityLog endpointActions={dummyEndpointActivityLog()} />
|
||||
);
|
|
@ -5,7 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, memo } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, memo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import {
|
||||
EuiFlyout,
|
||||
EuiFlyoutBody,
|
||||
|
@ -16,6 +17,8 @@ import {
|
|||
EuiSpacer,
|
||||
EuiEmptyPrompt,
|
||||
EuiToolTip,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
} from '@elastic/eui';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
@ -26,8 +29,11 @@ import {
|
|||
uiQueryParams,
|
||||
detailsData,
|
||||
detailsError,
|
||||
showView,
|
||||
detailsLoading,
|
||||
getActivityLogData,
|
||||
getActivityLogError,
|
||||
getActivityLogRequestLoading,
|
||||
showView,
|
||||
policyResponseConfigurations,
|
||||
policyResponseActions,
|
||||
policyResponseFailedOrWarningActionCount,
|
||||
|
@ -39,14 +45,36 @@ import {
|
|||
policyResponseAppliedRevision,
|
||||
} from '../../store/selectors';
|
||||
import { EndpointDetails } from './endpoint_details';
|
||||
import { EndpointActivityLog } from './endpoint_activity_log';
|
||||
import { PolicyResponse } from './policy_response';
|
||||
import * as i18 from '../translations';
|
||||
import { HostMetadata } from '../../../../../../common/endpoint/types';
|
||||
import {
|
||||
EndpointDetailsFlyoutTabs,
|
||||
EndpointDetailsTabsTypes,
|
||||
} from './components/endpoint_details_tabs';
|
||||
|
||||
import { PreferenceFormattedDateFromPrimitive } from '../../../../../common/components/formatted_date';
|
||||
import { EndpointIsolateFlyoutPanel } from './components/endpoint_isolate_flyout_panel';
|
||||
import { BackToEndpointDetailsFlyoutSubHeader } from './components/back_to_endpoint_details_flyout_subheader';
|
||||
import { FlyoutBodyNoTopPadding } from './components/flyout_body_no_top_padding';
|
||||
import { getEndpointListPath } from '../../../../common/routing';
|
||||
|
||||
const DetailsFlyoutBody = styled(EuiFlyoutBody)`
|
||||
overflow-y: hidden;
|
||||
flex: 1;
|
||||
|
||||
.euiFlyoutBody__overflow {
|
||||
overflow: hidden;
|
||||
mask-image: none;
|
||||
}
|
||||
|
||||
.euiFlyoutBody__overflowContent {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
}
|
||||
`;
|
||||
|
||||
export const EndpointDetailsFlyout = memo(() => {
|
||||
const history = useHistory();
|
||||
const toasts = useToasts();
|
||||
|
@ -55,13 +83,51 @@ export const EndpointDetailsFlyout = memo(() => {
|
|||
selected_endpoint: selectedEndpoint,
|
||||
...queryParamsWithoutSelectedEndpoint
|
||||
} = queryParams;
|
||||
const details = useEndpointSelector(detailsData);
|
||||
|
||||
const activityLog = useEndpointSelector(getActivityLogData);
|
||||
const activityLoading = useEndpointSelector(getActivityLogRequestLoading);
|
||||
const activityError = useEndpointSelector(getActivityLogError);
|
||||
const hostDetails = useEndpointSelector(detailsData);
|
||||
const hostDetailsLoading = useEndpointSelector(detailsLoading);
|
||||
const hostDetailsError = useEndpointSelector(detailsError);
|
||||
|
||||
const policyInfo = useEndpointSelector(policyVersionInfo);
|
||||
const hostStatus = useEndpointSelector(hostStatusInfo);
|
||||
const loading = useEndpointSelector(detailsLoading);
|
||||
const error = useEndpointSelector(detailsError);
|
||||
const show = useEndpointSelector(showView);
|
||||
|
||||
const ContentLoadingMarkup = useMemo(
|
||||
() => (
|
||||
<>
|
||||
<EuiLoadingContent lines={3} />
|
||||
<EuiSpacer size="l" />
|
||||
<EuiLoadingContent lines={3} />
|
||||
</>
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
id: EndpointDetailsTabsTypes.overview,
|
||||
name: i18.OVERVIEW,
|
||||
content:
|
||||
hostDetails === undefined ? (
|
||||
ContentLoadingMarkup
|
||||
) : (
|
||||
<EndpointDetails details={hostDetails} policyInfo={policyInfo} hostStatus={hostStatus} />
|
||||
),
|
||||
},
|
||||
{
|
||||
id: EndpointDetailsTabsTypes.activityLog,
|
||||
name: i18.ACTIVITY_LOG,
|
||||
content: activityLoading ? (
|
||||
ContentLoadingMarkup
|
||||
) : (
|
||||
<EndpointActivityLog endpointActions={activityLog} />
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const handleFlyoutClose = useCallback(() => {
|
||||
const { show: _show, ...urlSearchParams } = queryParamsWithoutSelectedEndpoint;
|
||||
history.push(
|
||||
|
@ -73,7 +139,7 @@ export const EndpointDetailsFlyout = memo(() => {
|
|||
}, [history, queryParamsWithoutSelectedEndpoint]);
|
||||
|
||||
useEffect(() => {
|
||||
if (error !== undefined) {
|
||||
if (hostDetailsError !== undefined) {
|
||||
toasts.addDanger({
|
||||
title: i18n.translate('xpack.securitySolution.endpoint.details.errorTitle', {
|
||||
defaultMessage: 'Could not find host',
|
||||
|
@ -83,7 +149,17 @@ export const EndpointDetailsFlyout = memo(() => {
|
|||
}),
|
||||
});
|
||||
}
|
||||
}, [error, toasts]);
|
||||
if (activityError !== undefined) {
|
||||
toasts.addDanger({
|
||||
title: i18n.translate('xpack.securitySolution.endpoint.activityLog.errorTitle', {
|
||||
defaultMessage: 'Could not find activity log for host',
|
||||
}),
|
||||
text: i18n.translate('xpack.securitySolution.endpoint.activityLog.errorBody', {
|
||||
defaultMessage: 'Please exit the flyout and select another host with actions.',
|
||||
}),
|
||||
});
|
||||
}
|
||||
}, [hostDetailsError, activityError, toasts]);
|
||||
|
||||
return (
|
||||
<EuiFlyout
|
||||
|
@ -91,38 +167,43 @@ export const EndpointDetailsFlyout = memo(() => {
|
|||
style={{ zIndex: 4001 }}
|
||||
data-test-subj="endpointDetailsFlyout"
|
||||
size="m"
|
||||
paddingSize="m"
|
||||
>
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
{loading ? (
|
||||
{hostDetailsLoading || activityLoading ? (
|
||||
<EuiLoadingContent lines={1} />
|
||||
) : (
|
||||
<EuiToolTip content={details?.host?.hostname} anchorClassName="eui-textTruncate">
|
||||
<EuiToolTip content={hostDetails?.host?.hostname} anchorClassName="eui-textTruncate">
|
||||
<EuiTitle size="s">
|
||||
<h2
|
||||
style={{ overflow: 'hidden', textOverflow: 'ellipsis' }}
|
||||
data-test-subj="endpointDetailsFlyoutTitle"
|
||||
>
|
||||
{details?.host?.hostname}
|
||||
{hostDetails?.host?.hostname}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiToolTip>
|
||||
)}
|
||||
</EuiFlyoutHeader>
|
||||
{details === undefined ? (
|
||||
{hostDetails === undefined ? (
|
||||
<EuiFlyoutBody>
|
||||
<EuiLoadingContent lines={3} /> <EuiSpacer size="l" /> <EuiLoadingContent lines={3} />
|
||||
</EuiFlyoutBody>
|
||||
) : (
|
||||
<>
|
||||
{show === 'details' && (
|
||||
<EuiFlyoutBody data-test-subj="endpointDetailsFlyoutBody">
|
||||
<EndpointDetails details={details} policyInfo={policyInfo} hostStatus={hostStatus} />
|
||||
</EuiFlyoutBody>
|
||||
{(show === 'details' || show === 'activity_log') && (
|
||||
<DetailsFlyoutBody data-test-subj="endpointDetailsFlyoutBody">
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EndpointDetailsFlyoutTabs show={show} tabs={tabs} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</DetailsFlyoutBody>
|
||||
)}
|
||||
|
||||
{show === 'policy_response' && <PolicyResponseFlyoutPanel hostMeta={details} />}
|
||||
{show === 'policy_response' && <PolicyResponseFlyoutPanel hostMeta={hostDetails} />}
|
||||
|
||||
{show === 'isolate' && <EndpointIsolateFlyoutPanel hostMeta={details} />}
|
||||
{show === 'isolate' && <EndpointIsolateFlyoutPanel hostMeta={hostDetails} />}
|
||||
</>
|
||||
)}
|
||||
</EuiFlyout>
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const OVERVIEW = i18n.translate('xpack.securitySolution.endpointDetails.overview', {
|
||||
defaultMessage: 'Overview',
|
||||
});
|
||||
|
||||
export const ACTIVITY_LOG = i18n.translate('xpack.securitySolution.endpointDetails.activityLog', {
|
||||
defaultMessage: 'Activity Log',
|
||||
});
|
||||
|
||||
export const SEARCH_ACTIVITY_LOG = i18n.translate(
|
||||
'xpack.securitySolution.endpointDetails.activityLog.search',
|
||||
{
|
||||
defaultMessage: 'Search activity log',
|
||||
}
|
||||
);
|
|
@ -30,7 +30,7 @@ import {
|
|||
GetOneTrustedAppResponse,
|
||||
} from '../../../../../common/endpoint/types/trusted_apps';
|
||||
|
||||
import { resolvePathVariables } from './utils';
|
||||
import { resolvePathVariables } from '../../../common/utils';
|
||||
import { sendGetEndpointSpecificPackagePolicies } from '../../policy/store/services/ingest';
|
||||
|
||||
export interface TrustedAppsService {
|
||||
|
|
|
@ -1,45 +0,0 @@
|
|||
/*
|
||||
* 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 { resolvePathVariables } from './utils';
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,11 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
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);
|
|
@ -31,7 +31,7 @@ import {
|
|||
import { EndpointDocGenerator } from '../../../../../common/endpoint/generate_data';
|
||||
import { isFailedResourceState, isLoadedResourceState } from '../state';
|
||||
import { forceHTMLElementOffsetWidth } from './components/effected_policy_select/test_utils';
|
||||
import { resolvePathVariables } from '../service/utils';
|
||||
import { resolvePathVariables } from '../../../common/utils';
|
||||
import { toUpdateTrustedApp } from '../../../../../common/endpoint/service/trusted_apps/to_update_trusted_app';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
|
||||
|
||||
|
|
|
@ -19,14 +19,12 @@ import {
|
|||
import { ImmutableCombineReducers } from '../../common/store';
|
||||
import { Immutable } from '../../../common/endpoint/types';
|
||||
import { ManagementState } from '../types';
|
||||
import {
|
||||
endpointListReducer,
|
||||
initialEndpointListState,
|
||||
} from '../pages/endpoint_hosts/store/reducer';
|
||||
import { endpointListReducer } from '../pages/endpoint_hosts/store/reducer';
|
||||
import { initialTrustedAppsPageState } from '../pages/trusted_apps/store/builders';
|
||||
import { trustedAppsPageReducer } from '../pages/trusted_apps/store/reducer';
|
||||
import { initialEventFiltersPageState } from '../pages/event_filters/store/builders';
|
||||
import { eventFiltersPageReducer } from '../pages/event_filters/store/reducer';
|
||||
import { initialEndpointPageState } from '../pages/endpoint_hosts/store/builders';
|
||||
|
||||
const immutableCombineReducers: ImmutableCombineReducers = combineReducers;
|
||||
|
||||
|
@ -35,7 +33,7 @@ const immutableCombineReducers: ImmutableCombineReducers = combineReducers;
|
|||
*/
|
||||
export const mockManagementState: Immutable<ManagementState> = {
|
||||
[MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE]: initialPolicyDetailsState(),
|
||||
[MANAGEMENT_STORE_ENDPOINTS_NAMESPACE]: initialEndpointListState,
|
||||
[MANAGEMENT_STORE_ENDPOINTS_NAMESPACE]: initialEndpointPageState(),
|
||||
[MANAGEMENT_STORE_TRUSTED_APPS_NAMESPACE]: initialTrustedAppsPageState(),
|
||||
[MANAGEMENT_STORE_EVENT_FILTERS_NAMESPACE]: initialEventFiltersPageState(),
|
||||
};
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* 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 { ENDPOINT_ACTION_LOG_ROUTE } from '../../../../common/endpoint/constants';
|
||||
import { EndpointActionLogRequestSchema } from '../../../../common/endpoint/schema/actions';
|
||||
import { actionsLogRequestHandler } from './audit_log_handler';
|
||||
|
||||
import { SecuritySolutionPluginRouter } from '../../../types';
|
||||
import { EndpointAppContext } from '../../types';
|
||||
|
||||
/**
|
||||
* Registers the endpoint activity_log route
|
||||
*/
|
||||
export function registerActionAuditLogRoutes(
|
||||
router: SecuritySolutionPluginRouter,
|
||||
endpointContext: EndpointAppContext
|
||||
) {
|
||||
router.get(
|
||||
{
|
||||
path: ENDPOINT_ACTION_LOG_ROUTE,
|
||||
validate: EndpointActionLogRequestSchema,
|
||||
options: { authRequired: true, tags: ['access:securitySolution'] },
|
||||
},
|
||||
actionsLogRequestHandler(endpointContext)
|
||||
);
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* 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 { TypeOf } from '@kbn/config-schema';
|
||||
import { RequestHandler } from 'kibana/server';
|
||||
import { AGENT_ACTIONS_INDEX } from '../../../../../fleet/common';
|
||||
import { EndpointActionLogRequestSchema } from '../../../../common/endpoint/schema/actions';
|
||||
|
||||
import { SecuritySolutionRequestHandlerContext } from '../../../types';
|
||||
import { EndpointAppContext } from '../../types';
|
||||
|
||||
export const actionsLogRequestHandler = (
|
||||
endpointContext: EndpointAppContext
|
||||
): RequestHandler<
|
||||
TypeOf<typeof EndpointActionLogRequestSchema.params>,
|
||||
unknown,
|
||||
unknown,
|
||||
SecuritySolutionRequestHandlerContext
|
||||
> => {
|
||||
const logger = endpointContext.logFactory.get('audit_log');
|
||||
return async (context, req, res) => {
|
||||
const esClient = context.core.elasticsearch.client.asCurrentUser;
|
||||
let result;
|
||||
try {
|
||||
result = await esClient.search({
|
||||
index: AGENT_ACTIONS_INDEX,
|
||||
body: {
|
||||
query: {
|
||||
match: {
|
||||
agents: req.params.agent_id,
|
||||
},
|
||||
},
|
||||
sort: [
|
||||
{
|
||||
'@timestamp': {
|
||||
order: 'desc',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
throw error;
|
||||
}
|
||||
if (result?.statusCode !== 200) {
|
||||
logger.error(`Error fetching actions log for agent_id ${req.params.agent_id}`);
|
||||
throw new Error(`Error fetching actions log for agent_id ${req.params.agent_id}`);
|
||||
}
|
||||
|
||||
return res.ok({
|
||||
body: result.body.hits.hits.map((e) => e._source),
|
||||
});
|
||||
};
|
||||
};
|
|
@ -6,3 +6,4 @@
|
|||
*/
|
||||
|
||||
export * from './isolation';
|
||||
export * from './audit_log';
|
||||
|
|
|
@ -75,7 +75,10 @@ import { registerEndpointRoutes } from './endpoint/routes/metadata';
|
|||
import { registerLimitedConcurrencyRoutes } from './endpoint/routes/limited_concurrency';
|
||||
import { registerResolverRoutes } from './endpoint/routes/resolver';
|
||||
import { registerPolicyRoutes } from './endpoint/routes/policy';
|
||||
import { registerHostIsolationRoutes } from './endpoint/routes/actions';
|
||||
import {
|
||||
registerHostIsolationRoutes,
|
||||
registerActionAuditLogRoutes,
|
||||
} from './endpoint/routes/actions';
|
||||
import { EndpointArtifactClient, ManifestManager } from './endpoint/services';
|
||||
import { EndpointAppContextService } from './endpoint/endpoint_app_context_services';
|
||||
import { EndpointAppContext } from './endpoint/types';
|
||||
|
@ -291,6 +294,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
|
|||
registerPolicyRoutes(router, endpointContext);
|
||||
registerTrustedAppsRoutes(router, endpointContext);
|
||||
registerHostIsolationRoutes(router, endpointContext);
|
||||
registerActionAuditLogRoutes(router, endpointContext);
|
||||
|
||||
const referenceRuleTypes = [
|
||||
REFERENCE_RULE_ALERT_TYPE_ID,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue