mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Security Solution] [Endpoint] Allow filtering activity log with date range (#104085)
* use date range in search query
fixes elastic/security-team/issues/1137
* make any date selection fetch matching log
fixes elastic/security-team/issues/1137
* use a single action for updating paging info and fetching data
fixes elastic/security-team/issues/1137
* use consistent types
for some reason TS was complaining earlier with `undefined`
* reset date picker on tab load
fixes elastic/security-team/issues/1137
* refactor date pickers into a component
refs elastic/security-team/issues/1137
* clear dates on change of endpoint
fixes elastic/security-team/issues/1137
* do not show empty state if date filtering results return empty data
fixes elastic/security-team/issues/1137
* add tests
fixes elastic/security-team/issues/1137
* review changes
* update comment
refs f551b67d66
* store invalidDateRange on redux store and decouple logic from the component
review changes
* fix test
* fix lint
* review changes
* expand date picker to use the full width of the flyout
review changes
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
facaeb7d65
commit
81f09a863d
18 changed files with 416 additions and 67 deletions
|
@ -23,6 +23,8 @@ export const EndpointActionLogRequestSchema = {
|
|||
query: schema.object({
|
||||
page: schema.number({ defaultValue: 1, min: 1 }),
|
||||
page_size: schema.number({ defaultValue: 10, min: 1, max: 100 }),
|
||||
start_date: schema.maybe(schema.string()),
|
||||
end_date: schema.maybe(schema.string()),
|
||||
}),
|
||||
params: schema.object({
|
||||
agent_id: schema.string(),
|
||||
|
|
|
@ -60,6 +60,8 @@ export type ActivityLogEntry = ActivityLogAction | ActivityLogActionResponse;
|
|||
export interface ActivityLog {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
data: ActivityLogEntry[];
|
||||
}
|
||||
|
||||
|
|
|
@ -146,13 +146,6 @@ export type EndpointIsolationRequestStateChange = Action<'endpointIsolationReque
|
|||
payload: EndpointState['isolationRequestState'];
|
||||
};
|
||||
|
||||
export interface AppRequestedEndpointActivityLog {
|
||||
type: 'appRequestedEndpointActivityLog';
|
||||
payload: {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
};
|
||||
}
|
||||
export type EndpointDetailsActivityLogChanged = Action<'endpointDetailsActivityLogChanged'> & {
|
||||
payload: EndpointState['endpointDetails']['activityLog']['logData'];
|
||||
};
|
||||
|
@ -165,9 +158,18 @@ export interface EndpointDetailsActivityLogUpdatePaging {
|
|||
type: 'endpointDetailsActivityLogUpdatePaging';
|
||||
payload: {
|
||||
// disable paging when no more data after paging
|
||||
disabled: boolean;
|
||||
disabled?: boolean;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface EndpointDetailsActivityLogUpdateIsInvalidDateRange {
|
||||
type: 'endpointDetailsActivityLogUpdateIsInvalidDateRange';
|
||||
payload: {
|
||||
isInvalidDateRange?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -181,8 +183,8 @@ export type EndpointAction =
|
|||
| ServerFailedToReturnEndpointList
|
||||
| ServerReturnedEndpointDetails
|
||||
| ServerFailedToReturnEndpointDetails
|
||||
| AppRequestedEndpointActivityLog
|
||||
| EndpointDetailsActivityLogUpdatePaging
|
||||
| EndpointDetailsActivityLogUpdateIsInvalidDateRange
|
||||
| EndpointDetailsFlyoutTabChanged
|
||||
| EndpointDetailsActivityLogChanged
|
||||
| ServerReturnedEndpointPolicyResponse
|
||||
|
|
|
@ -25,6 +25,9 @@ export const initialEndpointPageState = (): Immutable<EndpointState> => {
|
|||
disabled: false,
|
||||
page: 1,
|
||||
pageSize: 50,
|
||||
startDate: undefined,
|
||||
endDate: undefined,
|
||||
isInvalidDateRange: false,
|
||||
},
|
||||
logData: createUninitialisedResourceState(),
|
||||
},
|
||||
|
|
|
@ -48,6 +48,7 @@ describe('EndpointList store concerns', () => {
|
|||
disabled: false,
|
||||
page: 1,
|
||||
pageSize: 50,
|
||||
isInvalidDateRange: false,
|
||||
},
|
||||
logData: { type: 'UninitialisedResourceState' },
|
||||
},
|
||||
|
|
|
@ -65,6 +65,7 @@ import { resolvePathVariables } from '../../../../common/utils/resolve_path_vari
|
|||
import { EndpointPackageInfoStateChanged } from './action';
|
||||
import { fetchPendingActionsByAgentId } from '../../../../common/lib/endpoint_pending_actions';
|
||||
import { EndpointDetailsTabsTypes } from '../view/details/components/endpoint_details_tabs';
|
||||
import { getIsInvalidDateRange } from '../utils';
|
||||
|
||||
type EndpointPageStore = ImmutableMiddlewareAPI<EndpointState, AppAction>;
|
||||
|
||||
|
@ -400,21 +401,50 @@ export const endpointMiddlewareFactory: ImmutableMiddlewareFactory<EndpointState
|
|||
}
|
||||
|
||||
// page activity log API
|
||||
if (action.type === 'appRequestedEndpointActivityLog' && hasSelectedEndpoint(getState())) {
|
||||
dispatch({
|
||||
type: 'endpointDetailsActivityLogChanged',
|
||||
// ts error to be fixed when AsyncResourceState is refactored (#830)
|
||||
// @ts-expect-error
|
||||
payload: createLoadingResourceState<ActivityLog>(getActivityLogData(getState())),
|
||||
});
|
||||
|
||||
if (
|
||||
action.type === 'endpointDetailsActivityLogUpdatePaging' &&
|
||||
hasSelectedEndpoint(getState())
|
||||
) {
|
||||
try {
|
||||
const { page, pageSize } = getActivityLogDataPaging(getState());
|
||||
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 },
|
||||
query: {
|
||||
page,
|
||||
page_size: pageSize,
|
||||
start_date: startDate,
|
||||
end_date: endDate,
|
||||
},
|
||||
});
|
||||
|
||||
const lastLoadedLogData = getLastLoadedActivityLogData(getState());
|
||||
|
@ -428,6 +458,8 @@ export const endpointMiddlewareFactory: ImmutableMiddlewareFactory<EndpointState
|
|||
const updatedLogData = {
|
||||
page: activityLog.page,
|
||||
pageSize: activityLog.pageSize,
|
||||
startDate: activityLog.startDate,
|
||||
endDate: activityLog.endDate,
|
||||
data: activityLog.page === 1 ? activityLog.data : updatedLogDataItems,
|
||||
};
|
||||
dispatch({
|
||||
|
@ -439,8 +471,10 @@ export const endpointMiddlewareFactory: ImmutableMiddlewareFactory<EndpointState
|
|||
type: 'endpointDetailsActivityLogUpdatePaging',
|
||||
payload: {
|
||||
disabled: true,
|
||||
page: activityLog.page - 1,
|
||||
page: activityLog.page > 1 ? activityLog.page - 1 : 1,
|
||||
pageSize: activityLog.pageSize,
|
||||
startDate: activityLog.startDate,
|
||||
endDate: activityLog.endDate,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -41,6 +41,8 @@ const handleEndpointDetailsActivityLogChanged: CaseReducer<EndpointDetailsActivi
|
|||
...state.endpointDetails.activityLog.paging,
|
||||
page: action.payload.data.page,
|
||||
pageSize: action.payload.data.pageSize,
|
||||
startDate: action.payload.data.startDate,
|
||||
endDate: action.payload.data.endDate,
|
||||
},
|
||||
}
|
||||
: { ...state.endpointDetails.activityLog };
|
||||
|
@ -162,33 +164,31 @@ export const endpointListReducer: StateReducer = (state = initialEndpointPageSta
|
|||
},
|
||||
},
|
||||
};
|
||||
} else if (action.type === 'appRequestedEndpointActivityLog') {
|
||||
const paging = {
|
||||
disabled: state.endpointDetails.activityLog.paging.disabled,
|
||||
page: action.payload.page,
|
||||
pageSize: action.payload.pageSize,
|
||||
};
|
||||
} else if (action.type === 'endpointDetailsActivityLogUpdatePaging') {
|
||||
return {
|
||||
...state,
|
||||
endpointDetails: {
|
||||
...state.endpointDetails!,
|
||||
activityLog: {
|
||||
...state.endpointDetails.activityLog,
|
||||
paging,
|
||||
paging: {
|
||||
...state.endpointDetails.activityLog.paging,
|
||||
...action.payload,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
} else if (action.type === 'endpointDetailsActivityLogUpdatePaging') {
|
||||
const paging = {
|
||||
...action.payload,
|
||||
};
|
||||
} else if (action.type === 'endpointDetailsActivityLogUpdateIsInvalidDateRange') {
|
||||
return {
|
||||
...state,
|
||||
endpointDetails: {
|
||||
...state.endpointDetails!,
|
||||
activityLog: {
|
||||
...state.endpointDetails.activityLog,
|
||||
paging,
|
||||
paging: {
|
||||
...state.endpointDetails.activityLog.paging,
|
||||
...action.payload,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -304,6 +304,7 @@ export const endpointListReducer: StateReducer = (state = initialEndpointPageSta
|
|||
disabled: false,
|
||||
page: 1,
|
||||
pageSize: 50,
|
||||
isInvalidDateRange: false,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -40,9 +40,12 @@ export interface EndpointState {
|
|||
flyoutView: EndpointIndexUIQueryParams['show'];
|
||||
activityLog: {
|
||||
paging: {
|
||||
disabled: boolean;
|
||||
disabled?: boolean;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
isInvalidDateRange: boolean;
|
||||
};
|
||||
logData: AsyncResourceState<ActivityLog>;
|
||||
};
|
||||
|
|
|
@ -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 moment from 'moment';
|
||||
import { getIsInvalidDateRange } from './utils';
|
||||
|
||||
describe('utils', () => {
|
||||
describe('getIsInvalidDateRange', () => {
|
||||
it('should return FALSE when either dates are undefined', () => {
|
||||
expect(getIsInvalidDateRange({})).toBe(false);
|
||||
expect(getIsInvalidDateRange({ startDate: moment().subtract(1, 'd').toISOString() })).toBe(
|
||||
false
|
||||
);
|
||||
expect(getIsInvalidDateRange({ endDate: moment().toISOString() })).toBe(false);
|
||||
});
|
||||
|
||||
it('should return TRUE when startDate is after endDate', () => {
|
||||
expect(
|
||||
getIsInvalidDateRange({
|
||||
startDate: moment().toISOString(),
|
||||
endDate: moment().subtract(1, 'd').toISOString(),
|
||||
})
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import moment from 'moment';
|
||||
import { HostInfo, HostMetadata } from '../../../../common/endpoint/types';
|
||||
|
||||
export const isPolicyOutOfDate = (
|
||||
|
@ -23,3 +24,18 @@ export const isPolicyOutOfDate = (
|
|||
reported.endpoint_policy_version >= current.endpoint.revision
|
||||
);
|
||||
};
|
||||
|
||||
export const getIsInvalidDateRange = ({
|
||||
startDate,
|
||||
endDate,
|
||||
}: {
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
}) => {
|
||||
if (startDate && endDate) {
|
||||
const start = moment(startDate);
|
||||
const end = moment(endDate);
|
||||
return start.isAfter(end);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
|
|
@ -0,0 +1,127 @@
|
|||
/*
|
||||
* 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 { useDispatch } from 'react-redux';
|
||||
import React, { memo, useCallback } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import moment from 'moment';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiDatePicker, EuiDatePickerRange } from '@elastic/eui';
|
||||
|
||||
import * as i18 from '../../../translations';
|
||||
import { useEndpointSelector } from '../../../hooks';
|
||||
import { getActivityLogDataPaging } from '../../../../store/selectors';
|
||||
|
||||
const DatePickerWrapper = styled.div`
|
||||
width: ${(props) => props.theme.eui.fractions.single.percentage};
|
||||
background: white;
|
||||
`;
|
||||
const StickyFlexItem = styled(EuiFlexItem)`
|
||||
position: sticky;
|
||||
top: ${(props) => props.theme.eui.euiSizeM};
|
||||
z-index: 1;
|
||||
`;
|
||||
|
||||
export const DateRangePicker = memo(() => {
|
||||
const dispatch = useDispatch();
|
||||
const { page, pageSize, startDate, endDate, isInvalidDateRange } = useEndpointSelector(
|
||||
getActivityLogDataPaging
|
||||
);
|
||||
|
||||
const onClear = useCallback(
|
||||
({ clearStart = false, clearEnd = false }: { clearStart?: boolean; clearEnd?: boolean }) => {
|
||||
dispatch({
|
||||
type: 'endpointDetailsActivityLogUpdatePaging',
|
||||
payload: {
|
||||
disabled: false,
|
||||
page,
|
||||
pageSize,
|
||||
startDate: clearStart ? undefined : startDate,
|
||||
endDate: clearEnd ? undefined : endDate,
|
||||
},
|
||||
});
|
||||
},
|
||||
[dispatch, endDate, startDate, page, pageSize]
|
||||
);
|
||||
|
||||
const onChangeStartDate = useCallback(
|
||||
(date) => {
|
||||
dispatch({
|
||||
type: 'endpointDetailsActivityLogUpdatePaging',
|
||||
payload: {
|
||||
disabled: false,
|
||||
page,
|
||||
pageSize,
|
||||
startDate: date ? date?.toISOString() : undefined,
|
||||
endDate: endDate ? endDate : undefined,
|
||||
},
|
||||
});
|
||||
},
|
||||
[dispatch, endDate, page, pageSize]
|
||||
);
|
||||
|
||||
const onChangeEndDate = useCallback(
|
||||
(date) => {
|
||||
dispatch({
|
||||
type: 'endpointDetailsActivityLogUpdatePaging',
|
||||
payload: {
|
||||
disabled: false,
|
||||
page,
|
||||
pageSize,
|
||||
startDate: startDate ? startDate : undefined,
|
||||
endDate: date ? date.toISOString() : undefined,
|
||||
},
|
||||
});
|
||||
},
|
||||
[dispatch, startDate, page, pageSize]
|
||||
);
|
||||
|
||||
return (
|
||||
<StickyFlexItem grow={false}>
|
||||
<EuiFlexGroup justifyContent="flexEnd" responsive>
|
||||
<DatePickerWrapper>
|
||||
<EuiFlexItem>
|
||||
<EuiDatePickerRange
|
||||
fullWidth={true}
|
||||
data-test-subj="activityLogDateRangePicker"
|
||||
startDateControl={
|
||||
<EuiDatePicker
|
||||
aria-label="Start date"
|
||||
endDate={endDate ? moment(endDate) : undefined}
|
||||
isInvalid={isInvalidDateRange}
|
||||
maxDate={moment(endDate) || moment()}
|
||||
onChange={onChangeStartDate}
|
||||
onClear={() => onClear({ clearStart: true })}
|
||||
placeholderText={i18.ACTIVITY_LOG.datePicker.startDate}
|
||||
selected={startDate ? moment(startDate) : undefined}
|
||||
showTimeSelect
|
||||
startDate={startDate ? moment(startDate) : undefined}
|
||||
/>
|
||||
}
|
||||
endDateControl={
|
||||
<EuiDatePicker
|
||||
aria-label="End date"
|
||||
endDate={endDate ? moment(endDate) : undefined}
|
||||
isInvalid={isInvalidDateRange}
|
||||
maxDate={moment()}
|
||||
minDate={startDate ? moment(startDate) : undefined}
|
||||
onChange={onChangeEndDate}
|
||||
onClear={() => onClear({ clearEnd: true })}
|
||||
placeholderText={i18.ACTIVITY_LOG.datePicker.endDate}
|
||||
selected={endDate ? moment(endDate) : undefined}
|
||||
showTimeSelect
|
||||
startDate={startDate ? moment(startDate) : undefined}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</DatePickerWrapper>
|
||||
</EuiFlexGroup>
|
||||
</StickyFlexItem>
|
||||
);
|
||||
});
|
||||
|
||||
DateRangePicker.displayName = 'DateRangePicker';
|
|
@ -56,19 +56,14 @@ export const EndpointDetailsFlyoutTabs = memo(
|
|||
},
|
||||
});
|
||||
if (tab.id === EndpointDetailsTabsTypes.activityLog) {
|
||||
const paging = {
|
||||
page: 1,
|
||||
pageSize,
|
||||
};
|
||||
dispatch({
|
||||
type: 'appRequestedEndpointActivityLog',
|
||||
payload: paging,
|
||||
});
|
||||
dispatch({
|
||||
type: 'endpointDetailsActivityLogUpdatePaging',
|
||||
payload: {
|
||||
disabled: false,
|
||||
...paging,
|
||||
page: 1,
|
||||
pageSize,
|
||||
startDate: undefined,
|
||||
endDate: undefined,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { memo, useCallback, useEffect, useRef } from 'react';
|
||||
import React, { memo, useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import {
|
||||
|
@ -17,6 +17,7 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { LogEntry } from './components/log_entry';
|
||||
import { DateRangePicker } from './components/activity_log_date_range_picker';
|
||||
import * as i18 from '../translations';
|
||||
import { Immutable, ActivityLog } from '../../../../../../common/endpoint/types';
|
||||
import { AsyncResourceState } from '../../../../state';
|
||||
|
@ -31,12 +32,12 @@ import {
|
|||
getActivityLogRequestLoading,
|
||||
} from '../../store/selectors';
|
||||
|
||||
const StyledEuiFlexGroup = styled(EuiFlexGroup)`
|
||||
height: 85vh;
|
||||
const StyledEuiFlexGroup = styled(EuiFlexGroup)<{ isShorter: boolean }>`
|
||||
height: ${({ isShorter }) => (isShorter ? '25vh' : '85vh')};
|
||||
`;
|
||||
const LoadMoreTrigger = styled.div`
|
||||
height: 6px;
|
||||
width: 100%;
|
||||
height: ${(props) => props.theme.eui.euiSizeXS};
|
||||
width: ${(props) => props.theme.eui.fractions.single.percentage};
|
||||
`;
|
||||
|
||||
export const EndpointActivityLog = memo(
|
||||
|
@ -48,25 +49,37 @@ export const EndpointActivityLog = memo(
|
|||
const activityLogSize = activityLogData.length;
|
||||
const activityLogError = useEndpointSelector(getActivityLogError);
|
||||
const dispatch = useDispatch<(action: EndpointAction) => void>();
|
||||
const { page, pageSize, disabled: isPagingDisabled } = useEndpointSelector(
|
||||
const { page, pageSize, startDate, endDate, disabled: isPagingDisabled } = useEndpointSelector(
|
||||
getActivityLogDataPaging
|
||||
);
|
||||
|
||||
const hasActiveDateRange = useMemo(() => !!startDate || !!endDate, [startDate, endDate]);
|
||||
const showEmptyState = useMemo(
|
||||
() => (activityLogLoaded && !activityLogSize && !hasActiveDateRange) || activityLogError,
|
||||
[activityLogLoaded, activityLogSize, hasActiveDateRange, activityLogError]
|
||||
);
|
||||
const isShorter = useMemo(
|
||||
() => !!(hasActiveDateRange && isPagingDisabled && !activityLogLoading && !activityLogSize),
|
||||
[hasActiveDateRange, isPagingDisabled, activityLogLoading, activityLogSize]
|
||||
);
|
||||
|
||||
const loadMoreTrigger = useRef<HTMLInputElement | null>(null);
|
||||
const getActivityLog = useCallback(
|
||||
(entries: IntersectionObserverEntry[]) => {
|
||||
const isTargetIntersecting = entries.some((entry) => entry.isIntersecting);
|
||||
if (isTargetIntersecting && activityLogLoaded && !isPagingDisabled) {
|
||||
dispatch({
|
||||
type: 'appRequestedEndpointActivityLog',
|
||||
type: 'endpointDetailsActivityLogUpdatePaging',
|
||||
payload: {
|
||||
page: page + 1,
|
||||
pageSize,
|
||||
startDate,
|
||||
endDate,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
[activityLogLoaded, dispatch, isPagingDisabled, page, pageSize]
|
||||
[activityLogLoaded, dispatch, isPagingDisabled, page, pageSize, startDate, endDate]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -82,8 +95,8 @@ export const EndpointActivityLog = memo(
|
|||
|
||||
return (
|
||||
<>
|
||||
<StyledEuiFlexGroup direction="column" responsive={false}>
|
||||
{(activityLogLoaded && !activityLogSize) || activityLogError ? (
|
||||
<StyledEuiFlexGroup direction="column" responsive={false} isShorter={isShorter}>
|
||||
{showEmptyState ? (
|
||||
<EuiFlexItem>
|
||||
<EuiEmptyPrompt
|
||||
iconType="editorUnorderedList"
|
||||
|
@ -95,6 +108,7 @@ export const EndpointActivityLog = memo(
|
|||
</EuiFlexItem>
|
||||
) : (
|
||||
<>
|
||||
<DateRangePicker />
|
||||
<EuiFlexItem grow={true}>
|
||||
{activityLogLoaded &&
|
||||
activityLogData.map((logEntry) => (
|
||||
|
|
|
@ -889,6 +889,27 @@ describe('when on the endpoint list page', () => {
|
|||
const emptyState = await renderResult.queryByTestId('activityLogEmpty');
|
||||
expect(emptyState).not.toBe(null);
|
||||
});
|
||||
|
||||
it('should not display empty state with no log data while date range filter is active', async () => {
|
||||
const activityLogTab = await renderResult.findByTestId('activity_log');
|
||||
reactTestingLibrary.act(() => {
|
||||
reactTestingLibrary.fireEvent.click(activityLogTab);
|
||||
});
|
||||
await middlewareSpy.waitForAction('endpointDetailsActivityLogChanged');
|
||||
reactTestingLibrary.act(() => {
|
||||
dispatchEndpointDetailsActivityLogChanged('success', {
|
||||
page: 1,
|
||||
pageSize: 50,
|
||||
startDate: new Date().toISOString(),
|
||||
data: [],
|
||||
});
|
||||
});
|
||||
|
||||
const emptyState = await renderResult.queryByTestId('activityLogEmpty');
|
||||
const dateRangePicker = await renderResult.queryByTestId('activityLogDateRangePicker');
|
||||
expect(emptyState).toBe(null);
|
||||
expect(dateRangePicker).not.toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when showing host Policy Response panel', () => {
|
||||
|
|
|
@ -15,6 +15,20 @@ export const ACTIVITY_LOG = {
|
|||
tabTitle: i18n.translate('xpack.securitySolution.endpointDetails.activityLog', {
|
||||
defaultMessage: 'Activity Log',
|
||||
}),
|
||||
datePicker: {
|
||||
startDate: i18n.translate(
|
||||
'xpack.securitySolution.endpointDetails.activityLog.datePicker.startDate',
|
||||
{
|
||||
defaultMessage: 'Pick a start date',
|
||||
}
|
||||
),
|
||||
endDate: i18n.translate(
|
||||
'xpack.securitySolution.endpointDetails.activityLog.datePicker.endDate',
|
||||
{
|
||||
defaultMessage: 'Pick an end date',
|
||||
}
|
||||
),
|
||||
},
|
||||
LogEntry: {
|
||||
endOfLog: i18n.translate(
|
||||
'xpack.securitySolution.endpointDetails.activityLog.logEntry.action.endOfLog',
|
||||
|
|
|
@ -60,6 +60,37 @@ describe('Action Log API', () => {
|
|||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('should work with all query params', () => {
|
||||
expect(() => {
|
||||
EndpointActionLogRequestSchema.query.validate({
|
||||
page: 10,
|
||||
page_size: 100,
|
||||
start_date: new Date(new Date().setDate(new Date().getDate() - 1)).toISOString(), // yesterday
|
||||
end_date: new Date().toISOString(), // today
|
||||
});
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('should work with just startDate', () => {
|
||||
expect(() => {
|
||||
EndpointActionLogRequestSchema.query.validate({
|
||||
page: 1,
|
||||
page_size: 100,
|
||||
start_date: new Date(new Date().setDate(new Date().getDate() - 1)).toISOString(), // yesterday
|
||||
});
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('should work with just endDate', () => {
|
||||
expect(() => {
|
||||
EndpointActionLogRequestSchema.query.validate({
|
||||
page: 1,
|
||||
page_size: 100,
|
||||
end_date: new Date().toISOString(), // today
|
||||
});
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('should not work without allowed page and page_size params', () => {
|
||||
expect(() => {
|
||||
EndpointActionLogRequestSchema.query.validate({ page_size: 101 });
|
||||
|
@ -176,5 +207,20 @@ describe('Action Log API', () => {
|
|||
expect(error.message).toEqual(`Error fetching actions log for agent_id ${mockID}`);
|
||||
}
|
||||
});
|
||||
|
||||
it('should return date ranges if present in the query', async () => {
|
||||
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,
|
||||
});
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -27,10 +27,18 @@ export const actionsLogRequestHandler = (
|
|||
return async (context, req, res) => {
|
||||
const {
|
||||
params: { agent_id: elasticAgentId },
|
||||
query: { page, page_size: pageSize },
|
||||
query: { page, page_size: pageSize, start_date: startDate, end_date: endDate },
|
||||
} = req;
|
||||
|
||||
const body = await getAuditLogResponse({ elasticAgentId, page, pageSize, context, logger });
|
||||
const body = await getAuditLogResponse({
|
||||
elasticAgentId,
|
||||
page,
|
||||
pageSize,
|
||||
startDate,
|
||||
endDate,
|
||||
context,
|
||||
logger,
|
||||
});
|
||||
return res.ok({
|
||||
body,
|
||||
});
|
||||
|
|
|
@ -19,28 +19,37 @@ export const getAuditLogResponse = async ({
|
|||
elasticAgentId,
|
||||
page,
|
||||
pageSize,
|
||||
startDate,
|
||||
endDate,
|
||||
context,
|
||||
logger,
|
||||
}: {
|
||||
elasticAgentId: string;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
context: SecuritySolutionRequestHandlerContext;
|
||||
logger: Logger;
|
||||
}): Promise<{
|
||||
page: number;
|
||||
pageSize: number;
|
||||
data: ActivityLog['data'];
|
||||
}> => {
|
||||
}): Promise<ActivityLog> => {
|
||||
const size = Math.floor(pageSize / 2);
|
||||
const from = page <= 1 ? 0 : page * size - size + 1;
|
||||
const esClient = context.core.elasticsearch.client.asCurrentUser;
|
||||
|
||||
const data = await getActivityLog({ esClient, from, size, elasticAgentId, logger });
|
||||
const data = await getActivityLog({
|
||||
esClient,
|
||||
from,
|
||||
size,
|
||||
startDate,
|
||||
endDate,
|
||||
elasticAgentId,
|
||||
logger,
|
||||
});
|
||||
|
||||
return {
|
||||
page,
|
||||
pageSize,
|
||||
startDate,
|
||||
endDate,
|
||||
data,
|
||||
};
|
||||
};
|
||||
|
@ -49,6 +58,8 @@ const getActivityLog = async ({
|
|||
esClient,
|
||||
size,
|
||||
from,
|
||||
startDate,
|
||||
endDate,
|
||||
elasticAgentId,
|
||||
logger,
|
||||
}: {
|
||||
|
@ -56,6 +67,8 @@ const getActivityLog = async ({
|
|||
elasticAgentId: string;
|
||||
size: number;
|
||||
from: number;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
logger: Logger;
|
||||
}) => {
|
||||
const options = {
|
||||
|
@ -67,8 +80,22 @@ const getActivityLog = async ({
|
|||
|
||||
let actionsResult;
|
||||
let responsesResult;
|
||||
const dateFilters = [];
|
||||
if (startDate) {
|
||||
dateFilters.push({ range: { '@timestamp': { gte: startDate } } });
|
||||
}
|
||||
if (endDate) {
|
||||
dateFilters.push({ range: { '@timestamp': { lte: endDate } } });
|
||||
}
|
||||
|
||||
try {
|
||||
// fetch actions with matching agent_id
|
||||
const baseActionFilters = [
|
||||
{ term: { agents: elasticAgentId } },
|
||||
{ term: { input_type: 'endpoint' } },
|
||||
{ term: { type: 'INPUT_ACTION' } },
|
||||
];
|
||||
const actionsFilters = [...baseActionFilters, ...dateFilters];
|
||||
actionsResult = await esClient.search(
|
||||
{
|
||||
index: AGENT_ACTIONS_INDEX,
|
||||
|
@ -77,11 +104,8 @@ const getActivityLog = async ({
|
|||
body: {
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{ term: { agents: elasticAgentId } },
|
||||
{ term: { input_type: 'endpoint' } },
|
||||
{ term: { type: 'INPUT_ACTION' } },
|
||||
],
|
||||
// @ts-ignore
|
||||
filter: actionsFilters,
|
||||
},
|
||||
},
|
||||
sort: [
|
||||
|
@ -99,6 +123,12 @@ const getActivityLog = async ({
|
|||
(e) => (e._source as EndpointAction).action_id
|
||||
);
|
||||
|
||||
// fetch responses with matching `action_id`s
|
||||
const baseResponsesFilter = [
|
||||
{ term: { agent_id: elasticAgentId } },
|
||||
{ terms: { action_id: actionIds } },
|
||||
];
|
||||
const responsesFilters = [...baseResponsesFilter, ...dateFilters];
|
||||
responsesResult = await esClient.search(
|
||||
{
|
||||
index: AGENT_ACTIONS_RESULTS_INDEX,
|
||||
|
@ -106,7 +136,7 @@ const getActivityLog = async ({
|
|||
body: {
|
||||
query: {
|
||||
bool: {
|
||||
filter: [{ term: { agent_id: elasticAgentId } }, { terms: { action_id: actionIds } }],
|
||||
filter: responsesFilters,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue