mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Security Solution] Implement Azure and Okta asset integration (user flyout) (#171629)
## Summaryb749781c
-1941-40d5-8dc8-094659fba9e5 <img width="674" alt="Screenshot 2023-12-05 at 10 31 10" src="dc0dc39e
-6ac1-47e6-b608-ec6667be251b"> * Remove the `firstLastSeen` call from managed user data because it can be inferred from the event timestamp. * Updated the managed data API to return Okta and Azure data. * Create a Flyout asset document details panel * Create a cell action that add/remove fields from the asset table * Persist selected field on the redux store * Persist the selected fields on local storage * [] TODO update query match to use e-mail field ### How to test it? * Enable the experimental flag `xpack.securitySolution.enableExperimental: ['newUserDetailsFlyout']` * Start an elastic cluster with fleet and elastic agent * Follow this steps to setup a cluster with elastic-package * Install https://github.com/elastic/elastic-package * `elastic-package stack up -vd --version 8.12.0-SNAPSHOT` * connect your local kibana instance to the cluster ES instance * configured `server.port`, `elasticsearch.hosts`, `elasticsearch.ssl.certificateAuthorities`, `elasticsearch.serviceAccountToken` * Install Okta and Entra integrations (ask @machadoum for credentials) * Create a rule that generates alerts for every event * Go to the alerts table and click on the username to open the flyout **Tip:** You can open your Docker application and explore the files to copy the token and the certificate. <img width="1420" alt="Screenshot 2023-12-04 at 16 29 53" src="60032e34
-6f50-4316-ad88-2a13109a5622"> ### Checklist Delete any items that are not applicable to this PR. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [x] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [x] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
This commit is contained in:
parent
1f35f23a5d
commit
d922ae06ee
64 changed files with 2631 additions and 889 deletions
|
@ -12,6 +12,7 @@ import { requestBasicOptionsSchema } from '../model/request_basic_options';
|
|||
|
||||
export const managedUserDetailsSchema = requestBasicOptionsSchema.extend({
|
||||
userName: z.string(),
|
||||
userEmail: z.array(z.string()).optional(),
|
||||
factoryQueryType: z.literal(UsersQueries.managedDetails),
|
||||
});
|
||||
|
||||
|
|
|
@ -6,21 +6,25 @@
|
|||
*/
|
||||
|
||||
import type { IEsSearchResponse } from '@kbn/data-plugin/common';
|
||||
import type { EcsBase, EcsEvent, EcsHost, EcsUser, EcsAgent } from '@kbn/ecs';
|
||||
import type { SearchTypes } from '../../../../detection_engine/types';
|
||||
import type { Inspect, Maybe } from '../../../common';
|
||||
|
||||
export interface ManagedUserDetailsStrategyResponse extends IEsSearchResponse {
|
||||
userDetails?: AzureManagedUser;
|
||||
users: ManagedUserHits;
|
||||
inspect?: Maybe<Inspect>;
|
||||
}
|
||||
|
||||
export interface AzureManagedUser extends Pick<EcsBase, '@timestamp'> {
|
||||
agent: EcsAgent;
|
||||
host: EcsHost;
|
||||
event: EcsEvent;
|
||||
user: EcsUser & {
|
||||
last_name?: string;
|
||||
first_name?: string;
|
||||
phone?: string[];
|
||||
};
|
||||
export enum ManagedUserDatasetKey {
|
||||
ENTRA = 'entityanalytics_entra_id.user',
|
||||
OKTA = 'entityanalytics_okta.user',
|
||||
}
|
||||
|
||||
export interface ManagedUserHit {
|
||||
_index: string;
|
||||
_id: string;
|
||||
fields?: ManagedUserFields;
|
||||
}
|
||||
|
||||
export type ManagedUserHits = Record<ManagedUserDatasetKey, ManagedUserHit | undefined>;
|
||||
|
||||
export type ManagedUserFields = Record<string, SearchTypes[]>;
|
||||
|
|
|
@ -29,6 +29,7 @@ import {
|
|||
createCopyToClipboardDiscoverCellActionFactory,
|
||||
} from './copy_to_clipboard';
|
||||
import { createToggleColumnCellActionFactory } from './toggle_column';
|
||||
import { createToggleUserAssetFieldCellActionFactory } from './toggle_asset_column';
|
||||
import { SecurityCellActionsTrigger } from './constants';
|
||||
import type {
|
||||
DiscoverCellActionName,
|
||||
|
@ -117,6 +118,7 @@ const registerCellActions = (
|
|||
showTopN: createShowTopNCellActionFactory({ services }),
|
||||
copyToClipboard: createCopyToClipboardCellActionFactory({ services }),
|
||||
toggleColumn: createToggleColumnCellActionFactory({ store, services }),
|
||||
toggleUserAssetField: createToggleUserAssetFieldCellActionFactory({ store }),
|
||||
};
|
||||
|
||||
const registerCellActionsTrigger = (
|
||||
|
@ -147,6 +149,7 @@ const registerCellActions = (
|
|||
'filterOut',
|
||||
'addToTimeline',
|
||||
'toggleColumn',
|
||||
'toggleUserAssetField',
|
||||
'showTopN',
|
||||
'copyToClipboard',
|
||||
]);
|
||||
|
|
|
@ -0,0 +1,137 @@
|
|||
/*
|
||||
* 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 type { CellActionExecutionContext } from '@kbn/cell-actions';
|
||||
import { createToggleUserAssetFieldCellActionFactory } from './toggle_asset_column';
|
||||
import type { SecurityAppStore } from '../../../common/store/types';
|
||||
import { mockGlobalState } from '../../../common/mock';
|
||||
import { UserAssetTableType } from '../../../explore/users/store/model';
|
||||
import { usersActions } from '../../../explore/users/store';
|
||||
|
||||
const existingFieldName = 'existing.field';
|
||||
const fieldName = 'user.name';
|
||||
|
||||
const mockToggleColumn = jest.fn();
|
||||
const mockDispatch = jest.fn();
|
||||
const mockGetState = jest.fn().mockReturnValue({
|
||||
...mockGlobalState,
|
||||
users: {
|
||||
...mockGlobalState.users,
|
||||
flyout: {
|
||||
...mockGlobalState.users.flyout,
|
||||
queries: {
|
||||
[UserAssetTableType.assetEntra]: {
|
||||
fields: [existingFieldName],
|
||||
},
|
||||
[UserAssetTableType.assetOkta]: {
|
||||
fields: [existingFieldName],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const store = {
|
||||
dispatch: mockDispatch,
|
||||
getState: mockGetState,
|
||||
} as unknown as SecurityAppStore;
|
||||
|
||||
const context = {
|
||||
data: [
|
||||
{
|
||||
field: { name: fieldName },
|
||||
},
|
||||
],
|
||||
metadata: {
|
||||
scopeId: UserAssetTableType.assetEntra,
|
||||
},
|
||||
} as unknown as CellActionExecutionContext;
|
||||
|
||||
describe('createToggleUserAssetFieldCellActionFactory', () => {
|
||||
const toggleColumnActionFactory = createToggleUserAssetFieldCellActionFactory({
|
||||
store,
|
||||
});
|
||||
const toggleColumnAction = toggleColumnActionFactory({ id: 'testAction' });
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return display name', () => {
|
||||
expect(toggleColumnAction.getDisplayName(context)).toEqual('Toggle field in asset table');
|
||||
});
|
||||
|
||||
it('should return icon type', () => {
|
||||
expect(toggleColumnAction.getIconType(context)).toEqual('listAdd');
|
||||
});
|
||||
|
||||
describe('isCompatible', () => {
|
||||
it('should return false if scopeId is undefined', async () => {
|
||||
expect(
|
||||
await toggleColumnAction.isCompatible({ ...context, metadata: { scopeId: undefined } })
|
||||
).toEqual(false);
|
||||
});
|
||||
|
||||
it('should return false if scopeId is different than Okta or Entra asset table', async () => {
|
||||
expect(
|
||||
await toggleColumnAction.isCompatible({
|
||||
...context,
|
||||
metadata: { scopeId: 'test-scopeId-1234' },
|
||||
})
|
||||
).toEqual(false);
|
||||
});
|
||||
|
||||
it('should return true if scopeId is okta or entra asset table', async () => {
|
||||
expect(
|
||||
await toggleColumnAction.isCompatible({
|
||||
...context,
|
||||
metadata: { scopeId: UserAssetTableType.assetEntra },
|
||||
})
|
||||
).toEqual(true);
|
||||
expect(
|
||||
await toggleColumnAction.isCompatible({
|
||||
...context,
|
||||
metadata: { scopeId: UserAssetTableType.assetOkta },
|
||||
})
|
||||
).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
afterEach(() => {
|
||||
mockToggleColumn.mockClear();
|
||||
});
|
||||
it('should remove field', async () => {
|
||||
await toggleColumnAction.execute({
|
||||
...context,
|
||||
data: [
|
||||
{ ...context.data[0], field: { ...context.data[0].field, name: existingFieldName } },
|
||||
],
|
||||
});
|
||||
expect(mockDispatch).toHaveBeenCalledWith(
|
||||
usersActions.removeUserAssetTableField({
|
||||
tableId: UserAssetTableType.assetEntra,
|
||||
fieldName: existingFieldName,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should add field', async () => {
|
||||
const name = 'new-field-name';
|
||||
await toggleColumnAction.execute({
|
||||
...context,
|
||||
data: [{ ...context.data[0], field: { ...context.data[0].field, name } }],
|
||||
});
|
||||
expect(mockDispatch).toHaveBeenCalledWith(
|
||||
usersActions.addUserAssetTableField({
|
||||
tableId: UserAssetTableType.assetEntra,
|
||||
fieldName: name,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* 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';
|
||||
import { createCellActionFactory, type CellActionTemplate } from '@kbn/cell-actions';
|
||||
import { UserAssetTableType } from '../../../explore/users/store/model';
|
||||
import { usersActions, usersSelectors } from '../../../explore/users/store';
|
||||
import { fieldHasCellActions } from '../../utils';
|
||||
import type { SecurityAppStore } from '../../../common/store';
|
||||
import type { SecurityCellAction } from '../../types';
|
||||
import { SecurityCellActionType } from '../../constants';
|
||||
|
||||
const ICON = 'listAdd';
|
||||
const TOGGLE_FIELD = i18n.translate(
|
||||
'xpack.securitySolution.actions.toggleFieldToAssetTable.label',
|
||||
{
|
||||
defaultMessage: 'Toggle field in asset table',
|
||||
}
|
||||
);
|
||||
|
||||
export const createToggleUserAssetFieldCellActionFactory = createCellActionFactory(
|
||||
({ store }: { store: SecurityAppStore }): CellActionTemplate<SecurityCellAction> => ({
|
||||
type: SecurityCellActionType.TOGGLE_COLUMN,
|
||||
getIconType: () => ICON,
|
||||
getDisplayName: () => TOGGLE_FIELD,
|
||||
getDisplayNameTooltip: ({ data }) => TOGGLE_FIELD,
|
||||
isCompatible: async ({ data, metadata }) => {
|
||||
const field = data[0]?.field;
|
||||
|
||||
return (
|
||||
data.length === 1 &&
|
||||
fieldHasCellActions(field.name) &&
|
||||
!metadata?.isObjectArray &&
|
||||
!!metadata?.scopeId &&
|
||||
Object.values(UserAssetTableType).includes(
|
||||
metadata?.scopeId as unknown as UserAssetTableType
|
||||
)
|
||||
);
|
||||
},
|
||||
execute: async ({ metadata, data }) => {
|
||||
const field = data[0]?.field;
|
||||
const scopeId = metadata?.scopeId as UserAssetTableType | undefined;
|
||||
|
||||
if (!scopeId) return;
|
||||
|
||||
const { fields } = usersSelectors.selectUserAssetTableById(store.getState(), scopeId);
|
||||
|
||||
if (fields.some((f) => f === field.name)) {
|
||||
store.dispatch(
|
||||
usersActions.removeUserAssetTableField({
|
||||
fieldName: field.name,
|
||||
tableId: scopeId,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
store.dispatch(
|
||||
usersActions.addUserAssetTableField({
|
||||
fieldName: field.name,
|
||||
tableId: scopeId,
|
||||
})
|
||||
);
|
||||
}
|
||||
},
|
||||
})
|
||||
);
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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 { createToggleUserAssetFieldCellActionFactory } from './cell_action/toggle_asset_column';
|
|
@ -62,6 +62,7 @@ export interface SecurityCellActions {
|
|||
showTopN: CellActionFactory;
|
||||
copyToClipboard: CellActionFactory;
|
||||
toggleColumn: CellActionFactory;
|
||||
toggleUserAssetField: CellActionFactory;
|
||||
}
|
||||
|
||||
// All security cell actions names
|
||||
|
|
|
@ -257,6 +257,16 @@ export const mockGlobalState: State = {
|
|||
[usersModel.UsersTableType.events]: { activePage: 0, limit: 10 },
|
||||
},
|
||||
},
|
||||
flyout: {
|
||||
queries: {
|
||||
[usersModel.UserAssetTableType.assetEntra]: {
|
||||
fields: [],
|
||||
},
|
||||
[usersModel.UserAssetTableType.assetOkta]: {
|
||||
fields: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
inputs: {
|
||||
global: {
|
||||
|
|
|
@ -7,11 +7,12 @@
|
|||
|
||||
import { hostsReducer } from '../../explore/hosts/store';
|
||||
import { networkReducer } from '../../explore/network/store';
|
||||
import { usersReducer } from '../../explore/users/store';
|
||||
import { makeUsersReducer } from '../../explore/users/store';
|
||||
import { timelineReducer } from '../../timelines/store/timeline/reducer';
|
||||
import { managementReducer } from '../../management/store/reducer';
|
||||
import type { ManagementPluginReducer } from '../../management';
|
||||
import type { SubPluginsInitReducer } from '../store';
|
||||
import { createSecuritySolutionStorageMock } from './mock_local_storage';
|
||||
|
||||
type GlobalThis = typeof globalThis;
|
||||
interface Global extends GlobalThis {
|
||||
|
@ -23,10 +24,12 @@ interface Global extends GlobalThis {
|
|||
|
||||
export const globalNode: Global = global;
|
||||
|
||||
const { storage } = createSecuritySolutionStorageMock();
|
||||
|
||||
export const SUB_PLUGINS_REDUCER: SubPluginsInitReducer = {
|
||||
hosts: hostsReducer,
|
||||
network: networkReducer,
|
||||
users: usersReducer,
|
||||
users: makeUsersReducer(storage),
|
||||
timeline: timelineReducer,
|
||||
/**
|
||||
* These state's are wrapped in `Immutable`, but for compatibility with the overall app architecture,
|
||||
|
|
|
@ -9,6 +9,9 @@ import type { Epic } from 'redux-observable';
|
|||
import { combineEpics } from 'redux-observable';
|
||||
import type { Action } from 'redux';
|
||||
|
||||
import type { Observable } from 'rxjs';
|
||||
import type { Storage } from '@kbn/kibana-utils-plugin/public';
|
||||
import type { CoreStart } from '@kbn/core/public';
|
||||
import { createTimelineEpic } from '../../timelines/store/timeline/epic';
|
||||
import { createTimelineChangedEpic } from '../../timelines/store/timeline/epic_changed';
|
||||
import { createTimelineFavoriteEpic } from '../../timelines/store/timeline/epic_favorite';
|
||||
|
@ -16,18 +19,26 @@ import { createTimelineNoteEpic } from '../../timelines/store/timeline/epic_note
|
|||
import { createTimelinePinnedEventEpic } from '../../timelines/store/timeline/epic_pinned_event';
|
||||
import type { TimelineEpicDependencies } from '../../timelines/store/timeline/types';
|
||||
import { createDataTableLocalStorageEpic } from './data_table/epic_local_storage';
|
||||
import { createUserAssetTableLocalStorageEpic } from '../../explore/users/store/epic_storage';
|
||||
import type { State } from './types';
|
||||
|
||||
export const createRootEpic = <State>(): Epic<
|
||||
export interface RootEpicDependencies {
|
||||
kibana$: Observable<CoreStart>;
|
||||
storage: Storage;
|
||||
}
|
||||
|
||||
export const createRootEpic = <StateT extends State>(): Epic<
|
||||
Action,
|
||||
Action,
|
||||
State,
|
||||
TimelineEpicDependencies<State>
|
||||
StateT,
|
||||
TimelineEpicDependencies<StateT>
|
||||
> =>
|
||||
combineEpics(
|
||||
createTimelineEpic<State>(),
|
||||
createTimelineEpic<StateT>(),
|
||||
createTimelineChangedEpic(),
|
||||
createTimelineFavoriteEpic<State>(),
|
||||
createTimelineNoteEpic<State>(),
|
||||
createTimelinePinnedEventEpic<State>(),
|
||||
createDataTableLocalStorageEpic<State>()
|
||||
createTimelineFavoriteEpic<StateT>(),
|
||||
createTimelineNoteEpic<StateT>(),
|
||||
createTimelinePinnedEventEpic<StateT>(),
|
||||
createDataTableLocalStorageEpic<StateT>(),
|
||||
createUserAssetTableLocalStorageEpic<StateT>()
|
||||
);
|
||||
|
|
|
@ -169,6 +169,7 @@ export const createStoreFactory = async (
|
|||
|
||||
return createStore(initialState, rootReducer, libs$.pipe(pluck('kibana')), storage, [
|
||||
...(subPlugins.management.store.middleware ?? []),
|
||||
...(subPlugins.explore.store.middleware ?? []),
|
||||
...[resolverMiddlewareFactory(dataAccessLayerFactory(coreStart)) ?? []],
|
||||
]);
|
||||
};
|
||||
|
|
|
@ -15,7 +15,7 @@ import { routes } from './routes';
|
|||
import type { NetworkState } from './network/store';
|
||||
import { initialNetworkState, networkReducer } from './network/store';
|
||||
import { getDataTablesInStorageByIds } from '../timelines/containers/local_storage';
|
||||
import { initialUsersState, usersReducer } from './users/store';
|
||||
import { makeUsersReducer, getInitialUsersState } from './users/store';
|
||||
import { hostsReducer, initialHostsState } from './hosts/store';
|
||||
|
||||
export interface ExploreState {
|
||||
|
@ -51,10 +51,10 @@ export class Explore {
|
|||
store: {
|
||||
initialState: {
|
||||
network: initialNetworkState,
|
||||
users: initialUsersState,
|
||||
users: getInitialUsersState(storage),
|
||||
hosts: initialHostsState,
|
||||
},
|
||||
reducer: { network: networkReducer, users: usersReducer, hosts: hostsReducer },
|
||||
reducer: { network: networkReducer, users: makeUsersReducer(storage), hosts: hostsReducer },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import actionCreatorFactory from 'typescript-fsa';
|
|||
import type { usersModel } from '.';
|
||||
import type { RiskScoreSortField, RiskSeverity } from '../../../../common/search_strategy';
|
||||
import type { SortUsersField } from '../../../../common/search_strategy/security_solution/users/common';
|
||||
import type { UserAssetTableType } from './model';
|
||||
|
||||
const actionCreator = actionCreatorFactory('x-pack/security_solution/local/users');
|
||||
|
||||
|
@ -48,3 +49,13 @@ export const updateUsersAnomaliesInterval = actionCreator<{
|
|||
interval: string;
|
||||
usersType: usersModel.UsersType;
|
||||
}>('UPDATE_USERS_ANOMALIES_INTERVAL');
|
||||
|
||||
export const addUserAssetTableField = actionCreator<{
|
||||
fieldName: string;
|
||||
tableId: UserAssetTableType;
|
||||
}>('ADD_USER_ASSET_TABLE_FIELD');
|
||||
|
||||
export const removeUserAssetTableField = actionCreator<{
|
||||
fieldName: string;
|
||||
tableId: UserAssetTableType;
|
||||
}>('REMOVE_USER_ASSET_TABLE_FIELD');
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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 { UserAssetTableType } from './model';
|
||||
|
||||
export const USER_ASSET_TABLE_DEFAULTS_FIELDS = {
|
||||
[UserAssetTableType.assetOkta]: [
|
||||
'user.id',
|
||||
'user.profile.first_name',
|
||||
'user.profile.last_name',
|
||||
'user.profile.primaryPhone',
|
||||
'user.profile.mobile_phone',
|
||||
'user.profile.job_title',
|
||||
'user.geo.city_name',
|
||||
'user.geo.country_iso_code',
|
||||
],
|
||||
[UserAssetTableType.assetEntra]: [
|
||||
'user.id',
|
||||
'user.first_name',
|
||||
'user.last_name',
|
||||
'user.phone',
|
||||
'user.job_title',
|
||||
'user.work.location_name',
|
||||
],
|
||||
};
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* 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 { waitFor } from '@testing-library/react';
|
||||
import {
|
||||
createSecuritySolutionStorageMock,
|
||||
kibanaObservable,
|
||||
mockGlobalState,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
} from '../../../common/mock';
|
||||
import { createStore } from '../../../common/store';
|
||||
import { addUserAssetTableField, removeUserAssetTableField } from './actions';
|
||||
import { UserAssetTableType } from './model';
|
||||
import { getUserAssetTableFromStorage } from './storage';
|
||||
import type { Store } from 'redux';
|
||||
|
||||
let store: Store;
|
||||
const storage = createSecuritySolutionStorageMock().storage;
|
||||
|
||||
describe('UsersAssetTable EpicStorage', () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
storage.clear();
|
||||
store = createStore(mockGlobalState, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
|
||||
});
|
||||
|
||||
it('persist asset table when adding and removing fields', async () => {
|
||||
const fieldName = 'test-field';
|
||||
|
||||
// Add field to the table
|
||||
store.dispatch(addUserAssetTableField({ tableId: UserAssetTableType.assetEntra, fieldName }));
|
||||
await waitFor(() => {
|
||||
return expect(getUserAssetTableFromStorage(storage)).toEqual({
|
||||
[UserAssetTableType.assetEntra]: { fields: [fieldName] },
|
||||
});
|
||||
});
|
||||
|
||||
jest.runAllTimers(); // pass the time to ensure that the state is persisted to local storage
|
||||
|
||||
// Remove field from the table
|
||||
store.dispatch(
|
||||
removeUserAssetTableField({ tableId: UserAssetTableType.assetEntra, fieldName })
|
||||
);
|
||||
await waitFor(() => {
|
||||
return expect(getUserAssetTableFromStorage(storage)).toEqual({
|
||||
[UserAssetTableType.assetEntra]: { fields: [] },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* 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 type { Action } from 'redux';
|
||||
import { map, filter, ignoreElements, tap, withLatestFrom, delay } from 'rxjs/operators';
|
||||
import type { Epic } from 'redux-observable';
|
||||
import { get } from 'lodash/fp';
|
||||
import type { RootEpicDependencies } from '../../../common/store/epic';
|
||||
import { usersActions } from '.';
|
||||
import { selectUserAssetTables } from './selectors';
|
||||
import type { UserAssetTableType } from './model';
|
||||
import type { State } from '../../../common/store/types';
|
||||
export const isNotNull = <T>(value: T | null): value is T => value !== null;
|
||||
import { persistUserAssetTableInStorage } from './storage';
|
||||
|
||||
const { removeUserAssetTableField, addUserAssetTableField } = usersActions;
|
||||
const tableActionTypes = new Set([removeUserAssetTableField.type, addUserAssetTableField.type]);
|
||||
|
||||
export const createUserAssetTableLocalStorageEpic =
|
||||
<StateT extends State>(): Epic<Action, Action, StateT, RootEpicDependencies> =>
|
||||
(action$, state$, { storage }) => {
|
||||
const table$ = state$.pipe(map(selectUserAssetTables), filter(isNotNull));
|
||||
return action$.pipe(
|
||||
delay(500),
|
||||
withLatestFrom(table$),
|
||||
tap(([action, tableById]) => {
|
||||
if (tableActionTypes.has(action.type)) {
|
||||
const tableId: UserAssetTableType = get('payload.tableId', action);
|
||||
persistUserAssetTableInStorage(storage, tableId, tableById[tableId]);
|
||||
}
|
||||
}),
|
||||
ignoreElements()
|
||||
);
|
||||
};
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* 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 { addAssetTableField, removeAssetTableField } from './helpers';
|
||||
import { UserAssetTableType } from './model';
|
||||
|
||||
describe('Users store helpers', () => {
|
||||
const fieldName = 'test-field-name';
|
||||
const emptyTableById = {
|
||||
[UserAssetTableType.assetEntra]: {
|
||||
fields: [],
|
||||
},
|
||||
[UserAssetTableType.assetOkta]: {
|
||||
fields: [],
|
||||
},
|
||||
};
|
||||
|
||||
test('addAssetTableField', () => {
|
||||
expect(
|
||||
addAssetTableField({
|
||||
tableById: emptyTableById,
|
||||
tableId: UserAssetTableType.assetEntra,
|
||||
fieldName,
|
||||
})
|
||||
).toEqual({
|
||||
...emptyTableById,
|
||||
[UserAssetTableType.assetEntra]: {
|
||||
fields: [fieldName],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('addAssetTableField does not add field if it already exists', () => {
|
||||
const tableById = {
|
||||
...emptyTableById,
|
||||
[UserAssetTableType.assetEntra]: {
|
||||
fields: [fieldName],
|
||||
},
|
||||
};
|
||||
|
||||
expect(
|
||||
addAssetTableField({
|
||||
tableById,
|
||||
tableId: UserAssetTableType.assetEntra,
|
||||
fieldName,
|
||||
})
|
||||
).toBe(tableById);
|
||||
});
|
||||
|
||||
test('removeAssetTableField', () => {
|
||||
expect(
|
||||
removeAssetTableField({
|
||||
tableById: {
|
||||
...emptyTableById,
|
||||
[UserAssetTableType.assetEntra]: {
|
||||
fields: [fieldName],
|
||||
},
|
||||
},
|
||||
tableId: UserAssetTableType.assetEntra,
|
||||
fieldName,
|
||||
})
|
||||
).toEqual(emptyTableById);
|
||||
});
|
||||
});
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { UsersModel, UsersQueries } from './model';
|
||||
import type { UserFlyoutQueries, UserAssetTableType, UsersModel, UsersQueries } from './model';
|
||||
import { UsersTableType } from './model';
|
||||
import { DEFAULT_TABLE_ACTIVE_PAGE } from '../../../common/store/constants';
|
||||
|
||||
|
@ -16,3 +16,54 @@ export const setUsersPageQueriesActivePageToZero = (state: UsersModel): UsersQue
|
|||
activePage: DEFAULT_TABLE_ACTIVE_PAGE,
|
||||
},
|
||||
});
|
||||
|
||||
interface UpsertAssetTableFieldParams {
|
||||
fieldName: string;
|
||||
tableId: UserAssetTableType;
|
||||
tableById: UserFlyoutQueries;
|
||||
}
|
||||
|
||||
export const addAssetTableField = ({
|
||||
tableById,
|
||||
tableId,
|
||||
fieldName,
|
||||
}: UpsertAssetTableFieldParams): UserFlyoutQueries => {
|
||||
const table = tableById[tableId];
|
||||
|
||||
if (table.fields.includes(fieldName)) {
|
||||
// Do not add the field if it already exists
|
||||
return tableById;
|
||||
}
|
||||
|
||||
return {
|
||||
...tableById,
|
||||
[tableId]: {
|
||||
...table,
|
||||
fields: [fieldName, ...table.fields],
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
interface RemoveAssetTableFieldParams {
|
||||
tableId: UserAssetTableType;
|
||||
fieldName: string;
|
||||
tableById: UserFlyoutQueries;
|
||||
}
|
||||
|
||||
export const removeAssetTableField = ({
|
||||
tableId,
|
||||
fieldName,
|
||||
tableById,
|
||||
}: RemoveAssetTableFieldParams): UserFlyoutQueries => {
|
||||
const table = tableById[tableId];
|
||||
|
||||
const fields = table.fields.filter((c) => c !== fieldName);
|
||||
|
||||
return {
|
||||
...tableById,
|
||||
[tableId]: {
|
||||
...table,
|
||||
fields,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -11,6 +11,7 @@ import type { SortUsersField } from '../../../../common/search_strategy/security
|
|||
export enum UsersType {
|
||||
page = 'page',
|
||||
details = 'details',
|
||||
flyout = 'flyout',
|
||||
}
|
||||
|
||||
export enum UsersTableType {
|
||||
|
@ -28,6 +29,11 @@ export enum UsersDetailsTableType {
|
|||
events = 'events',
|
||||
}
|
||||
|
||||
export enum UserAssetTableType {
|
||||
assetEntra = 'userAssetEntra',
|
||||
assetOkta = 'userAssetOkta',
|
||||
}
|
||||
|
||||
export interface BasicQueryPaginated {
|
||||
activePage: number;
|
||||
limit: number;
|
||||
|
@ -47,6 +53,10 @@ export interface UsersAnomaliesQuery {
|
|||
intervalSelection: string;
|
||||
}
|
||||
|
||||
export interface UserAssetQuery {
|
||||
fields: string[];
|
||||
}
|
||||
|
||||
export interface UsersQueries {
|
||||
[UsersTableType.allUsers]: AllUsersQuery;
|
||||
[UsersTableType.authentications]: BasicQueryPaginated;
|
||||
|
@ -60,6 +70,11 @@ export interface UserDetailsQueries {
|
|||
[UsersTableType.events]: BasicQueryPaginated;
|
||||
}
|
||||
|
||||
export interface UserFlyoutQueries {
|
||||
[UserAssetTableType.assetEntra]: UserAssetQuery;
|
||||
[UserAssetTableType.assetOkta]: UserAssetQuery;
|
||||
}
|
||||
|
||||
export interface UsersPageModel {
|
||||
queries: UsersQueries;
|
||||
}
|
||||
|
@ -68,7 +83,12 @@ export interface UserDetailsPageModel {
|
|||
queries: UserDetailsQueries;
|
||||
}
|
||||
|
||||
export interface UserFlyoutModel {
|
||||
queries: UserFlyoutQueries;
|
||||
}
|
||||
|
||||
export interface UsersModel {
|
||||
[UsersType.page]: UsersPageModel;
|
||||
[UsersType.details]: UserDetailsPageModel;
|
||||
[UsersType.flyout]: UserFlyoutModel;
|
||||
}
|
||||
|
|
|
@ -7,9 +7,11 @@
|
|||
|
||||
import { reducerWithInitialState } from 'typescript-fsa-reducers';
|
||||
import { set } from '@kbn/safer-lodash-set/fp';
|
||||
import type { Storage } from '@kbn/kibana-utils-plugin/public';
|
||||
import { DEFAULT_TABLE_ACTIVE_PAGE, DEFAULT_TABLE_LIMIT } from '../../../common/store/constants';
|
||||
|
||||
import {
|
||||
removeUserAssetTableField,
|
||||
setUsersTablesActivePageToZero,
|
||||
updateTableActivePage,
|
||||
updateTableLimit,
|
||||
|
@ -17,138 +19,184 @@ import {
|
|||
updateUserRiskScoreSeverityFilter,
|
||||
updateUsersAnomaliesInterval,
|
||||
updateUsersAnomaliesJobIdFilter,
|
||||
addUserAssetTableField,
|
||||
} from './actions';
|
||||
import { setUsersPageQueriesActivePageToZero } from './helpers';
|
||||
import {
|
||||
addAssetTableField,
|
||||
removeAssetTableField,
|
||||
setUsersPageQueriesActivePageToZero,
|
||||
} from './helpers';
|
||||
import type { UsersModel } from './model';
|
||||
import { UsersTableType } from './model';
|
||||
import { UserAssetTableType, UsersTableType } from './model';
|
||||
import { Direction } from '../../../../common/search_strategy/common';
|
||||
import { RiskScoreFields } from '../../../../common/search_strategy';
|
||||
import { UsersFields } from '../../../../common/search_strategy/security_solution/users/common';
|
||||
import { USER_ASSET_TABLE_DEFAULTS_FIELDS } from './constants';
|
||||
import { getUserAssetTableFromStorage } from './storage';
|
||||
|
||||
export type UsersState = UsersModel;
|
||||
|
||||
export const initialUsersState: UsersState = {
|
||||
page: {
|
||||
queries: {
|
||||
[UsersTableType.allUsers]: {
|
||||
activePage: DEFAULT_TABLE_ACTIVE_PAGE,
|
||||
limit: DEFAULT_TABLE_LIMIT,
|
||||
sort: {
|
||||
field: UsersFields.lastSeen,
|
||||
direction: Direction.desc,
|
||||
export const getInitialUsersState = (storage: Storage): UsersState => {
|
||||
const userAssetTable = getUserAssetTableFromStorage(storage);
|
||||
return {
|
||||
page: {
|
||||
queries: {
|
||||
[UsersTableType.allUsers]: {
|
||||
activePage: DEFAULT_TABLE_ACTIVE_PAGE,
|
||||
limit: DEFAULT_TABLE_LIMIT,
|
||||
sort: {
|
||||
field: UsersFields.lastSeen,
|
||||
direction: Direction.desc,
|
||||
},
|
||||
},
|
||||
[UsersTableType.authentications]: {
|
||||
activePage: DEFAULT_TABLE_ACTIVE_PAGE,
|
||||
limit: DEFAULT_TABLE_LIMIT,
|
||||
},
|
||||
[UsersTableType.risk]: {
|
||||
activePage: DEFAULT_TABLE_ACTIVE_PAGE,
|
||||
limit: DEFAULT_TABLE_LIMIT,
|
||||
sort: {
|
||||
field: RiskScoreFields.userRiskScore,
|
||||
direction: Direction.desc,
|
||||
},
|
||||
severitySelection: [],
|
||||
},
|
||||
[UsersTableType.anomalies]: {
|
||||
jobIdSelection: [],
|
||||
intervalSelection: 'auto',
|
||||
},
|
||||
[UsersTableType.events]: {
|
||||
activePage: DEFAULT_TABLE_ACTIVE_PAGE,
|
||||
limit: DEFAULT_TABLE_LIMIT,
|
||||
},
|
||||
},
|
||||
[UsersTableType.authentications]: {
|
||||
activePage: DEFAULT_TABLE_ACTIVE_PAGE,
|
||||
limit: DEFAULT_TABLE_LIMIT,
|
||||
},
|
||||
[UsersTableType.risk]: {
|
||||
activePage: DEFAULT_TABLE_ACTIVE_PAGE,
|
||||
limit: DEFAULT_TABLE_LIMIT,
|
||||
sort: {
|
||||
field: RiskScoreFields.userRiskScore,
|
||||
direction: Direction.desc,
|
||||
},
|
||||
details: {
|
||||
queries: {
|
||||
[UsersTableType.anomalies]: {
|
||||
jobIdSelection: [],
|
||||
intervalSelection: 'auto',
|
||||
},
|
||||
[UsersTableType.events]: {
|
||||
activePage: DEFAULT_TABLE_ACTIVE_PAGE,
|
||||
limit: DEFAULT_TABLE_LIMIT,
|
||||
},
|
||||
severitySelection: [],
|
||||
},
|
||||
[UsersTableType.anomalies]: {
|
||||
jobIdSelection: [],
|
||||
intervalSelection: 'auto',
|
||||
},
|
||||
[UsersTableType.events]: {
|
||||
activePage: DEFAULT_TABLE_ACTIVE_PAGE,
|
||||
limit: DEFAULT_TABLE_LIMIT,
|
||||
},
|
||||
},
|
||||
},
|
||||
details: {
|
||||
queries: {
|
||||
[UsersTableType.anomalies]: {
|
||||
jobIdSelection: [],
|
||||
intervalSelection: 'auto',
|
||||
},
|
||||
[UsersTableType.events]: {
|
||||
activePage: DEFAULT_TABLE_ACTIVE_PAGE,
|
||||
limit: DEFAULT_TABLE_LIMIT,
|
||||
flyout: {
|
||||
queries: {
|
||||
[UserAssetTableType.assetEntra]: (userAssetTable &&
|
||||
userAssetTable[UserAssetTableType.assetEntra]) ?? {
|
||||
fields: USER_ASSET_TABLE_DEFAULTS_FIELDS[UserAssetTableType.assetEntra],
|
||||
},
|
||||
|
||||
[UserAssetTableType.assetOkta]: (userAssetTable &&
|
||||
userAssetTable[UserAssetTableType.assetOkta]) ?? {
|
||||
fields: USER_ASSET_TABLE_DEFAULTS_FIELDS[UserAssetTableType.assetOkta],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const usersReducer = reducerWithInitialState(initialUsersState)
|
||||
.case(setUsersTablesActivePageToZero, (state) => ({
|
||||
...state,
|
||||
page: {
|
||||
...state.page,
|
||||
queries: setUsersPageQueriesActivePageToZero(state),
|
||||
},
|
||||
}))
|
||||
.case(updateTableActivePage, (state, { activePage, tableType }) => ({
|
||||
...state,
|
||||
page: {
|
||||
...state.page,
|
||||
queries: {
|
||||
...state.page.queries,
|
||||
[tableType]: {
|
||||
...state.page.queries[tableType],
|
||||
activePage,
|
||||
export const makeUsersReducer = (storage: Storage) =>
|
||||
reducerWithInitialState(getInitialUsersState(storage))
|
||||
.case(setUsersTablesActivePageToZero, (state) => ({
|
||||
...state,
|
||||
page: {
|
||||
...state.page,
|
||||
queries: setUsersPageQueriesActivePageToZero(state),
|
||||
},
|
||||
}))
|
||||
.case(updateTableActivePage, (state, { activePage, tableType }) => ({
|
||||
...state,
|
||||
page: {
|
||||
...state.page,
|
||||
queries: {
|
||||
...state.page.queries,
|
||||
[tableType]: {
|
||||
...state.page.queries[tableType],
|
||||
activePage,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
.case(updateTableLimit, (state, { limit, tableType }) => ({
|
||||
...state,
|
||||
page: {
|
||||
...state.page,
|
||||
queries: {
|
||||
...state.page.queries,
|
||||
[tableType]: {
|
||||
...state.page.queries[tableType],
|
||||
limit,
|
||||
}))
|
||||
.case(updateTableLimit, (state, { limit, tableType }) => ({
|
||||
...state,
|
||||
page: {
|
||||
...state.page,
|
||||
queries: {
|
||||
...state.page.queries,
|
||||
[tableType]: {
|
||||
...state.page.queries[tableType],
|
||||
limit,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
.case(updateTableSorting, (state, { sort, tableType }) => ({
|
||||
...state,
|
||||
page: {
|
||||
...state.page,
|
||||
queries: {
|
||||
...state.page.queries,
|
||||
[tableType]: {
|
||||
...state.page.queries[tableType],
|
||||
sort,
|
||||
activePage: DEFAULT_TABLE_ACTIVE_PAGE,
|
||||
}))
|
||||
.case(updateTableSorting, (state, { sort, tableType }) => ({
|
||||
...state,
|
||||
page: {
|
||||
...state.page,
|
||||
queries: {
|
||||
...state.page.queries,
|
||||
[tableType]: {
|
||||
...state.page.queries[tableType],
|
||||
sort,
|
||||
activePage: DEFAULT_TABLE_ACTIVE_PAGE,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
.case(updateUserRiskScoreSeverityFilter, (state, { severitySelection }) => ({
|
||||
...state,
|
||||
page: {
|
||||
...state.page,
|
||||
queries: {
|
||||
...state.page.queries,
|
||||
[UsersTableType.risk]: {
|
||||
...state.page.queries[UsersTableType.risk],
|
||||
severitySelection,
|
||||
activePage: DEFAULT_TABLE_ACTIVE_PAGE,
|
||||
}))
|
||||
.case(updateUserRiskScoreSeverityFilter, (state, { severitySelection }) => ({
|
||||
...state,
|
||||
page: {
|
||||
...state.page,
|
||||
queries: {
|
||||
...state.page.queries,
|
||||
[UsersTableType.risk]: {
|
||||
...state.page.queries[UsersTableType.risk],
|
||||
severitySelection,
|
||||
activePage: DEFAULT_TABLE_ACTIVE_PAGE,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
.case(updateUsersAnomaliesJobIdFilter, (state, { jobIds, usersType }) => {
|
||||
if (usersType === 'page') {
|
||||
return set('page.queries.anomalies.jobIdSelection', jobIds, state);
|
||||
} else {
|
||||
return set('details.queries.anomalies.jobIdSelection', jobIds, state);
|
||||
}
|
||||
})
|
||||
.case(updateUsersAnomaliesInterval, (state, { interval, usersType }) => {
|
||||
if (usersType === 'page') {
|
||||
return set('page.queries.anomalies.intervalSelection', interval, state);
|
||||
} else {
|
||||
return set('details.queries.anomalies.intervalSelection', interval, state);
|
||||
}
|
||||
})
|
||||
.build();
|
||||
}))
|
||||
.case(updateUsersAnomaliesJobIdFilter, (state, { jobIds, usersType }) => {
|
||||
if (usersType === 'page') {
|
||||
return set('page.queries.anomalies.jobIdSelection', jobIds, state);
|
||||
} else {
|
||||
return set('details.queries.anomalies.jobIdSelection', jobIds, state);
|
||||
}
|
||||
})
|
||||
.case(updateUsersAnomaliesInterval, (state, { interval, usersType }) => {
|
||||
if (usersType === 'page') {
|
||||
return set('page.queries.anomalies.intervalSelection', interval, state);
|
||||
} else {
|
||||
return set('details.queries.anomalies.intervalSelection', interval, state);
|
||||
}
|
||||
})
|
||||
.case(addUserAssetTableField, (state, { fieldName, tableId }) => ({
|
||||
...state,
|
||||
flyout: {
|
||||
...state.flyout,
|
||||
queries: addAssetTableField({
|
||||
fieldName,
|
||||
tableId,
|
||||
tableById: state.flyout.queries,
|
||||
}),
|
||||
},
|
||||
}))
|
||||
.case(removeUserAssetTableField, (state, { fieldName, tableId }) => ({
|
||||
...state,
|
||||
flyout: {
|
||||
...state.flyout,
|
||||
queries: removeAssetTableField({
|
||||
tableId,
|
||||
fieldName,
|
||||
tableById: state.flyout.queries,
|
||||
}),
|
||||
},
|
||||
}))
|
||||
.build();
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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 { mockGlobalState } from '../../../common/mock';
|
||||
import { UserAssetTableType } from './model';
|
||||
import { selectUserAssetTableById } from './selectors';
|
||||
|
||||
describe('Users store selector', () => {
|
||||
test('selectUserAssetTableById', () => {
|
||||
expect(selectUserAssetTableById(mockGlobalState, UserAssetTableType.assetOkta)).toBe(
|
||||
mockGlobalState.users.flyout.queries[UserAssetTableType.assetOkta]
|
||||
);
|
||||
expect(selectUserAssetTableById(mockGlobalState, UserAssetTableType.assetEntra)).toBe(
|
||||
mockGlobalState.users.flyout.queries[UserAssetTableType.assetEntra]
|
||||
);
|
||||
});
|
||||
});
|
|
@ -9,13 +9,26 @@ import { createSelector } from 'reselect';
|
|||
|
||||
import type { State } from '../../../common/store/types';
|
||||
|
||||
import type { UserDetailsPageModel, UsersPageModel, UsersType } from './model';
|
||||
import { UsersTableType } from './model';
|
||||
import type {
|
||||
UserAssetQuery,
|
||||
UserDetailsPageModel,
|
||||
UserFlyoutQueries,
|
||||
UserAssetTableType,
|
||||
UsersPageModel,
|
||||
} from './model';
|
||||
import { UsersTableType, UsersType } from './model';
|
||||
|
||||
const selectUserPage = (state: State): UsersPageModel => state.users.page;
|
||||
|
||||
const selectUsers = (state: State, usersType: UsersType): UsersPageModel | UserDetailsPageModel =>
|
||||
state.users[usersType];
|
||||
const selectUsersAndDetailsPage = (
|
||||
state: State,
|
||||
usersType: UsersType
|
||||
): UsersPageModel | UserDetailsPageModel | null => {
|
||||
if (usersType === UsersType.details || usersType === UsersType.page) {
|
||||
return state.users[usersType];
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const allUsersSelector = () =>
|
||||
createSelector(selectUserPage, (users) => users.queries[UsersTableType.allUsers]);
|
||||
|
@ -30,7 +43,21 @@ export const authenticationsSelector = () =>
|
|||
createSelector(selectUserPage, (users) => users.queries[UsersTableType.authentications]);
|
||||
|
||||
export const usersAnomaliesJobIdFilterSelector = () =>
|
||||
createSelector(selectUsers, (users) => users.queries[UsersTableType.anomalies].jobIdSelection);
|
||||
createSelector(
|
||||
selectUsersAndDetailsPage,
|
||||
(users) => users?.queries[UsersTableType.anomalies].jobIdSelection ?? []
|
||||
);
|
||||
|
||||
export const usersAnomaliesIntervalSelector = () =>
|
||||
createSelector(selectUsers, (users) => users.queries[UsersTableType.anomalies].intervalSelection);
|
||||
createSelector(
|
||||
selectUsersAndDetailsPage,
|
||||
(users) => users?.queries[UsersTableType.anomalies].intervalSelection ?? 'auto'
|
||||
);
|
||||
|
||||
export const selectUserAssetTableById = (
|
||||
state: State,
|
||||
tableId: UserAssetTableType
|
||||
): UserAssetQuery => state.users.flyout.queries[tableId];
|
||||
|
||||
export const selectUserAssetTables = (state: State): UserFlyoutQueries =>
|
||||
state.users.flyout.queries;
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* 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 type { Storage } from '@kbn/kibana-utils-plugin/public';
|
||||
import type { UserAssetQuery, UserAssetTableType } from './model';
|
||||
export const LOCAL_STORAGE_KEY = 'securityUserFlyoutAssetTable';
|
||||
|
||||
export const getUserAssetTableFromStorage = (storage: Storage) => storage.get(LOCAL_STORAGE_KEY);
|
||||
|
||||
export const persistUserAssetTableInStorage = (
|
||||
storage: Storage,
|
||||
id: UserAssetTableType,
|
||||
table: UserAssetQuery
|
||||
) => {
|
||||
const tables = storage.get(LOCAL_STORAGE_KEY);
|
||||
storage.set(LOCAL_STORAGE_KEY, {
|
||||
...tables,
|
||||
[id]: table,
|
||||
});
|
||||
};
|
|
@ -11,7 +11,6 @@ import { FLYOUT_BODY_TEST_ID } from './test_ids';
|
|||
import type { RightPanelPaths } from '.';
|
||||
import type { RightPanelTabsType } from './tabs';
|
||||
import { FlyoutBody } from '../../shared/components/flyout_body';
|
||||
import {} from './tabs';
|
||||
|
||||
export interface PanelContentProps {
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* 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 from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { TestProviders } from '../../../common/mock';
|
||||
import { AssetDocumentLeftPanel } from '.';
|
||||
import { JSON_TAB_TEST_ID, TABLE_TAB_TEST_ID } from './test_ids';
|
||||
import { RightPanelContext } from '../../document_details/right/context';
|
||||
import { mockContextValue } from '../../document_details/right/mocks/mock_context';
|
||||
|
||||
describe('<AssetDocumentLeftPanel />', () => {
|
||||
it('renders', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<RightPanelContext.Provider value={mockContextValue}>
|
||||
<AssetDocumentLeftPanel />
|
||||
</RightPanelContext.Provider>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(getByTestId(TABLE_TAB_TEST_ID)).toBeInTheDocument();
|
||||
expect(getByTestId(JSON_TAB_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should preselect the table tab', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<RightPanelContext.Provider value={mockContextValue}>
|
||||
<AssetDocumentLeftPanel />
|
||||
</RightPanelContext.Provider>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(getByTestId('securitySolutionFlyoutAssetTableTab')).toHaveAttribute(
|
||||
'aria-selected',
|
||||
'true'
|
||||
);
|
||||
});
|
||||
|
||||
it('should select json tab when path tab is json', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<RightPanelContext.Provider value={mockContextValue}>
|
||||
<AssetDocumentLeftPanel path={{ tab: 'json' }} />
|
||||
</RightPanelContext.Provider>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(getByTestId('securitySolutionFlyoutAssetJsonTab')).toHaveAttribute(
|
||||
'aria-selected',
|
||||
'true'
|
||||
);
|
||||
});
|
||||
|
||||
it('should select table tab when path tab is table', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<RightPanelContext.Provider value={mockContextValue}>
|
||||
<AssetDocumentLeftPanel path={{ tab: 'table' }} />
|
||||
</RightPanelContext.Provider>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(getByTestId('securitySolutionFlyoutAssetTableTab')).toHaveAttribute(
|
||||
'aria-selected',
|
||||
'true'
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* 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 type { FC } from 'react';
|
||||
import React, { memo, useMemo } from 'react';
|
||||
import type { FlyoutPanelProps, PanelPath } from '@kbn/expandable-flyout';
|
||||
import { useExpandableFlyoutContext } from '@kbn/expandable-flyout';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { useRightPanelContext } from '../../document_details/right/context';
|
||||
import { PanelHeader } from '../../document_details/right/header';
|
||||
import type { RightPanelTabsType } from '../../document_details/right/tabs';
|
||||
import { PanelContent } from '../../document_details/right/content';
|
||||
import { JsonTab } from '../../document_details/right/tabs/json_tab';
|
||||
import { TableTab } from '../../document_details/right/tabs/table_tab';
|
||||
import { JSON_TAB_TEST_ID, TABLE_TAB_TEST_ID } from './test_ids';
|
||||
export type RightPanelPaths = 'overview' | 'table' | 'json';
|
||||
export const AssetDocumentLeftPanelKey: AssetDocumentLeftPanelProps['key'] =
|
||||
'asset-document-details-left';
|
||||
|
||||
export interface AssetDocumentLeftPanelProps extends FlyoutPanelProps {
|
||||
key: 'asset-document-details-left';
|
||||
path?: PanelPath;
|
||||
params?: {
|
||||
id: string;
|
||||
indexName: string;
|
||||
scopeId: string;
|
||||
};
|
||||
}
|
||||
|
||||
const tabs: RightPanelTabsType = [
|
||||
{
|
||||
id: 'table',
|
||||
'data-test-subj': TABLE_TAB_TEST_ID,
|
||||
name: (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.flyout.left.header.tableTabLabel"
|
||||
defaultMessage="Table"
|
||||
/>
|
||||
),
|
||||
content: <TableTab />,
|
||||
},
|
||||
{
|
||||
id: 'json',
|
||||
'data-test-subj': JSON_TAB_TEST_ID,
|
||||
name: (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.flyout.left.header.jsonTabLabel"
|
||||
defaultMessage="JSON"
|
||||
/>
|
||||
),
|
||||
content: <JsonTab />,
|
||||
},
|
||||
];
|
||||
|
||||
export const AssetDocumentLeftPanel: FC<Partial<AssetDocumentLeftPanelProps>> = memo(({ path }) => {
|
||||
const { openLeftPanel } = useExpandableFlyoutContext();
|
||||
const { eventId, indexName, scopeId } = useRightPanelContext();
|
||||
|
||||
const selectedTabId = useMemo(() => {
|
||||
const defaultTab = tabs[0].id;
|
||||
if (!path) return defaultTab;
|
||||
return tabs.map((tab) => tab.id).find((tabId) => tabId === path.tab) ?? defaultTab;
|
||||
}, [path]);
|
||||
|
||||
const setSelectedTabId = (tabId: string) => {
|
||||
openLeftPanel({
|
||||
id: AssetDocumentLeftPanelKey,
|
||||
path: {
|
||||
tab: tabId,
|
||||
},
|
||||
params: {
|
||||
id: eventId,
|
||||
indexName,
|
||||
scopeId,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<PanelHeader tabs={tabs} selectedTabId={selectedTabId} setSelectedTabId={setSelectedTabId} />
|
||||
<PanelContent tabs={tabs} selectedTabId={selectedTabId} />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
AssetDocumentLeftPanel.displayName = 'AssetDocumentLeftPanel';
|
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* 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 { PREFIX } from '../../shared/test_ids';
|
||||
|
||||
export const TABLE_TAB_TEST_ID = `${PREFIX}AssetTableTab` as const;
|
||||
export const JSON_TAB_TEST_ID = `${PREFIX}AssetJsonTab` as const;
|
|
@ -28,7 +28,7 @@ export const Default: Story<void> = () => {
|
|||
<StorybookProviders>
|
||||
<ExpandableFlyoutContext.Provider value={flyoutContextValue}>
|
||||
<div style={{ maxWidth: '300px' }}>
|
||||
<RiskSummary riskScoreData={mockRiskScoreState} queryId={'testQuery'} />
|
||||
<RiskSummary riskScoreData={{ ...mockRiskScoreState, data: [] }} queryId={'testQuery'} />
|
||||
</div>
|
||||
</ExpandableFlyoutContext.Provider>
|
||||
</StorybookProviders>
|
||||
|
|
|
@ -12,7 +12,7 @@ import type { ExpandableFlyoutContextValue } from '@kbn/expandable-flyout/src/co
|
|||
import { ExpandableFlyoutContext } from '@kbn/expandable-flyout/src/context';
|
||||
import { StorybookProviders } from '../../../common/mock/storybook_providers';
|
||||
import {
|
||||
mockManagedUser,
|
||||
mockManagedUserData,
|
||||
mockObservedUser,
|
||||
mockRiskScoreState,
|
||||
} from '../../../timelines/components/side_panel/new_user_detail/__mocks__';
|
||||
|
@ -23,6 +23,8 @@ const flyoutContextValue = {
|
|||
panels: {},
|
||||
} as unknown as ExpandableFlyoutContextValue;
|
||||
|
||||
const riskScoreData = { ...mockRiskScoreState, data: [] };
|
||||
|
||||
storiesOf('Components/UserPanelContent', module)
|
||||
.addDecorator((storyFn) => (
|
||||
<StorybookProviders>
|
||||
|
@ -35,9 +37,9 @@ storiesOf('Components/UserPanelContent', module)
|
|||
))
|
||||
.add('default', () => (
|
||||
<UserPanelContent
|
||||
managedUser={mockManagedUser}
|
||||
managedUser={mockManagedUserData}
|
||||
observedUser={mockObservedUser}
|
||||
riskScoreState={mockRiskScoreState}
|
||||
riskScoreState={riskScoreData}
|
||||
contextID={'test-user-details'}
|
||||
scopeId={'test-scopeId'}
|
||||
isDraggable={false}
|
||||
|
@ -46,20 +48,12 @@ storiesOf('Components/UserPanelContent', module)
|
|||
.add('integration disabled', () => (
|
||||
<UserPanelContent
|
||||
managedUser={{
|
||||
details: undefined,
|
||||
data: undefined,
|
||||
isLoading: false,
|
||||
isIntegrationEnabled: false,
|
||||
firstSeen: {
|
||||
isLoading: false,
|
||||
date: undefined,
|
||||
},
|
||||
lastSeen: {
|
||||
isLoading: false,
|
||||
date: undefined,
|
||||
},
|
||||
}}
|
||||
observedUser={mockObservedUser}
|
||||
riskScoreState={mockRiskScoreState}
|
||||
riskScoreState={riskScoreData}
|
||||
contextID={'test-user-details'}
|
||||
scopeId={'test-scopeId'}
|
||||
isDraggable={false}
|
||||
|
@ -68,20 +62,12 @@ storiesOf('Components/UserPanelContent', module)
|
|||
.add('no managed data', () => (
|
||||
<UserPanelContent
|
||||
managedUser={{
|
||||
details: undefined,
|
||||
data: undefined,
|
||||
isLoading: false,
|
||||
isIntegrationEnabled: true,
|
||||
firstSeen: {
|
||||
isLoading: false,
|
||||
date: undefined,
|
||||
},
|
||||
lastSeen: {
|
||||
isLoading: false,
|
||||
date: undefined,
|
||||
},
|
||||
}}
|
||||
observedUser={mockObservedUser}
|
||||
riskScoreState={mockRiskScoreState}
|
||||
riskScoreState={riskScoreData}
|
||||
contextID={'test-user-details'}
|
||||
scopeId={'test-scopeId'}
|
||||
isDraggable={false}
|
||||
|
@ -89,7 +75,7 @@ storiesOf('Components/UserPanelContent', module)
|
|||
))
|
||||
.add('no observed data', () => (
|
||||
<UserPanelContent
|
||||
managedUser={mockManagedUser}
|
||||
managedUser={mockManagedUserData}
|
||||
observedUser={{
|
||||
details: {
|
||||
user: {
|
||||
|
@ -115,7 +101,7 @@ storiesOf('Components/UserPanelContent', module)
|
|||
},
|
||||
anomalies: { isLoading: false, anomalies: null, jobNameById: {} },
|
||||
}}
|
||||
riskScoreState={mockRiskScoreState}
|
||||
riskScoreState={riskScoreData}
|
||||
contextID={'test-user-details'}
|
||||
scopeId={'test-scopeId'}
|
||||
isDraggable={false}
|
||||
|
@ -124,17 +110,9 @@ storiesOf('Components/UserPanelContent', module)
|
|||
.add('loading', () => (
|
||||
<UserPanelContent
|
||||
managedUser={{
|
||||
details: undefined,
|
||||
data: undefined,
|
||||
isLoading: true,
|
||||
isIntegrationEnabled: true,
|
||||
firstSeen: {
|
||||
isLoading: true,
|
||||
date: undefined,
|
||||
},
|
||||
lastSeen: {
|
||||
isLoading: true,
|
||||
date: undefined,
|
||||
},
|
||||
}}
|
||||
observedUser={{
|
||||
details: {
|
||||
|
@ -161,7 +139,7 @@ storiesOf('Components/UserPanelContent', module)
|
|||
},
|
||||
anomalies: { isLoading: true, anomalies: null, jobNameById: {} },
|
||||
}}
|
||||
riskScoreState={mockRiskScoreState}
|
||||
riskScoreState={riskScoreData}
|
||||
contextID={'test-user-details'}
|
||||
scopeId={'test-scopeId'}
|
||||
isDraggable={false}
|
||||
|
|
|
@ -8,11 +8,11 @@
|
|||
import { EuiHorizontalRule } from '@elastic/eui';
|
||||
|
||||
import React from 'react';
|
||||
import { ManagedUser } from '../../../timelines/components/side_panel/new_user_detail/managed_user';
|
||||
import type {
|
||||
ManagedUserData,
|
||||
ObservedUserData,
|
||||
} from '../../../timelines/components/side_panel/new_user_detail/types';
|
||||
import { ManagedUser } from '../../../timelines/components/side_panel/new_user_detail/managed_user';
|
||||
import { ObservedUser } from '../../../timelines/components/side_panel/new_user_detail/observed_user';
|
||||
import type { RiskScoreEntity } from '../../../../common/search_strategy';
|
||||
import type { RiskScoreState } from '../../../explore/containers/risk_score';
|
||||
|
@ -52,12 +52,7 @@ export const UserPanelContent = ({
|
|||
isDraggable={isDraggable}
|
||||
/>
|
||||
<EuiHorizontalRule margin="m" />
|
||||
<ManagedUser
|
||||
managedUser={managedUser}
|
||||
contextID={contextID}
|
||||
scopeId={scopeId}
|
||||
isDraggable={isDraggable}
|
||||
/>
|
||||
<ManagedUser managedUser={managedUser} contextID={contextID} isDraggable={isDraggable} />
|
||||
</FlyoutBody>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -5,18 +5,20 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ManagedUserDatasetKey } from '../../../../common/search_strategy/security_solution/users/managed_details';
|
||||
import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { TestProviders } from '../../../common/mock';
|
||||
import {
|
||||
mockManagedUser,
|
||||
managedUserDetails,
|
||||
mockManagedUserData,
|
||||
mockObservedUser,
|
||||
} from '../../../timelines/components/side_panel/new_user_detail/__mocks__';
|
||||
import { UserPanelHeader } from './header';
|
||||
|
||||
const mockProps = {
|
||||
userName: 'test',
|
||||
managedUser: mockManagedUser,
|
||||
managedUser: mockManagedUserData,
|
||||
observedUser: mockObservedUser,
|
||||
};
|
||||
|
||||
|
@ -57,16 +59,23 @@ describe('UserDetailsContent', () => {
|
|||
|
||||
it('renders managed user date when it is bigger than observed user date', () => {
|
||||
const futureDay = '2989-03-07T20:00:00.000Z';
|
||||
const entraManagedUser = managedUserDetails[ManagedUserDatasetKey.ENTRA]!;
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<UserPanelHeader
|
||||
{...{
|
||||
...mockProps,
|
||||
managedUser: {
|
||||
...mockManagedUser,
|
||||
lastSeen: {
|
||||
isLoading: false,
|
||||
date: futureDay,
|
||||
...mockManagedUserData,
|
||||
data: {
|
||||
[ManagedUserDatasetKey.ENTRA]: {
|
||||
...entraManagedUser,
|
||||
fields: {
|
||||
...entraManagedUser.fields,
|
||||
'@timestamp': [futureDay],
|
||||
},
|
||||
},
|
||||
[ManagedUserDatasetKey.OKTA]: undefined,
|
||||
},
|
||||
},
|
||||
}}
|
||||
|
@ -109,17 +118,17 @@ describe('UserDetailsContent', () => {
|
|||
expect(queryByTestId('user-panel-header-observed-badge')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render managed badge when lastSeen date is undefined', () => {
|
||||
it('does not render managed badge when managed data is undefined', () => {
|
||||
const { queryByTestId } = render(
|
||||
<TestProviders>
|
||||
<UserPanelHeader
|
||||
{...{
|
||||
...mockProps,
|
||||
managedUser: {
|
||||
...mockManagedUser,
|
||||
lastSeen: {
|
||||
isLoading: false,
|
||||
date: undefined,
|
||||
...mockManagedUserData,
|
||||
data: {
|
||||
[ManagedUserDatasetKey.ENTRA]: undefined,
|
||||
[ManagedUserDatasetKey.OKTA]: undefined,
|
||||
},
|
||||
},
|
||||
}}
|
||||
|
|
|
@ -10,6 +10,7 @@ import { FormattedMessage } from '@kbn/i18n-react';
|
|||
import React, { useMemo } from 'react';
|
||||
import { max } from 'lodash/fp';
|
||||
import { SecurityPageName } from '@kbn/security-solution-navigation';
|
||||
import { ManagedUserDatasetKey } from '../../../../common/search_strategy/security_solution/users/managed_details';
|
||||
import { getUsersDetailsUrl } from '../../../common/components/link_to/redirect_to_users';
|
||||
import type {
|
||||
ManagedUserData,
|
||||
|
@ -28,10 +29,20 @@ interface UserPanelHeaderProps {
|
|||
}
|
||||
|
||||
export const UserPanelHeader = ({ userName, observedUser, managedUser }: UserPanelHeaderProps) => {
|
||||
const oktaTimestamp = managedUser.data?.[ManagedUserDatasetKey.OKTA]?.fields?.[
|
||||
'@timestamp'
|
||||
][0] as string | undefined;
|
||||
const entraTimestamp = managedUser.data?.[ManagedUserDatasetKey.ENTRA]?.fields?.[
|
||||
'@timestamp'
|
||||
][0] as string | undefined;
|
||||
|
||||
const isManaged = !!oktaTimestamp || !!entraTimestamp;
|
||||
const lastSeenDate = useMemo(
|
||||
() =>
|
||||
max([observedUser.lastSeen, managedUser.lastSeen].map((el) => el.date && new Date(el.date))),
|
||||
[managedUser.lastSeen, observedUser.lastSeen]
|
||||
max(
|
||||
[observedUser.lastSeen.date, entraTimestamp, oktaTimestamp].map((el) => el && new Date(el))
|
||||
),
|
||||
[oktaTimestamp, entraTimestamp, observedUser.lastSeen]
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -66,7 +77,7 @@ export const UserPanelHeader = ({ userName, observedUser, managedUser }: UserPan
|
|||
)}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
{managedUser.lastSeen.date && (
|
||||
{isManaged && (
|
||||
<EuiBadge data-test-subj="user-panel-header-managed-badge" color="hollow">
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.flyout.entityDetails.user.managedBadge"
|
||||
|
|
|
@ -13,7 +13,7 @@ import { UserPanel } from '.';
|
|||
import { mockRiskScoreState } from './mocks';
|
||||
|
||||
import {
|
||||
mockManagedUser,
|
||||
mockManagedUserData,
|
||||
mockObservedUser,
|
||||
} from '../../../timelines/components/side_panel/new_user_detail/__mocks__';
|
||||
|
||||
|
@ -31,24 +31,27 @@ jest.mock('../../../explore/containers/risk_score', () => ({
|
|||
useRiskScore: () => mockedUseRiskScore(),
|
||||
}));
|
||||
|
||||
const mockedUseManagedUser = jest.fn().mockReturnValue(mockManagedUser);
|
||||
const mockedUseManagedUser = jest.fn().mockReturnValue(mockManagedUserData);
|
||||
const mockedUseObservedUser = jest.fn().mockReturnValue(mockObservedUser);
|
||||
|
||||
jest.mock('../../../timelines/components/side_panel/new_user_detail/hooks', () => {
|
||||
const originalModule = jest.requireActual(
|
||||
'../../../timelines/components/side_panel/new_user_detail/hooks'
|
||||
);
|
||||
return {
|
||||
...originalModule,
|
||||
jest.mock(
|
||||
'../../../timelines/components/side_panel/new_user_detail/hooks/use_managed_user',
|
||||
() => ({
|
||||
useManagedUser: () => mockedUseManagedUser(),
|
||||
})
|
||||
);
|
||||
|
||||
jest.mock(
|
||||
'../../../timelines/components/side_panel/new_user_detail/hooks/use_observed_user',
|
||||
() => ({
|
||||
useObservedUser: () => mockedUseObservedUser(),
|
||||
};
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
describe('UserPanel', () => {
|
||||
beforeEach(() => {
|
||||
mockedUseRiskScore.mockReturnValue(mockRiskScoreState);
|
||||
mockedUseManagedUser.mockReturnValue(mockManagedUser);
|
||||
mockedUseManagedUser.mockReturnValue(mockManagedUserData);
|
||||
mockedUseObservedUser.mockReturnValue(mockObservedUser);
|
||||
});
|
||||
|
||||
|
@ -97,7 +100,7 @@ describe('UserPanel', () => {
|
|||
|
||||
it('renders loading state when managed user is loading', () => {
|
||||
mockedUseManagedUser.mockReturnValue({
|
||||
...mockManagedUser,
|
||||
...mockManagedUserData,
|
||||
isLoading: true,
|
||||
});
|
||||
|
||||
|
|
|
@ -8,14 +8,12 @@
|
|||
import React, { useCallback, useMemo } from 'react';
|
||||
import type { FlyoutPanelProps } from '@kbn/expandable-flyout';
|
||||
import { useExpandableFlyoutContext } from '@kbn/expandable-flyout';
|
||||
import { useManagedUser } from '../../../timelines/components/side_panel/new_user_detail/hooks/use_managed_user';
|
||||
import { useObservedUser } from '../../../timelines/components/side_panel/new_user_detail/hooks/use_observed_user';
|
||||
import { useQueryInspector } from '../../../common/components/page/manage_query';
|
||||
import { UsersType } from '../../../explore/users/store/model';
|
||||
import { getCriteriaFromUsersType } from '../../../common/components/ml/criteria/get_criteria_from_users_type';
|
||||
import { useGlobalTime } from '../../../common/containers/use_global_time';
|
||||
import {
|
||||
useManagedUser,
|
||||
useObservedUser,
|
||||
} from '../../../timelines/components/side_panel/new_user_detail/hooks';
|
||||
import { AnomalyTableProvider } from '../../../common/components/ml/anomaly/anomaly_table_provider';
|
||||
import { buildUserNamesFilter } from '../../../../common/search_strategy';
|
||||
import { useRiskScore } from '../../../explore/containers/risk_score';
|
||||
|
@ -62,7 +60,7 @@ export const UserPanel = ({ contextID, scopeId, userName, isDraggable }: UserPan
|
|||
const { to, from, isInitializing, setQuery, deleteQuery } = useGlobalTime();
|
||||
|
||||
const observedUser = useObservedUser(userName);
|
||||
const managedUser = useManagedUser(userName);
|
||||
const managedUser = useManagedUser(userName, observedUser);
|
||||
|
||||
const { data: userRisk } = riskScoreState;
|
||||
const userRiskData = userRisk && userRisk.length > 0 ? userRisk[0] : undefined;
|
||||
|
|
|
@ -30,6 +30,12 @@ import type { UserPanelExpandableFlyoutProps } from './entity_details/user_right
|
|||
import { UserPanel, UserPanelKey } from './entity_details/user_right';
|
||||
import type { RiskInputsExpandableFlyoutProps } from './entity_details/risk_inputs_left';
|
||||
import { RiskInputsPanel, RiskInputsPanelKey } from './entity_details/risk_inputs_left';
|
||||
import type { AssetDocumentLeftPanelProps } from './entity_details/asset_document_left';
|
||||
import {
|
||||
AssetDocumentLeftPanel,
|
||||
AssetDocumentLeftPanelKey,
|
||||
} from './entity_details/asset_document_left';
|
||||
|
||||
/**
|
||||
* List of all panels that will be used within the document details expandable flyout.
|
||||
* This needs to be passed to the expandable flyout registeredPanels property.
|
||||
|
@ -43,6 +49,14 @@ const expandableFlyoutDocumentsPanels: ExpandableFlyoutProps['registeredPanels']
|
|||
</RightPanelProvider>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: AssetDocumentLeftPanelKey,
|
||||
component: (props) => (
|
||||
<RightPanelProvider {...(props as RightPanelProps).params}>
|
||||
<AssetDocumentLeftPanel {...(props as AssetDocumentLeftPanelProps)} />
|
||||
</RightPanelProvider>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: DocumentDetailsLeftPanelKey,
|
||||
component: (props) => (
|
||||
|
|
|
@ -5,9 +5,14 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type {
|
||||
ManagedUserHits,
|
||||
ManagedUserFields,
|
||||
} from '../../../../../../common/search_strategy/security_solution/users/managed_details';
|
||||
import { ManagedUserDatasetKey } from '../../../../../../common/search_strategy/security_solution/users/managed_details';
|
||||
import { RiskSeverity } from '../../../../../../common/search_strategy';
|
||||
import { mockAnomalies } from '../../../../../common/components/ml/mock';
|
||||
import type { ObservedUserData } from '../types';
|
||||
import type { ManagedUserData, ObservedUserData } from '../types';
|
||||
|
||||
const userRiskScore = {
|
||||
'@timestamp': '123456',
|
||||
|
@ -41,20 +46,6 @@ export const mockRiskScoreState = {
|
|||
|
||||
const anomaly = mockAnomalies.anomalies[0];
|
||||
|
||||
export const managedUserDetails = {
|
||||
'@timestamp': '',
|
||||
agent: {},
|
||||
host: {},
|
||||
event: {},
|
||||
user: {
|
||||
id: '123456',
|
||||
last_name: 'user',
|
||||
first_name: 'test',
|
||||
full_name: 'test user',
|
||||
phone: ['123456', '654321'],
|
||||
},
|
||||
};
|
||||
|
||||
export const observedUserDetails = {
|
||||
user: {
|
||||
id: ['1234', '321'],
|
||||
|
@ -69,20 +60,6 @@ export const observedUserDetails = {
|
|||
},
|
||||
};
|
||||
|
||||
export const mockManagedUser = {
|
||||
details: managedUserDetails,
|
||||
isLoading: false,
|
||||
isIntegrationEnabled: true,
|
||||
firstSeen: {
|
||||
isLoading: false,
|
||||
date: '2023-03-23T20:03:17.489Z',
|
||||
},
|
||||
lastSeen: {
|
||||
isLoading: false,
|
||||
date: '2023-03-23T20:03:17.489Z',
|
||||
},
|
||||
};
|
||||
|
||||
export const mockObservedUser: ObservedUserData = {
|
||||
details: observedUserDetails,
|
||||
isLoading: false,
|
||||
|
@ -103,3 +80,44 @@ export const mockObservedUser: ObservedUserData = {
|
|||
jobNameById: { [anomaly.jobId]: 'job_name' },
|
||||
},
|
||||
};
|
||||
|
||||
export const mockOktaUserFields: ManagedUserFields = {
|
||||
'@timestamp': ['2023-11-16T13:42:23.074Z'],
|
||||
'event.dataset': [ManagedUserDatasetKey.OKTA],
|
||||
'user.profile.last_name': ['Okta last name'],
|
||||
'user.profile.first_name': ['Okta first name'],
|
||||
'user.profile.mobile_phone': ['1234567'],
|
||||
'user.profile.job_title': ['Okta Unit tester'],
|
||||
'user.geo.city_name': ["A'dam"],
|
||||
'user.geo.country_iso_code': ['NL'],
|
||||
'user.id': ['00ud9ohoh9ww644Px5d7'],
|
||||
'user.email': ['okta.test.user@elastic.co'],
|
||||
'user.name': ['okta.test.user@elastic.co'],
|
||||
};
|
||||
|
||||
export const mockEntraUserFields: ManagedUserFields = {
|
||||
'@timestamp': ['2023-11-16T13:42:23.074Z'],
|
||||
'event.dataset': [ManagedUserDatasetKey.ENTRA],
|
||||
'user.id': ['12345'],
|
||||
'user.first_name': ['Entra first name'],
|
||||
'user.last_name': ['Entra last name'],
|
||||
'user.full_name': ['Entra full name'],
|
||||
'user.phone': ['123456'],
|
||||
'user.job_title': ['Entra Unit tester'],
|
||||
'user.work.location_name': ['USA, CA'],
|
||||
};
|
||||
|
||||
export const managedUserDetails: ManagedUserHits = {
|
||||
[ManagedUserDatasetKey.ENTRA]: {
|
||||
fields: mockEntraUserFields,
|
||||
_index: 'test-index',
|
||||
_id: '123-test',
|
||||
},
|
||||
[ManagedUserDatasetKey.OKTA]: undefined,
|
||||
};
|
||||
|
||||
export const mockManagedUserData: ManagedUserData = {
|
||||
data: managedUserDetails,
|
||||
isLoading: false,
|
||||
isIntegrationEnabled: true,
|
||||
};
|
||||
|
|
|
@ -10,7 +10,10 @@ import React, { useCallback } from 'react';
|
|||
import { head } from 'lodash/fp';
|
||||
import { euiLightVars } from '@kbn/ui-theme';
|
||||
import type { EuiBasicTableColumn } from '@elastic/eui';
|
||||
import { EuiToolTip } from '@elastic/eui';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { EcsFlat } from '@kbn/ecs';
|
||||
import { SourcererScopeName } from '../../../../common/store/sourcerer/model';
|
||||
import { DefaultFieldRenderer } from '../../field_renderers/field_renderers';
|
||||
import type {
|
||||
ManagedUsersTableColumns,
|
||||
|
@ -32,21 +35,22 @@ import { getSourcererScopeId } from '../../../../helpers';
|
|||
const fieldColumn: EuiBasicTableColumn<ObservedUserTable | ManagedUserTable> = {
|
||||
name: i18n.FIELD_COLUMN_TITLE,
|
||||
field: 'label',
|
||||
render: (label: string) => (
|
||||
<span
|
||||
css={css`
|
||||
font-weight: ${euiLightVars.euiFontWeightMedium};
|
||||
color: ${euiLightVars.euiTitleColor};
|
||||
`}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
render: (label: string, { field }) => (
|
||||
<EuiToolTip content={EcsFlat[field as keyof typeof EcsFlat]?.short ?? field}>
|
||||
<span
|
||||
css={css`
|
||||
font-weight: ${euiLightVars.euiFontWeightMedium};
|
||||
color: ${euiLightVars.euiTitleColor};
|
||||
`}
|
||||
>
|
||||
{label ?? field}
|
||||
</span>
|
||||
</EuiToolTip>
|
||||
),
|
||||
};
|
||||
|
||||
export const getManagedUserTableColumns = (
|
||||
contextID: string,
|
||||
scopeId: string,
|
||||
isDraggable: boolean
|
||||
): ManagedUsersTableColumns => [
|
||||
fieldColumn,
|
||||
|
@ -56,11 +60,11 @@ export const getManagedUserTableColumns = (
|
|||
render: (value: ManagedUserTable['value'], { field }) => {
|
||||
return field && value ? (
|
||||
<DefaultFieldRenderer
|
||||
rowItems={[value]}
|
||||
rowItems={value.map((v) => value.toString())}
|
||||
attrName={field}
|
||||
idPrefix={contextID ? `managedUser-${contextID}` : 'managedUser'}
|
||||
isDraggable={isDraggable}
|
||||
sourcererScopeId={getSourcererScopeId(scopeId)}
|
||||
sourcererScopeId={SourcererScopeName.default}
|
||||
/>
|
||||
) : (
|
||||
defaultToEmptyTag(value)
|
||||
|
|
|
@ -4,9 +4,17 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
export const getEntraUserIndex = (spaceId: string = 'default') =>
|
||||
`logs-entityanalytics_entra_id.user-${spaceId}`;
|
||||
|
||||
export const ENTRA_ID_PACKAGE_NAME = 'entityanalytics_entra_id';
|
||||
|
||||
export const INSTALL_EA_INTEGRATIONS_HREF = `browse/security?q=entityanalytics`;
|
||||
|
||||
export const MANAGED_USER_INDEX = ['logs-entityanalytics_azure.users-*'];
|
||||
export const MANAGED_USER_PACKAGE_NAME = 'entityanalytics_azure';
|
||||
export const INSTALL_INTEGRATION_HREF = `/detail/${MANAGED_USER_PACKAGE_NAME}/overview`;
|
||||
export const ONE_WEEK_IN_HOURS = 24 * 7;
|
||||
export const MANAGED_USER_QUERY_ID = 'managedUserDetailsQuery';
|
||||
|
||||
export const getOktaUserIndex = (spaceId: string = 'default') =>
|
||||
`logs-entityanalytics_okta.user-${spaceId}`;
|
||||
|
||||
export const OKTA_PACKAGE_NAME = 'entityanalytics_okta';
|
||||
|
|
|
@ -1,61 +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 { renderHook } from '@testing-library/react-hooks';
|
||||
import type { InstalledIntegration } from '../../../../../common/api/detection_engine/fleet_integrations';
|
||||
import { TestProviders } from '../../../../common/mock';
|
||||
import { MANAGED_USER_PACKAGE_NAME } from './constants';
|
||||
import { useManagedUser } from './hooks';
|
||||
|
||||
const makeInstalledIntegration = (
|
||||
pkgName = 'testPkg',
|
||||
isEnabled = false
|
||||
): InstalledIntegration => ({
|
||||
package_name: pkgName,
|
||||
package_title: '',
|
||||
package_version: '',
|
||||
integration_name: '',
|
||||
integration_title: '',
|
||||
is_enabled: isEnabled,
|
||||
});
|
||||
|
||||
const mockUseInstalledIntegrations = jest.fn().mockReturnValue({
|
||||
data: [],
|
||||
});
|
||||
|
||||
jest.mock(
|
||||
'../../../../detections/components/rules/related_integrations/use_installed_integrations',
|
||||
() => ({
|
||||
useInstalledIntegrations: () => mockUseInstalledIntegrations(),
|
||||
})
|
||||
);
|
||||
|
||||
describe('useManagedUser', () => {
|
||||
it('returns isIntegrationEnabled:true when it finds an enabled integration with the given name', () => {
|
||||
mockUseInstalledIntegrations.mockReturnValue({
|
||||
data: [makeInstalledIntegration(MANAGED_USER_PACKAGE_NAME, true)],
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useManagedUser('test-userName'), {
|
||||
wrapper: TestProviders,
|
||||
});
|
||||
|
||||
expect(result.current.isIntegrationEnabled).toBeTruthy();
|
||||
});
|
||||
|
||||
it('returns isIntegrationEnabled:false when it does not find an enabled integration with the given name', () => {
|
||||
mockUseInstalledIntegrations.mockReturnValue({
|
||||
data: [makeInstalledIntegration('fake-name', true)],
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useManagedUser('test-userName'), {
|
||||
wrapper: TestProviders,
|
||||
});
|
||||
|
||||
expect(result.current.isIntegrationEnabled).toBeFalsy();
|
||||
});
|
||||
});
|
|
@ -1,236 +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 { useMemo, useEffect } from 'react';
|
||||
import * as i18n from './translations';
|
||||
import type { ManagedUserTable, ObservedUserData, ObservedUserTable } from './types';
|
||||
import type { AzureManagedUser } from '../../../../../common/search_strategy/security_solution/users/managed_details';
|
||||
|
||||
import {
|
||||
Direction,
|
||||
UsersQueries,
|
||||
NOT_EVENT_KIND_ASSET_FILTER,
|
||||
} from '../../../../../common/search_strategy';
|
||||
import { useSourcererDataView } from '../../../../common/containers/sourcerer';
|
||||
import { useGlobalTime } from '../../../../common/containers/use_global_time';
|
||||
import { useSearchStrategy } from '../../../../common/containers/use_search_strategy';
|
||||
import { useObservedUserDetails } from '../../../../explore/users/containers/users/observed_details';
|
||||
import { useFirstLastSeen } from '../../../../common/containers/use_first_last_seen';
|
||||
import { useInstalledIntegrations } from '../../../../detections/components/rules/related_integrations/use_installed_integrations';
|
||||
import { MANAGED_USER_INDEX, MANAGED_USER_PACKAGE_NAME, MANAGED_USER_QUERY_ID } from './constants';
|
||||
import { useQueryInspector } from '../../../../common/components/page/manage_query';
|
||||
|
||||
export const useObservedUserItems = (userData: ObservedUserData): ObservedUserTable[] =>
|
||||
useMemo(
|
||||
() =>
|
||||
!userData.details
|
||||
? []
|
||||
: [
|
||||
{ label: i18n.USER_ID, values: userData.details.user?.id, field: 'user.id' },
|
||||
{ label: 'Domain', values: userData.details.user?.domain, field: 'user.domain' },
|
||||
{
|
||||
label: i18n.MAX_ANOMALY_SCORE_BY_JOB,
|
||||
field: 'anomalies',
|
||||
values: userData.anomalies,
|
||||
},
|
||||
{
|
||||
label: i18n.FIRST_SEEN,
|
||||
values: userData.firstSeen.date ? [userData.firstSeen.date] : undefined,
|
||||
field: '@timestamp',
|
||||
},
|
||||
{
|
||||
label: i18n.LAST_SEEN,
|
||||
values: userData.lastSeen.date ? [userData.lastSeen.date] : undefined,
|
||||
field: '@timestamp',
|
||||
},
|
||||
{
|
||||
label: i18n.OPERATING_SYSTEM_TITLE,
|
||||
values: userData.details.host?.os?.name,
|
||||
field: 'host.os.name',
|
||||
},
|
||||
{
|
||||
label: i18n.FAMILY,
|
||||
values: userData.details.host?.os?.family,
|
||||
field: 'host.os.family',
|
||||
},
|
||||
{ label: i18n.IP_ADDRESSES, values: userData.details.host?.ip, field: 'host.ip' },
|
||||
],
|
||||
[userData.details, userData.anomalies, userData.firstSeen, userData.lastSeen]
|
||||
);
|
||||
|
||||
export const useManagedUserItems = (
|
||||
managedUserDetails?: AzureManagedUser
|
||||
): ManagedUserTable[] | null =>
|
||||
useMemo(
|
||||
() =>
|
||||
!managedUserDetails
|
||||
? null
|
||||
: [
|
||||
{
|
||||
label: i18n.USER_ID,
|
||||
value: managedUserDetails.user.id,
|
||||
field: 'user.id',
|
||||
},
|
||||
{
|
||||
label: i18n.FULL_NAME,
|
||||
value: managedUserDetails.user.full_name,
|
||||
field: 'user.full_name',
|
||||
},
|
||||
{
|
||||
label: i18n.FIRST_NAME,
|
||||
value: managedUserDetails.user.first_name,
|
||||
},
|
||||
{
|
||||
label: i18n.LAST_NAME,
|
||||
value: managedUserDetails.user.last_name,
|
||||
},
|
||||
{ label: i18n.PHONE, value: managedUserDetails.user.phone?.join(', ') },
|
||||
],
|
||||
[managedUserDetails]
|
||||
);
|
||||
|
||||
export const useManagedUser = (userName: string) => {
|
||||
const { to, from, isInitializing, deleteQuery, setQuery } = useGlobalTime();
|
||||
const {
|
||||
loading: loadingManagedUser,
|
||||
result: { userDetails: managedUserDetails },
|
||||
search,
|
||||
refetch,
|
||||
inspect,
|
||||
} = useSearchStrategy<UsersQueries.managedDetails>({
|
||||
factoryQueryType: UsersQueries.managedDetails,
|
||||
initialResult: {},
|
||||
errorMessage: i18n.FAIL_MANAGED_USER,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!isInitializing) {
|
||||
search({
|
||||
defaultIndex: MANAGED_USER_INDEX,
|
||||
userName,
|
||||
});
|
||||
}
|
||||
}, [from, search, to, userName, isInitializing]);
|
||||
|
||||
const { data: installedIntegrations, isLoading: loadingIntegrations } = useInstalledIntegrations({
|
||||
packages: [MANAGED_USER_PACKAGE_NAME],
|
||||
});
|
||||
|
||||
useQueryInspector({
|
||||
deleteQuery,
|
||||
inspect,
|
||||
refetch,
|
||||
setQuery,
|
||||
queryId: MANAGED_USER_QUERY_ID,
|
||||
loading: loadingManagedUser,
|
||||
});
|
||||
|
||||
const isIntegrationEnabled = useMemo(
|
||||
() =>
|
||||
!!installedIntegrations?.some(
|
||||
({ package_name: packageName, is_enabled: isEnabled }) =>
|
||||
packageName === MANAGED_USER_PACKAGE_NAME && isEnabled
|
||||
),
|
||||
[installedIntegrations]
|
||||
);
|
||||
|
||||
const [loadingFirstSeen, { firstSeen }] = useFirstLastSeen({
|
||||
field: 'user.name',
|
||||
value: userName,
|
||||
defaultIndex: MANAGED_USER_INDEX,
|
||||
order: Direction.asc,
|
||||
});
|
||||
|
||||
const [loadingLastSeen, { lastSeen }] = useFirstLastSeen({
|
||||
field: 'user.name',
|
||||
value: userName,
|
||||
defaultIndex: MANAGED_USER_INDEX,
|
||||
order: Direction.desc,
|
||||
});
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
details: managedUserDetails,
|
||||
isLoading: loadingManagedUser || loadingIntegrations,
|
||||
isIntegrationEnabled,
|
||||
firstSeen: {
|
||||
date: firstSeen,
|
||||
isLoading: loadingFirstSeen,
|
||||
},
|
||||
lastSeen: { date: lastSeen, isLoading: loadingLastSeen },
|
||||
}),
|
||||
[
|
||||
firstSeen,
|
||||
isIntegrationEnabled,
|
||||
loadingIntegrations,
|
||||
lastSeen,
|
||||
loadingFirstSeen,
|
||||
loadingLastSeen,
|
||||
loadingManagedUser,
|
||||
managedUserDetails,
|
||||
]
|
||||
);
|
||||
};
|
||||
|
||||
export const useObservedUser = (userName: string) => {
|
||||
const { selectedPatterns } = useSourcererDataView();
|
||||
const { to, from, isInitializing, deleteQuery, setQuery } = useGlobalTime();
|
||||
|
||||
const [loadingObservedUser, { userDetails: observedUserDetails, inspect, refetch, id: queryId }] =
|
||||
useObservedUserDetails({
|
||||
endDate: to,
|
||||
startDate: from,
|
||||
userName,
|
||||
indexNames: selectedPatterns,
|
||||
skip: isInitializing,
|
||||
});
|
||||
|
||||
useQueryInspector({
|
||||
deleteQuery,
|
||||
inspect,
|
||||
refetch,
|
||||
setQuery,
|
||||
queryId,
|
||||
loading: loadingObservedUser,
|
||||
});
|
||||
|
||||
const [loadingFirstSeen, { firstSeen }] = useFirstLastSeen({
|
||||
field: 'user.name',
|
||||
value: userName,
|
||||
defaultIndex: selectedPatterns,
|
||||
order: Direction.asc,
|
||||
filterQuery: NOT_EVENT_KIND_ASSET_FILTER,
|
||||
});
|
||||
|
||||
const [loadingLastSeen, { lastSeen }] = useFirstLastSeen({
|
||||
field: 'user.name',
|
||||
value: userName,
|
||||
defaultIndex: selectedPatterns,
|
||||
order: Direction.desc,
|
||||
filterQuery: NOT_EVENT_KIND_ASSET_FILTER,
|
||||
});
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
details: observedUserDetails,
|
||||
isLoading: loadingObservedUser,
|
||||
firstSeen: {
|
||||
date: firstSeen,
|
||||
isLoading: loadingFirstSeen,
|
||||
},
|
||||
lastSeen: { date: lastSeen, isLoading: loadingLastSeen },
|
||||
}),
|
||||
[
|
||||
firstSeen,
|
||||
lastSeen,
|
||||
loadingFirstSeen,
|
||||
loadingLastSeen,
|
||||
loadingObservedUser,
|
||||
observedUserDetails,
|
||||
]
|
||||
);
|
||||
};
|
|
@ -0,0 +1,132 @@
|
|||
/*
|
||||
* 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 { renderHook } from '@testing-library/react-hooks';
|
||||
import type { InstalledIntegration } from '../../../../../../common/api/detection_engine/fleet_integrations';
|
||||
import { TestProviders } from '../../../../../common/mock';
|
||||
import { ENTRA_ID_PACKAGE_NAME } from '../constants';
|
||||
import { useManagedUser } from './use_managed_user';
|
||||
import type { ObserverUser } from './use_observed_user';
|
||||
|
||||
const makeInstalledIntegration = (
|
||||
pkgName = 'testPkg',
|
||||
isEnabled = false
|
||||
): InstalledIntegration => ({
|
||||
package_name: pkgName,
|
||||
package_title: '',
|
||||
package_version: '',
|
||||
integration_name: '',
|
||||
integration_title: '',
|
||||
is_enabled: isEnabled,
|
||||
});
|
||||
|
||||
const mockUseInstalledIntegrations = jest.fn().mockReturnValue({
|
||||
data: [],
|
||||
});
|
||||
|
||||
jest.mock(
|
||||
'../../../../../detections/components/rules/related_integrations/use_installed_integrations',
|
||||
() => ({
|
||||
useInstalledIntegrations: () => mockUseInstalledIntegrations(),
|
||||
})
|
||||
);
|
||||
|
||||
jest.mock('../../../../../common/hooks/use_space_id', () => ({
|
||||
useSpaceId: () => 'test-space-id',
|
||||
}));
|
||||
|
||||
const mockSearch = jest.fn().mockReturnValue({
|
||||
data: [],
|
||||
});
|
||||
|
||||
jest.mock('../../../../../common/containers/use_search_strategy', () => ({
|
||||
useSearchStrategy: () => ({
|
||||
loading: false,
|
||||
result: { users: [] },
|
||||
search: (...params: unknown[]) => mockSearch(...params),
|
||||
refetch: () => {},
|
||||
inspect: {},
|
||||
}),
|
||||
}));
|
||||
|
||||
const observedUser: ObserverUser = {
|
||||
isLoading: false,
|
||||
details: {},
|
||||
firstSeen: {
|
||||
date: undefined,
|
||||
isLoading: false,
|
||||
},
|
||||
lastSeen: {
|
||||
date: undefined,
|
||||
isLoading: false,
|
||||
},
|
||||
};
|
||||
|
||||
describe('useManagedUser', () => {
|
||||
beforeEach(() => {
|
||||
mockSearch.mockClear();
|
||||
});
|
||||
it('returns isIntegrationEnabled:true when it finds an enabled integration with the given name', () => {
|
||||
mockUseInstalledIntegrations.mockReturnValue({
|
||||
data: [makeInstalledIntegration(ENTRA_ID_PACKAGE_NAME, true)],
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useManagedUser('test-userName', observedUser), {
|
||||
wrapper: TestProviders,
|
||||
});
|
||||
|
||||
expect(result.current.isIntegrationEnabled).toBeTruthy();
|
||||
});
|
||||
|
||||
it('returns isIntegrationEnabled:false when it does not find an enabled integration with the given name', () => {
|
||||
mockUseInstalledIntegrations.mockReturnValue({
|
||||
data: [makeInstalledIntegration('fake-name', true)],
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useManagedUser('test-userName', observedUser), {
|
||||
wrapper: TestProviders,
|
||||
});
|
||||
|
||||
expect(result.current.isIntegrationEnabled).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should search', () => {
|
||||
renderHook(() => useManagedUser('test-userName', observedUser), {
|
||||
wrapper: TestProviders,
|
||||
});
|
||||
|
||||
expect(mockSearch).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not search while observed user is loading', () => {
|
||||
renderHook(() => useManagedUser('test-userName', { ...observedUser, isLoading: true }), {
|
||||
wrapper: TestProviders,
|
||||
});
|
||||
|
||||
expect(mockSearch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should search by email if the field is available', () => {
|
||||
const email = ['test@email.com'];
|
||||
renderHook(
|
||||
() =>
|
||||
useManagedUser('test-userName', {
|
||||
...observedUser,
|
||||
details: { user: { email } },
|
||||
}),
|
||||
{
|
||||
wrapper: TestProviders,
|
||||
}
|
||||
);
|
||||
|
||||
expect(mockSearch).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
userEmail: email,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,102 @@
|
|||
/*
|
||||
* 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 { useEffect, useMemo } from 'react';
|
||||
import { useInstalledIntegrations } from '../../../../../detections/components/rules/related_integrations/use_installed_integrations';
|
||||
import { ManagedUserDatasetKey } from '../../../../../../common/search_strategy/security_solution/users/managed_details';
|
||||
import { UsersQueries } from '../../../../../../common/search_strategy';
|
||||
import { useSpaceId } from '../../../../../common/hooks/use_space_id';
|
||||
import { useSearchStrategy } from '../../../../../common/containers/use_search_strategy';
|
||||
import { useGlobalTime } from '../../../../../common/containers/use_global_time';
|
||||
import { useQueryInspector } from '../../../../../common/components/page/manage_query';
|
||||
import {
|
||||
ENTRA_ID_PACKAGE_NAME,
|
||||
OKTA_PACKAGE_NAME,
|
||||
getEntraUserIndex,
|
||||
getOktaUserIndex,
|
||||
MANAGED_USER_QUERY_ID,
|
||||
} from '../constants';
|
||||
import * as i18n from '../translations';
|
||||
import type { ObserverUser } from './use_observed_user';
|
||||
|
||||
const packages = [ENTRA_ID_PACKAGE_NAME, OKTA_PACKAGE_NAME];
|
||||
|
||||
export const useManagedUser = (userName: string, observedUser: ObserverUser) => {
|
||||
const { to, from, isInitializing, deleteQuery, setQuery } = useGlobalTime();
|
||||
const spaceId = useSpaceId();
|
||||
const {
|
||||
loading: loadingManagedUser,
|
||||
result: { users: managedUserData },
|
||||
search,
|
||||
refetch,
|
||||
inspect,
|
||||
} = useSearchStrategy<UsersQueries.managedDetails>({
|
||||
factoryQueryType: UsersQueries.managedDetails,
|
||||
initialResult: {
|
||||
users: {
|
||||
[ManagedUserDatasetKey.ENTRA]: undefined,
|
||||
[ManagedUserDatasetKey.OKTA]: undefined,
|
||||
},
|
||||
},
|
||||
errorMessage: i18n.FAIL_MANAGED_USER,
|
||||
});
|
||||
|
||||
const defaultIndex = useMemo(
|
||||
() => (spaceId ? [getEntraUserIndex(spaceId), getOktaUserIndex(spaceId)] : []),
|
||||
[spaceId]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isInitializing && defaultIndex.length > 0 && !observedUser.isLoading && userName) {
|
||||
search({
|
||||
defaultIndex,
|
||||
userEmail: observedUser.details.user?.email,
|
||||
userName,
|
||||
});
|
||||
}
|
||||
}, [
|
||||
from,
|
||||
search,
|
||||
to,
|
||||
isInitializing,
|
||||
defaultIndex,
|
||||
userName,
|
||||
observedUser.isLoading,
|
||||
observedUser.details.user?.email,
|
||||
]);
|
||||
|
||||
const { data: installedIntegrations, isLoading: loadingIntegrations } = useInstalledIntegrations({
|
||||
packages,
|
||||
});
|
||||
|
||||
useQueryInspector({
|
||||
deleteQuery,
|
||||
inspect,
|
||||
refetch,
|
||||
setQuery,
|
||||
queryId: MANAGED_USER_QUERY_ID,
|
||||
loading: loadingManagedUser,
|
||||
});
|
||||
|
||||
const isIntegrationEnabled = useMemo(
|
||||
() =>
|
||||
!!installedIntegrations?.some(
|
||||
({ package_name: packageName, is_enabled: isEnabled }) =>
|
||||
isEnabled && packages.includes(packageName)
|
||||
),
|
||||
[installedIntegrations]
|
||||
);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
data: managedUserData,
|
||||
isLoading: loadingManagedUser || loadingIntegrations,
|
||||
isIntegrationEnabled,
|
||||
}),
|
||||
[isIntegrationEnabled, loadingIntegrations, loadingManagedUser, managedUserData]
|
||||
);
|
||||
};
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* 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 { renderHook } from '@testing-library/react-hooks';
|
||||
import {
|
||||
createSecuritySolutionStorageMock,
|
||||
kibanaObservable,
|
||||
mockGlobalState,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
TestProviders,
|
||||
} from '../../../../../common/mock';
|
||||
import { useManagedUserItems } from './use_managed_user_items';
|
||||
import { mockEntraUserFields, mockOktaUserFields } from '../__mocks__';
|
||||
import { UserAssetTableType } from '../../../../../explore/users/store/model';
|
||||
import React from 'react';
|
||||
import { createStore } from '../../../../../common/store';
|
||||
|
||||
const mockState = {
|
||||
...mockGlobalState,
|
||||
users: {
|
||||
...mockGlobalState.users,
|
||||
flyout: {
|
||||
...mockGlobalState.users.flyout,
|
||||
queries: {
|
||||
[UserAssetTableType.assetEntra]: {
|
||||
fields: ['user.id', 'user.first_name'],
|
||||
},
|
||||
[UserAssetTableType.assetOkta]: {
|
||||
fields: ['user.id', 'user.profile.first_name'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const { storage } = createSecuritySolutionStorageMock();
|
||||
const mockStore = createStore(mockState, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
|
||||
|
||||
const TestWrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<TestProviders store={mockStore}>{children}</TestProviders>
|
||||
);
|
||||
|
||||
describe('useManagedUserItems', () => {
|
||||
it('returns managed user items for Entra user', () => {
|
||||
const { result } = renderHook(
|
||||
() => useManagedUserItems(UserAssetTableType.assetEntra, mockEntraUserFields),
|
||||
{
|
||||
wrapper: TestWrapper,
|
||||
}
|
||||
);
|
||||
|
||||
expect(result.current).toEqual([
|
||||
{
|
||||
field: 'user.id',
|
||||
value: ['12345'],
|
||||
},
|
||||
{
|
||||
field: 'user.first_name',
|
||||
value: ['Entra first name'],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns managed user items for Okta user', () => {
|
||||
const { result } = renderHook(
|
||||
() => useManagedUserItems(UserAssetTableType.assetOkta, mockOktaUserFields),
|
||||
{
|
||||
wrapper: TestWrapper,
|
||||
}
|
||||
);
|
||||
|
||||
expect(result.current).toEqual([
|
||||
{
|
||||
field: 'user.id',
|
||||
value: ['00ud9ohoh9ww644Px5d7'],
|
||||
},
|
||||
{
|
||||
field: 'user.profile.first_name',
|
||||
value: ['Okta first name'],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* 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 { useMemo } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import type { UserAssetTableType } from '../../../../../explore/users/store/model';
|
||||
import type { State } from '../../../../../common/store/types';
|
||||
import { usersSelectors } from '../../../../../explore/users/store';
|
||||
import type { ManagedUserTable } from '../types';
|
||||
import type { ManagedUserFields } from '../../../../../../common/search_strategy/security_solution/users/managed_details';
|
||||
|
||||
export const useManagedUserItems = (
|
||||
tableType: UserAssetTableType,
|
||||
managedUserDetails: ManagedUserFields
|
||||
): ManagedUserTable[] | null => {
|
||||
const tableData = useSelector((state: State) =>
|
||||
usersSelectors.selectUserAssetTableById(state, tableType)
|
||||
);
|
||||
|
||||
return useMemo(
|
||||
() =>
|
||||
tableData.fields.map((fieldName) => ({
|
||||
value: managedUserDetails[fieldName],
|
||||
field: fieldName,
|
||||
})),
|
||||
[managedUserDetails, tableData.fields]
|
||||
);
|
||||
};
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* 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 { useMemo } from 'react';
|
||||
import { useObservedUserDetails } from '../../../../../explore/users/containers/users/observed_details';
|
||||
import type { UserItem } from '../../../../../../common/search_strategy';
|
||||
import { Direction, NOT_EVENT_KIND_ASSET_FILTER } from '../../../../../../common/search_strategy';
|
||||
import { useSourcererDataView } from '../../../../../common/containers/sourcerer';
|
||||
import { useGlobalTime } from '../../../../../common/containers/use_global_time';
|
||||
import { useFirstLastSeen } from '../../../../../common/containers/use_first_last_seen';
|
||||
import { useQueryInspector } from '../../../../../common/components/page/manage_query';
|
||||
|
||||
export interface ObserverUser {
|
||||
details: UserItem;
|
||||
isLoading: boolean;
|
||||
firstSeen: {
|
||||
date: string | null | undefined;
|
||||
isLoading: boolean;
|
||||
};
|
||||
lastSeen: {
|
||||
date: string | null | undefined;
|
||||
isLoading: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export const useObservedUser = (userName: string): ObserverUser => {
|
||||
const { selectedPatterns } = useSourcererDataView();
|
||||
const { to, from, isInitializing, deleteQuery, setQuery } = useGlobalTime();
|
||||
|
||||
const [loadingObservedUser, { userDetails: observedUserDetails, inspect, refetch, id: queryId }] =
|
||||
useObservedUserDetails({
|
||||
endDate: to,
|
||||
startDate: from,
|
||||
userName,
|
||||
indexNames: selectedPatterns,
|
||||
skip: isInitializing,
|
||||
});
|
||||
|
||||
useQueryInspector({
|
||||
deleteQuery,
|
||||
inspect,
|
||||
refetch,
|
||||
setQuery,
|
||||
queryId,
|
||||
loading: loadingObservedUser,
|
||||
});
|
||||
|
||||
const [loadingFirstSeen, { firstSeen }] = useFirstLastSeen({
|
||||
field: 'user.name',
|
||||
value: userName,
|
||||
defaultIndex: selectedPatterns,
|
||||
order: Direction.asc,
|
||||
filterQuery: NOT_EVENT_KIND_ASSET_FILTER,
|
||||
});
|
||||
|
||||
const [loadingLastSeen, { lastSeen }] = useFirstLastSeen({
|
||||
field: 'user.name',
|
||||
value: userName,
|
||||
defaultIndex: selectedPatterns,
|
||||
order: Direction.desc,
|
||||
filterQuery: NOT_EVENT_KIND_ASSET_FILTER,
|
||||
});
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
details: observedUserDetails,
|
||||
isLoading: loadingObservedUser,
|
||||
firstSeen: {
|
||||
date: firstSeen,
|
||||
isLoading: loadingFirstSeen,
|
||||
},
|
||||
lastSeen: { date: lastSeen, isLoading: loadingLastSeen },
|
||||
}),
|
||||
[
|
||||
firstSeen,
|
||||
lastSeen,
|
||||
loadingFirstSeen,
|
||||
loadingLastSeen,
|
||||
loadingObservedUser,
|
||||
observedUserDetails,
|
||||
]
|
||||
);
|
||||
};
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* 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 { renderHook } from '@testing-library/react-hooks';
|
||||
import { TestProviders } from '../../../../../common/mock';
|
||||
import { mockObservedUser } from '../__mocks__';
|
||||
import { useObservedUserItems } from './use_observed_user_items';
|
||||
|
||||
describe('useManagedUserItems', () => {
|
||||
it('returns managed user items for Entra user', () => {
|
||||
const { result } = renderHook(() => useObservedUserItems(mockObservedUser), {
|
||||
wrapper: TestProviders,
|
||||
});
|
||||
|
||||
expect(result.current).toEqual([
|
||||
{
|
||||
field: 'user.id',
|
||||
label: 'User ID',
|
||||
values: ['1234', '321'],
|
||||
},
|
||||
{
|
||||
field: 'user.domain',
|
||||
label: 'Domain',
|
||||
values: ['test domain', 'another test domain'],
|
||||
},
|
||||
{
|
||||
field: 'anomalies',
|
||||
label: 'Max anomaly score by job',
|
||||
values: mockObservedUser.anomalies,
|
||||
},
|
||||
{
|
||||
field: '@timestamp',
|
||||
label: 'First seen',
|
||||
values: ['2023-02-23T20:03:17.489Z'],
|
||||
},
|
||||
{
|
||||
field: '@timestamp',
|
||||
label: 'Last seen',
|
||||
values: ['2023-02-23T20:03:17.489Z'],
|
||||
},
|
||||
{
|
||||
field: 'host.os.name',
|
||||
label: 'Operating system',
|
||||
values: ['testOs'],
|
||||
},
|
||||
{
|
||||
field: 'host.os.family',
|
||||
label: 'Family',
|
||||
values: ['testFamily'],
|
||||
},
|
||||
{
|
||||
field: 'host.ip',
|
||||
label: 'IP addresses',
|
||||
values: ['10.0.0.1', '127.0.0.1'],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* 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 { useMemo } from 'react';
|
||||
import * as i18n from '../translations';
|
||||
import type { ObservedUserData, ObservedUserTable } from '../types';
|
||||
|
||||
export const useObservedUserItems = (userData: ObservedUserData): ObservedUserTable[] =>
|
||||
useMemo(
|
||||
() =>
|
||||
!userData.details
|
||||
? []
|
||||
: [
|
||||
{ label: i18n.USER_ID, values: userData.details.user?.id, field: 'user.id' },
|
||||
{ label: 'Domain', values: userData.details.user?.domain, field: 'user.domain' },
|
||||
{
|
||||
label: i18n.MAX_ANOMALY_SCORE_BY_JOB,
|
||||
field: 'anomalies',
|
||||
values: userData.anomalies,
|
||||
},
|
||||
{
|
||||
label: i18n.FIRST_SEEN,
|
||||
values: userData.firstSeen.date ? [userData.firstSeen.date] : undefined,
|
||||
field: '@timestamp',
|
||||
},
|
||||
{
|
||||
label: i18n.LAST_SEEN,
|
||||
values: userData.lastSeen.date ? [userData.lastSeen.date] : undefined,
|
||||
field: '@timestamp',
|
||||
},
|
||||
{
|
||||
label: i18n.OPERATING_SYSTEM_TITLE,
|
||||
values: userData.details.host?.os?.name,
|
||||
field: 'host.os.name',
|
||||
},
|
||||
{
|
||||
label: i18n.FAMILY,
|
||||
values: userData.details.host?.os?.family,
|
||||
field: 'host.os.family',
|
||||
},
|
||||
{ label: i18n.IP_ADDRESSES, values: userData.details.host?.ip, field: 'host.ip' },
|
||||
],
|
||||
[userData.details, userData.anomalies, userData.firstSeen, userData.lastSeen]
|
||||
);
|
|
@ -5,15 +5,17 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ManagedUserDatasetKey } from '../../../../../common/search_strategy/security_solution/users/managed_details';
|
||||
import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { TestProviders } from '../../../../common/mock';
|
||||
|
||||
import { mockManagedUserData, mockOktaUserFields } from './__mocks__';
|
||||
import { ManagedUser } from './managed_user';
|
||||
import { mockManagedUser } from './__mocks__';
|
||||
|
||||
describe('ManagedUser', () => {
|
||||
const mockProps = {
|
||||
managedUser: mockManagedUser,
|
||||
managedUser: mockManagedUserData,
|
||||
contextID: '',
|
||||
scopeId: '',
|
||||
isDraggable: false,
|
||||
|
@ -36,7 +38,7 @@ describe('ManagedUser', () => {
|
|||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(getByTestId('managedUser-data')).toHaveTextContent('Updated Mar 23, 2023');
|
||||
expect(getByTestId('managedUser-data')).toHaveTextContent('Nov 16, 2023');
|
||||
});
|
||||
|
||||
it('renders enable integration callout when the integration is disabled', () => {
|
||||
|
@ -46,7 +48,7 @@ describe('ManagedUser', () => {
|
|||
{...{
|
||||
...mockProps,
|
||||
managedUser: {
|
||||
...mockManagedUser,
|
||||
...mockManagedUserData,
|
||||
isIntegrationEnabled: false,
|
||||
},
|
||||
}}
|
||||
|
@ -57,23 +59,13 @@ describe('ManagedUser', () => {
|
|||
expect(getByTestId('managedUser-integration-disable-callout')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders phone number separated by comma', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<ManagedUser {...mockProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(getByTestId('managedUser-data')).toHaveTextContent('123456, 654321');
|
||||
});
|
||||
|
||||
it('it renders the call out when the integration is disabled', () => {
|
||||
it('renders the call out when the integration is disabled', () => {
|
||||
const { queryByTestId } = render(
|
||||
<TestProviders>
|
||||
<ManagedUser
|
||||
{...{
|
||||
...mockProps,
|
||||
managedUser: { ...mockManagedUser, isIntegrationEnabled: false },
|
||||
managedUser: { ...mockManagedUserData, isIntegrationEnabled: false },
|
||||
}}
|
||||
/>
|
||||
</TestProviders>
|
||||
|
@ -88,7 +80,7 @@ describe('ManagedUser', () => {
|
|||
<ManagedUser
|
||||
{...{
|
||||
...mockProps,
|
||||
managedUser: { ...mockManagedUser, isLoading: true, isIntegrationEnabled: false },
|
||||
managedUser: { ...mockManagedUserData, isLoading: true, isIntegrationEnabled: false },
|
||||
}}
|
||||
/>
|
||||
</TestProviders>
|
||||
|
@ -96,4 +88,39 @@ describe('ManagedUser', () => {
|
|||
|
||||
expect(queryByTestId('managedUser-integration-disable-callout')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders Entra managed user', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<ManagedUser {...mockProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(getByTestId('managedUser-table')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders Okta managed user', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<ManagedUser
|
||||
{...{
|
||||
...mockProps,
|
||||
managedUser: {
|
||||
...mockManagedUserData,
|
||||
data: {
|
||||
[ManagedUserDatasetKey.ENTRA]: undefined,
|
||||
[ManagedUserDatasetKey.OKTA]: {
|
||||
fields: mockOktaUserFields,
|
||||
_index: '123',
|
||||
_id: '12234',
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(getByTestId('managedUser-table')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -11,66 +11,54 @@ import {
|
|||
EuiAccordion,
|
||||
EuiTitle,
|
||||
EuiPanel,
|
||||
useEuiTheme,
|
||||
EuiEmptyPrompt,
|
||||
EuiCallOut,
|
||||
useEuiFontSize,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import { css } from '@emotion/react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { UserAssetTableType } from '../../../../explore/users/store/model';
|
||||
import type { ManagedUserFields } from '../../../../../common/search_strategy/security_solution/users/managed_details';
|
||||
import { ManagedUserDatasetKey } from '../../../../../common/search_strategy/security_solution/users/managed_details';
|
||||
import * as i18n from './translations';
|
||||
|
||||
import { BasicTable } from '../../../../common/components/ml/tables/basic_table';
|
||||
import { getManagedUserTableColumns } from './columns';
|
||||
import { useManagedUserItems } from './hooks';
|
||||
|
||||
import { FormattedRelativePreferenceDate } from '../../../../common/components/formatted_date';
|
||||
import type { ManagedUserData } from './types';
|
||||
import { INSTALL_INTEGRATION_HREF, MANAGED_USER_QUERY_ID, ONE_WEEK_IN_HOURS } from './constants';
|
||||
import { INSTALL_EA_INTEGRATIONS_HREF, MANAGED_USER_QUERY_ID } from './constants';
|
||||
import { InspectButton, InspectButtonContainer } from '../../../../common/components/inspect';
|
||||
import { useAppUrl } from '../../../../common/lib/kibana';
|
||||
import { ManagedUserAccordion } from './managed_user_accordion';
|
||||
import { useManagedUserItems } from './hooks/use_managed_user_items';
|
||||
|
||||
export const ManagedUser = ({
|
||||
managedUser,
|
||||
contextID,
|
||||
scopeId,
|
||||
isDraggable,
|
||||
}: {
|
||||
managedUser: ManagedUserData;
|
||||
contextID: string;
|
||||
scopeId: string;
|
||||
isDraggable: boolean;
|
||||
}) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const managedItems = useManagedUserItems(managedUser.details);
|
||||
const managedUserTableColumns = useMemo(
|
||||
() => getManagedUserTableColumns(contextID, scopeId, isDraggable),
|
||||
[isDraggable, contextID, scopeId]
|
||||
);
|
||||
const entraManagedUser = managedUser.data?.[ManagedUserDatasetKey.ENTRA];
|
||||
const oktaManagedUser = managedUser.data?.[ManagedUserDatasetKey.OKTA];
|
||||
const { getAppUrl } = useAppUrl();
|
||||
|
||||
const installedIntegrationHref = useMemo(
|
||||
() => getAppUrl({ appId: 'integrations', path: INSTALL_INTEGRATION_HREF }),
|
||||
() => getAppUrl({ appId: 'integrations', path: INSTALL_EA_INTEGRATIONS_HREF }),
|
||||
[getAppUrl]
|
||||
);
|
||||
|
||||
const xsFontSize = useEuiFontSize('xxs').fontSize;
|
||||
|
||||
return (
|
||||
<>
|
||||
<InspectButtonContainer>
|
||||
<EuiAccordion
|
||||
isLoading={managedUser.isLoading}
|
||||
initialIsOpen={false}
|
||||
initialIsOpen={true}
|
||||
id={'managedUser-data'}
|
||||
data-test-subj="managedUser-data"
|
||||
buttonProps={{
|
||||
'data-test-subj': 'managedUser-accordion-button',
|
||||
css: css`
|
||||
color: ${euiTheme.colors.primary};
|
||||
`,
|
||||
}}
|
||||
buttonContent={
|
||||
<EuiTitle size="xs">
|
||||
|
@ -78,47 +66,21 @@ export const ManagedUser = ({
|
|||
</EuiTitle>
|
||||
}
|
||||
extraAction={
|
||||
<>
|
||||
<span
|
||||
css={css`
|
||||
margin-right: ${euiTheme.size.s};
|
||||
`}
|
||||
>
|
||||
<InspectButton
|
||||
queryId={MANAGED_USER_QUERY_ID}
|
||||
title={i18n.MANAGED_USER_INSPECT_TITLE}
|
||||
/>
|
||||
</span>
|
||||
{managedUser.lastSeen.date && (
|
||||
<span
|
||||
css={css`
|
||||
font-size: ${xsFontSize};
|
||||
`}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.timeline.userDetails.updatedTime"
|
||||
defaultMessage="Updated {time}"
|
||||
values={{
|
||||
time: (
|
||||
<FormattedRelativePreferenceDate
|
||||
value={managedUser.lastSeen.date}
|
||||
dateFormat="MMM D, YYYY"
|
||||
relativeThresholdInHrs={ONE_WEEK_IN_HOURS}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
<InspectButton
|
||||
queryId={MANAGED_USER_QUERY_ID}
|
||||
title={i18n.MANAGED_USER_INSPECT_TITLE}
|
||||
/>
|
||||
}
|
||||
css={css`
|
||||
.euiAccordion__optionalAction {
|
||||
margin-left: auto;
|
||||
}
|
||||
`}
|
||||
>
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.timeline.userDetails.managed.description"
|
||||
defaultMessage="Metadata from any asset repository integrations enabled in your environment."
|
||||
/>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
{!managedUser.isLoading && !managedUser.isIntegrationEnabled ? (
|
||||
<EuiPanel
|
||||
data-test-subj="managedUser-integration-disable-callout"
|
||||
|
@ -138,22 +100,53 @@ export const ManagedUser = ({
|
|||
</EuiPanel>
|
||||
) : (
|
||||
<>
|
||||
{managedItems || managedUser.isLoading ? (
|
||||
<BasicTable
|
||||
loading={managedUser.isLoading}
|
||||
data-test-subj="managedUser-table"
|
||||
columns={managedUserTableColumns}
|
||||
items={managedItems ?? []}
|
||||
/>
|
||||
) : (
|
||||
{!entraManagedUser && !oktaManagedUser && !managedUser.isLoading ? (
|
||||
<EuiCallOut
|
||||
data-test-subj="managedUser-no-data"
|
||||
title={i18n.NO_AZURE_DATA_TITLE}
|
||||
title={i18n.NO_MANAGED_DATA_TITLE}
|
||||
color="warning"
|
||||
iconType="help"
|
||||
>
|
||||
<p>{i18n.NO_AZURE_DATA_TEXT}</p>
|
||||
<p>{i18n.NO_MANAGED_DATA_TEXT}</p>
|
||||
</EuiCallOut>
|
||||
) : (
|
||||
<>
|
||||
{entraManagedUser && entraManagedUser.fields && (
|
||||
<ManagedUserAccordion
|
||||
title={i18n.ENTRA_DATA_PANEL_TITLE}
|
||||
managedUser={entraManagedUser.fields}
|
||||
indexName={entraManagedUser._index}
|
||||
eventId={entraManagedUser._id}
|
||||
tableType={UserAssetTableType.assetEntra}
|
||||
>
|
||||
<ManagedUserTable
|
||||
isDraggable={isDraggable}
|
||||
contextID={contextID}
|
||||
managedUser={entraManagedUser.fields}
|
||||
tableType={UserAssetTableType.assetEntra}
|
||||
/>
|
||||
</ManagedUserAccordion>
|
||||
)}
|
||||
|
||||
{entraManagedUser && oktaManagedUser && <EuiSpacer size="m" />}
|
||||
|
||||
{oktaManagedUser && oktaManagedUser.fields && (
|
||||
<ManagedUserAccordion
|
||||
title={i18n.OKTA_DATA_PANEL_TITLE}
|
||||
managedUser={oktaManagedUser.fields}
|
||||
indexName={oktaManagedUser._index}
|
||||
eventId={oktaManagedUser._id}
|
||||
tableType={UserAssetTableType.assetEntra}
|
||||
>
|
||||
<ManagedUserTable
|
||||
isDraggable={isDraggable}
|
||||
contextID={contextID}
|
||||
managedUser={oktaManagedUser.fields}
|
||||
tableType={UserAssetTableType.assetOkta}
|
||||
/>
|
||||
</ManagedUserAccordion>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
@ -162,3 +155,29 @@ export const ManagedUser = ({
|
|||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const ManagedUserTable = ({
|
||||
managedUser,
|
||||
contextID,
|
||||
isDraggable,
|
||||
tableType,
|
||||
}: {
|
||||
managedUser: ManagedUserFields;
|
||||
contextID: string;
|
||||
isDraggable: boolean;
|
||||
tableType: UserAssetTableType;
|
||||
}) => {
|
||||
const managedUserTableColumns = useMemo(
|
||||
() => getManagedUserTableColumns(contextID, isDraggable),
|
||||
[isDraggable, contextID]
|
||||
);
|
||||
const managedItems = useManagedUserItems(tableType, managedUser);
|
||||
|
||||
return (
|
||||
<BasicTable
|
||||
data-test-subj="managedUser-table"
|
||||
columns={managedUserTableColumns}
|
||||
items={managedItems ?? []}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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 { TestProviders } from '../../../../common/mock';
|
||||
import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { ManagedUserAccordion } from './managed_user_accordion';
|
||||
import { mockEntraUserFields } from './__mocks__';
|
||||
import { UserAssetTableType } from '../../../../explore/users/store/model';
|
||||
|
||||
describe('useManagedUserItems', () => {
|
||||
it('it renders children', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<ManagedUserAccordion
|
||||
title="test title"
|
||||
managedUser={mockEntraUserFields}
|
||||
indexName="test-index"
|
||||
eventId="123"
|
||||
tableType={UserAssetTableType.assetEntra}
|
||||
>
|
||||
<div data-test-subj="test-children" />
|
||||
</ManagedUserAccordion>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(getByTestId('test-children')).toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
* 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 { useEuiFontSize } from '@elastic/eui';
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import { css } from '@emotion/react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { get } from 'lodash/fp';
|
||||
import { useExpandableFlyoutContext } from '@kbn/expandable-flyout';
|
||||
import { ExpandablePanel } from '../../../../flyout/shared/components/expandable_panel';
|
||||
import type { ManagedUserFields } from '../../../../../common/search_strategy/security_solution/users/managed_details';
|
||||
|
||||
import { FormattedRelativePreferenceDate } from '../../../../common/components/formatted_date';
|
||||
import { ONE_WEEK_IN_HOURS } from './constants';
|
||||
import { AssetDocumentLeftPanelKey } from '../../../../flyout/entity_details/asset_document_left';
|
||||
import type { UserAssetTableType } from '../../../../explore/users/store/model';
|
||||
interface ManagedUserAccordionProps {
|
||||
children: React.ReactNode;
|
||||
title: string;
|
||||
managedUser: ManagedUserFields;
|
||||
indexName: string;
|
||||
eventId: string;
|
||||
tableType: UserAssetTableType;
|
||||
}
|
||||
|
||||
export const ManagedUserAccordion: React.FC<ManagedUserAccordionProps> = ({
|
||||
children,
|
||||
title,
|
||||
managedUser,
|
||||
indexName,
|
||||
eventId,
|
||||
tableType,
|
||||
}) => {
|
||||
const xsFontSize = useEuiFontSize('xxs').fontSize;
|
||||
const timestamp = get('@timestamp[0]', managedUser) as unknown as string | undefined;
|
||||
|
||||
const { openLeftPanel } = useExpandableFlyoutContext();
|
||||
const toggleDetails = useCallback(() => {
|
||||
openLeftPanel({
|
||||
id: AssetDocumentLeftPanelKey,
|
||||
params: {
|
||||
id: eventId,
|
||||
indexName,
|
||||
scopeId: tableType,
|
||||
},
|
||||
});
|
||||
}, [openLeftPanel, eventId, indexName, tableType]);
|
||||
|
||||
return (
|
||||
<ExpandablePanel
|
||||
header={{
|
||||
title,
|
||||
iconType: 'arrowStart',
|
||||
headerContent: timestamp && (
|
||||
<span
|
||||
css={css`
|
||||
font-size: ${xsFontSize};
|
||||
`}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.timeline.userDetails.updatedTime"
|
||||
defaultMessage="Updated {time}"
|
||||
values={{
|
||||
time: (
|
||||
<FormattedRelativePreferenceDate
|
||||
value={timestamp}
|
||||
dateFormat="MMM D, YYYY"
|
||||
relativeThresholdInHrs={ONE_WEEK_IN_HOURS}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
),
|
||||
link: {
|
||||
callback: toggleDetails,
|
||||
tooltip: (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.flyout.entityDetails.showAssetDocument"
|
||||
defaultMessage="Show asset details"
|
||||
/>
|
||||
),
|
||||
},
|
||||
}}
|
||||
expand={{ expandable: false }}
|
||||
>
|
||||
{children}
|
||||
</ExpandablePanel>
|
||||
);
|
||||
};
|
|
@ -12,13 +12,13 @@ import { css } from '@emotion/react';
|
|||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import * as i18n from './translations';
|
||||
import type { ObservedUserData } from './types';
|
||||
import { useObservedUserItems } from './hooks';
|
||||
import { BasicTable } from '../../../../common/components/ml/tables/basic_table';
|
||||
import { FormattedRelativePreferenceDate } from '../../../../common/components/formatted_date';
|
||||
import { getObservedUserTableColumns } from './columns';
|
||||
import { ONE_WEEK_IN_HOURS } from './constants';
|
||||
import { InspectButton, InspectButtonContainer } from '../../../../common/components/inspect';
|
||||
import { OBSERVED_USER_QUERY_ID } from '../../../../explore/users/containers/users/observed_details';
|
||||
import { useObservedUserItems } from './hooks/use_observed_user_items';
|
||||
|
||||
export const ObservedUser = ({
|
||||
observedUser,
|
||||
|
@ -44,7 +44,7 @@ export const ObservedUser = ({
|
|||
<>
|
||||
<InspectButtonContainer>
|
||||
<EuiAccordion
|
||||
initialIsOpen={false}
|
||||
initialIsOpen={true}
|
||||
isLoading={observedUser.isLoading}
|
||||
id="observedUser-data"
|
||||
data-test-subj="observedUser-data"
|
||||
|
|
|
@ -46,17 +46,17 @@ export const OBSERVED_DATA_TITLE = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const HIDE_AZURE_DATA_BUTTON = i18n.translate(
|
||||
'xpack.securitySolution.timeline.userDetails.hideManagedDataButton',
|
||||
export const ENTRA_DATA_PANEL_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.timeline.userDetails.EntraDataPanelTitle',
|
||||
{
|
||||
defaultMessage: 'Hide Azure AD data',
|
||||
defaultMessage: 'Entra ID data',
|
||||
}
|
||||
);
|
||||
|
||||
export const SHOW_AZURE_DATA_BUTTON = i18n.translate(
|
||||
'xpack.securitySolution.timeline.userDetails.showManagedDataButton',
|
||||
export const OKTA_DATA_PANEL_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.timeline.userDetails.hideOktaDataPanelTitle',
|
||||
{
|
||||
defaultMessage: 'Show Azure AD data',
|
||||
defaultMessage: 'Okta data',
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -124,31 +124,6 @@ export const IP_ADDRESSES = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const FULL_NAME = i18n.translate(
|
||||
'xpack.securitySolution.timeline.userDetails.fullNameLabel',
|
||||
{
|
||||
defaultMessage: 'Full name',
|
||||
}
|
||||
);
|
||||
|
||||
export const FIRST_NAME = i18n.translate(
|
||||
'xpack.securitySolution.timeline.userDetails.firstNameLabel',
|
||||
{
|
||||
defaultMessage: 'First name',
|
||||
}
|
||||
);
|
||||
|
||||
export const LAST_NAME = i18n.translate(
|
||||
'xpack.securitySolution.timeline.userDetails.lastNameLabel',
|
||||
{
|
||||
defaultMessage: 'Last name',
|
||||
}
|
||||
);
|
||||
|
||||
export const PHONE = i18n.translate('xpack.securitySolution.timeline.userDetails.phoneLabel', {
|
||||
defaultMessage: 'Phone',
|
||||
});
|
||||
|
||||
export const NO_ACTIVE_INTEGRATION_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.timeline.userDetails.noActiveIntegrationTitle',
|
||||
{
|
||||
|
@ -171,14 +146,14 @@ export const ADD_EXTERNAL_INTEGRATION_BUTTON = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const NO_AZURE_DATA_TITLE = i18n.translate(
|
||||
export const NO_MANAGED_DATA_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.timeline.userDetails.noAzureDataTitle',
|
||||
{
|
||||
defaultMessage: 'No metadata found for this user',
|
||||
}
|
||||
);
|
||||
|
||||
export const NO_AZURE_DATA_TEXT = i18n.translate(
|
||||
export const NO_MANAGED_DATA_TEXT = i18n.translate(
|
||||
'xpack.securitySolution.timeline.userDetails.noAzureDataText',
|
||||
{
|
||||
defaultMessage:
|
||||
|
|
|
@ -6,19 +6,18 @@
|
|||
*/
|
||||
|
||||
import type { EuiBasicTableColumn } from '@elastic/eui';
|
||||
import type { SearchTypes } from '../../../../../common/detection_engine/types';
|
||||
import type { UserItem } from '../../../../../common/search_strategy';
|
||||
import type { AzureManagedUser } from '../../../../../common/search_strategy/security_solution/users/managed_details';
|
||||
import type { ManagedUserHits } from '../../../../../common/search_strategy/security_solution/users/managed_details';
|
||||
import type { AnomalyTableProviderChildrenProps } from '../../../../common/components/ml/anomaly/anomaly_table_provider';
|
||||
|
||||
export interface ObservedUserTable {
|
||||
label: string;
|
||||
values: string[] | null | undefined | UserAnomalies;
|
||||
field: string;
|
||||
}
|
||||
|
||||
export interface ManagedUserTable {
|
||||
label: string;
|
||||
value: string | null | undefined;
|
||||
value: SearchTypes[];
|
||||
field?: string;
|
||||
}
|
||||
|
||||
|
@ -35,10 +34,8 @@ export interface ObservedUserData {
|
|||
|
||||
export interface ManagedUserData {
|
||||
isLoading: boolean;
|
||||
details: AzureManagedUser | undefined;
|
||||
data: ManagedUserHits | undefined;
|
||||
isIntegrationEnabled: boolean;
|
||||
firstSeen: FirstLastSeenData;
|
||||
lastSeen: FirstLastSeenData;
|
||||
}
|
||||
|
||||
export interface FirstLastSeenData {
|
||||
|
|
|
@ -5,12 +5,9 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { Observable } from 'rxjs';
|
||||
|
||||
import type { Storage } from '@kbn/kibana-utils-plugin/public';
|
||||
import type { CoreStart } from '@kbn/core/public';
|
||||
import type { FilterManager } from '@kbn/data-plugin/public';
|
||||
import type { TableById } from '@kbn/securitysolution-data-table';
|
||||
import type { RootEpicDependencies } from '../../../common/store/epic';
|
||||
import type { ColumnHeaderOptions, SortColumnTimeline } from '../../../../common/types';
|
||||
import type { RowRendererId } from '../../../../common/api/timeline';
|
||||
import type { inputsModel } from '../../../common/store/inputs';
|
||||
|
@ -39,14 +36,12 @@ export interface TimelineState {
|
|||
insertTimeline: InsertTimeline | null;
|
||||
}
|
||||
|
||||
export interface TimelineEpicDependencies<State> {
|
||||
export interface TimelineEpicDependencies<State> extends RootEpicDependencies {
|
||||
timelineByIdSelector: (state: State) => TimelineById;
|
||||
timelineTimeRangeSelector: (state: State) => inputsModel.TimeRange;
|
||||
selectAllTimelineQuery: () => (state: State, id: string) => inputsModel.GlobalQuery;
|
||||
selectNotesByIdSelector: (state: State) => NotesById;
|
||||
tableByIdSelector: (state: State) => TableById;
|
||||
kibana$: Observable<CoreStart>;
|
||||
storage: Storage;
|
||||
}
|
||||
|
||||
export interface TimelineModelSettings {
|
||||
|
|
|
@ -16,8 +16,11 @@ Object {
|
|||
\\"bool\\": {
|
||||
\\"filter\\": [
|
||||
{
|
||||
\\"term\\": {
|
||||
\\"user.name\\": \\"test-user-name\\"
|
||||
\\"terms\\": {
|
||||
\\"event.dataset\\": [
|
||||
\\"entityanalytics_okta.user\\",
|
||||
\\"entityanalytics_entra_id.user\\"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -25,10 +28,52 @@ Object {
|
|||
\\"event.kind\\": \\"asset\\"
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
\\"should\\": [
|
||||
{
|
||||
\\"term\\": {
|
||||
\\"user.name\\": \\"test-user-name\\"
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"terms\\": {
|
||||
\\"user.email\\": [
|
||||
\\"test-user-name@mail.com\\"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
\\"minimum_should_match\\": 1
|
||||
}
|
||||
},
|
||||
\\"size\\": 1
|
||||
\\"size\\": 0,
|
||||
\\"aggs\\": {
|
||||
\\"datasets\\": {
|
||||
\\"terms\\": {
|
||||
\\"field\\": \\"event.dataset\\"
|
||||
},
|
||||
\\"aggs\\": {
|
||||
\\"latest_hit\\": {
|
||||
\\"top_hits\\": {
|
||||
\\"fields\\": [
|
||||
\\"*\\",
|
||||
\\"_index\\",
|
||||
\\"_id\\"
|
||||
],
|
||||
\\"_source\\": false,
|
||||
\\"size\\": 1,
|
||||
\\"sort\\": [
|
||||
{
|
||||
\\"@timestamp\\": {
|
||||
\\"order\\": \\"desc\\"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
\\"sort\\": [
|
||||
{
|
||||
|
@ -44,128 +89,430 @@ Object {
|
|||
"rawResponse": Object {
|
||||
"_shards": Object {
|
||||
"failed": 0,
|
||||
"skipped": 1,
|
||||
"skipped": 0,
|
||||
"successful": 2,
|
||||
"total": 2,
|
||||
},
|
||||
"hits": Object {
|
||||
"hits": Array [
|
||||
Object {
|
||||
"_id": "9AxbIocB-WLv2258YZtS",
|
||||
"_index": ".test",
|
||||
"_score": null,
|
||||
"_source": Object {
|
||||
"@timestamp": "2023-02-23T20:03:17.489Z",
|
||||
"agent": Object {
|
||||
"ephemeral_id": "914fd1fa-aa37-4ab4-b36d-972ab9b19cde",
|
||||
"id": "9528bb69-1511-4631-a5af-1d7e93c02009",
|
||||
"name": "docker-fleet-agent",
|
||||
"type": "filebeat",
|
||||
"version": "8.8.0",
|
||||
},
|
||||
"event": Object {
|
||||
"action": "user-discovered",
|
||||
"agent_id_status": "verified",
|
||||
"dataset": "entityanalytics_azure.users",
|
||||
"ingested": "2023-02-23T20:03:18Z",
|
||||
"kind": "asset",
|
||||
"provider": "Azure AD",
|
||||
"type": Array [
|
||||
"user",
|
||||
"info",
|
||||
],
|
||||
},
|
||||
"host": Object {
|
||||
"architecture": "x86_64",
|
||||
"hostname": "docker-fleet-agent",
|
||||
"id": "cff3d165179d4aef9596ddbb263e3adb",
|
||||
"ip": Array [
|
||||
"172.26.0.7",
|
||||
],
|
||||
"mac": Array [
|
||||
"02-42-AC-1A-00-07",
|
||||
],
|
||||
"name": "docker-fleet-agent",
|
||||
"os": Object {
|
||||
"family": "debian",
|
||||
"kernel": "5.10.47-linuxkit",
|
||||
"name": "Ubuntu",
|
||||
"platform": "ubuntu",
|
||||
"type": "linux",
|
||||
"version": "20.04.5 LTS (Focal Fossa)",
|
||||
"aggregations": Object {
|
||||
"datasets": Object {
|
||||
"buckets": Array [
|
||||
Object {
|
||||
"doc_count": 122,
|
||||
"key": "entityanalytics_okta.user",
|
||||
"latest_hit": Object {
|
||||
"hits": Object {
|
||||
"hits": Array [
|
||||
Object {
|
||||
"_id": "Bnwi8osBcjOsowlA78aM",
|
||||
"_index": ".ds-logs-entityanalytics_okta.user-default-2023.11.15-000001",
|
||||
"_score": null,
|
||||
"fields": Object {
|
||||
"@timestamp": Array [
|
||||
"2023-11-28T13:32:54.446Z",
|
||||
],
|
||||
"_id": Array [
|
||||
"XoEiFowBcjOsowlAIN1T",
|
||||
],
|
||||
"_index": Array [
|
||||
".ds-logs-entityanalytics_okta.user-default-2023.11.15-000001",
|
||||
],
|
||||
"agent.ephemeral_id": Array [
|
||||
"7ddc108f-026a-4a20-afc1-ebc983145df4",
|
||||
],
|
||||
"agent.id": Array [
|
||||
"ced095f0-df97-4bdc-86a9-25cc11238317",
|
||||
],
|
||||
"agent.name": Array [
|
||||
"docker-fleet-agent",
|
||||
],
|
||||
"agent.type": Array [
|
||||
"filebeat",
|
||||
],
|
||||
"agent.version": Array [
|
||||
"8.12.0",
|
||||
],
|
||||
"asset.category": Array [
|
||||
"entity",
|
||||
],
|
||||
"asset.create_date": Array [
|
||||
"2023-11-14T16:33:53.000Z",
|
||||
],
|
||||
"asset.id": Array [
|
||||
"00ud9ohoh9ww644Px5d7",
|
||||
],
|
||||
"asset.last_seen": Array [
|
||||
"2023-11-21T08:08:46.000Z",
|
||||
],
|
||||
"asset.last_status_change_date": Array [
|
||||
"2023-11-15T07:09:05.000Z",
|
||||
],
|
||||
"asset.last_updated": Array [
|
||||
"2023-11-21T08:14:56.000Z",
|
||||
],
|
||||
"asset.status": Array [
|
||||
"ACTIVE",
|
||||
],
|
||||
"asset.type": Array [
|
||||
"okta_user",
|
||||
],
|
||||
"data_stream.dataset": Array [
|
||||
"entityanalytics_okta.user",
|
||||
],
|
||||
"data_stream.namespace": Array [
|
||||
"default",
|
||||
],
|
||||
"data_stream.type": Array [
|
||||
"logs",
|
||||
],
|
||||
"ecs.version": Array [
|
||||
"8.11.0",
|
||||
],
|
||||
"elastic_agent.id": Array [
|
||||
"ced095f0-df97-4bdc-86a9-25cc11238317",
|
||||
],
|
||||
"elastic_agent.snapshot": Array [
|
||||
true,
|
||||
],
|
||||
"elastic_agent.version": Array [
|
||||
"8.12.0",
|
||||
],
|
||||
"entityanalytics_okta.user._links": Array [
|
||||
Object {
|
||||
"self": Object {
|
||||
"href": "https://dev-36006609.okta.com/api/v1/users/00ud9ohoh9ww644Px5d7",
|
||||
},
|
||||
},
|
||||
],
|
||||
"entityanalytics_okta.user.type": Array [
|
||||
Object {
|
||||
"id": "otyf1r6hlGf9AXhZ95d6",
|
||||
},
|
||||
],
|
||||
"event.action": Array [
|
||||
"user-modified",
|
||||
],
|
||||
"event.agent_id_status": Array [
|
||||
"verified",
|
||||
],
|
||||
"event.category": Array [
|
||||
"iam",
|
||||
],
|
||||
"event.dataset": Array [
|
||||
"entityanalytics_okta.user",
|
||||
],
|
||||
"event.ingested": Array [
|
||||
"2023-11-28T13:33:04Z",
|
||||
],
|
||||
"event.kind": Array [
|
||||
"asset",
|
||||
],
|
||||
"event.module": Array [
|
||||
"entityanalytics_okta",
|
||||
],
|
||||
"event.type": Array [
|
||||
"user",
|
||||
"info",
|
||||
],
|
||||
"input.type": Array [
|
||||
"entity-analytics",
|
||||
],
|
||||
"labels.identity_source": Array [
|
||||
"entity-analytics-entityanalytics_okta.user-be940503-bec8-4849-8ec7-2b526d6f2609",
|
||||
],
|
||||
"related.user": Array [
|
||||
"00ud9ohoh9ww644Px5d7",
|
||||
"test@elastic.co",
|
||||
"Test",
|
||||
"User",
|
||||
],
|
||||
"tags": Array [
|
||||
"forwarded",
|
||||
"entityanalytics_okta-user",
|
||||
],
|
||||
"user.account.activated_date": Array [
|
||||
"2023-11-14T16:33:54.000Z",
|
||||
],
|
||||
"user.account.change_date": Array [
|
||||
"2023-11-15T07:09:05.000Z",
|
||||
],
|
||||
"user.account.create_date": Array [
|
||||
"2023-11-14T16:33:53.000Z",
|
||||
],
|
||||
"user.account.password_change_date": Array [
|
||||
"2023-11-15T07:09:05.000Z",
|
||||
],
|
||||
"user.account.status.deprovisioned": Array [
|
||||
false,
|
||||
],
|
||||
"user.account.status.locked_out": Array [
|
||||
false,
|
||||
],
|
||||
"user.account.status.password_expired": Array [
|
||||
false,
|
||||
],
|
||||
"user.account.status.recovery": Array [
|
||||
false,
|
||||
],
|
||||
"user.account.status.suspended": Array [
|
||||
false,
|
||||
],
|
||||
"user.email": Array [
|
||||
"test@elastic.co",
|
||||
],
|
||||
"user.geo.city_name": Array [
|
||||
"Adam",
|
||||
],
|
||||
"user.geo.country_iso_code": Array [
|
||||
"NL",
|
||||
],
|
||||
"user.id": Array [
|
||||
"00ud9ohoh9ww644Px5d7",
|
||||
],
|
||||
"user.name": Array [
|
||||
"test@elastic.co",
|
||||
],
|
||||
"user.name.text": Array [
|
||||
"test@elastic.co",
|
||||
],
|
||||
"user.profile.first_name": Array [
|
||||
"User First Name",
|
||||
],
|
||||
"user.profile.job_title": Array [
|
||||
"Unit Test Writer",
|
||||
],
|
||||
"user.profile.last_name": Array [
|
||||
"Test Last Name",
|
||||
],
|
||||
"user.profile.mobile_phone": Array [
|
||||
"99999999",
|
||||
],
|
||||
"user.profile.primaryPhone": Array [
|
||||
"99999999",
|
||||
],
|
||||
"user.profile.status": Array [
|
||||
"ACTIVE",
|
||||
],
|
||||
},
|
||||
"sort": Array [
|
||||
1700574447551,
|
||||
],
|
||||
},
|
||||
],
|
||||
"max_score": null,
|
||||
"total": Object {
|
||||
"relation": "eq",
|
||||
"value": 122,
|
||||
},
|
||||
},
|
||||
},
|
||||
"user": Object {
|
||||
"email": "tes.user@elastic.co",
|
||||
"first_name": "Taylor",
|
||||
"full_name": "Test user",
|
||||
"id": "39fac578-91fb-47f6-8f7a-fab05ce70d8b",
|
||||
"last_name": "Test last name",
|
||||
"phone": Array [
|
||||
"1235559999",
|
||||
],
|
||||
},
|
||||
},
|
||||
"sort": Array [
|
||||
1677182597489,
|
||||
],
|
||||
},
|
||||
],
|
||||
],
|
||||
"doc_count_error_upper_bound": 0,
|
||||
"sum_other_doc_count": 0,
|
||||
},
|
||||
},
|
||||
"hits": Object {
|
||||
"hits": Array [],
|
||||
"max_score": null,
|
||||
},
|
||||
"timed_out": false,
|
||||
"took": 124,
|
||||
"took": 5,
|
||||
},
|
||||
"total": 21,
|
||||
"userDetails": Object {
|
||||
"@timestamp": "2023-02-23T20:03:17.489Z",
|
||||
"agent": Object {
|
||||
"ephemeral_id": "914fd1fa-aa37-4ab4-b36d-972ab9b19cde",
|
||||
"id": "9528bb69-1511-4631-a5af-1d7e93c02009",
|
||||
"name": "docker-fleet-agent",
|
||||
"type": "filebeat",
|
||||
"version": "8.8.0",
|
||||
},
|
||||
"event": Object {
|
||||
"action": "user-discovered",
|
||||
"agent_id_status": "verified",
|
||||
"dataset": "entityanalytics_azure.users",
|
||||
"ingested": "2023-02-23T20:03:18Z",
|
||||
"kind": "asset",
|
||||
"provider": "Azure AD",
|
||||
"type": Array [
|
||||
"user",
|
||||
"info",
|
||||
],
|
||||
},
|
||||
"host": Object {
|
||||
"architecture": "x86_64",
|
||||
"hostname": "docker-fleet-agent",
|
||||
"id": "cff3d165179d4aef9596ddbb263e3adb",
|
||||
"ip": Array [
|
||||
"172.26.0.7",
|
||||
],
|
||||
"mac": Array [
|
||||
"02-42-AC-1A-00-07",
|
||||
],
|
||||
"name": "docker-fleet-agent",
|
||||
"os": Object {
|
||||
"family": "debian",
|
||||
"kernel": "5.10.47-linuxkit",
|
||||
"name": "Ubuntu",
|
||||
"platform": "ubuntu",
|
||||
"type": "linux",
|
||||
"version": "20.04.5 LTS (Focal Fossa)",
|
||||
"users": Object {
|
||||
"entityanalytics_okta.user": Object {
|
||||
"_id": "Bnwi8osBcjOsowlA78aM",
|
||||
"_index": ".ds-logs-entityanalytics_okta.user-default-2023.11.15-000001",
|
||||
"_score": null,
|
||||
"fields": Object {
|
||||
"@timestamp": Array [
|
||||
"2023-11-28T13:32:54.446Z",
|
||||
],
|
||||
"_id": Array [
|
||||
"XoEiFowBcjOsowlAIN1T",
|
||||
],
|
||||
"_index": Array [
|
||||
".ds-logs-entityanalytics_okta.user-default-2023.11.15-000001",
|
||||
],
|
||||
"agent.ephemeral_id": Array [
|
||||
"7ddc108f-026a-4a20-afc1-ebc983145df4",
|
||||
],
|
||||
"agent.id": Array [
|
||||
"ced095f0-df97-4bdc-86a9-25cc11238317",
|
||||
],
|
||||
"agent.name": Array [
|
||||
"docker-fleet-agent",
|
||||
],
|
||||
"agent.type": Array [
|
||||
"filebeat",
|
||||
],
|
||||
"agent.version": Array [
|
||||
"8.12.0",
|
||||
],
|
||||
"asset.category": Array [
|
||||
"entity",
|
||||
],
|
||||
"asset.create_date": Array [
|
||||
"2023-11-14T16:33:53.000Z",
|
||||
],
|
||||
"asset.id": Array [
|
||||
"00ud9ohoh9ww644Px5d7",
|
||||
],
|
||||
"asset.last_seen": Array [
|
||||
"2023-11-21T08:08:46.000Z",
|
||||
],
|
||||
"asset.last_status_change_date": Array [
|
||||
"2023-11-15T07:09:05.000Z",
|
||||
],
|
||||
"asset.last_updated": Array [
|
||||
"2023-11-21T08:14:56.000Z",
|
||||
],
|
||||
"asset.status": Array [
|
||||
"ACTIVE",
|
||||
],
|
||||
"asset.type": Array [
|
||||
"okta_user",
|
||||
],
|
||||
"data_stream.dataset": Array [
|
||||
"entityanalytics_okta.user",
|
||||
],
|
||||
"data_stream.namespace": Array [
|
||||
"default",
|
||||
],
|
||||
"data_stream.type": Array [
|
||||
"logs",
|
||||
],
|
||||
"ecs.version": Array [
|
||||
"8.11.0",
|
||||
],
|
||||
"elastic_agent.id": Array [
|
||||
"ced095f0-df97-4bdc-86a9-25cc11238317",
|
||||
],
|
||||
"elastic_agent.snapshot": Array [
|
||||
true,
|
||||
],
|
||||
"elastic_agent.version": Array [
|
||||
"8.12.0",
|
||||
],
|
||||
"entityanalytics_okta.user._links": Array [
|
||||
Object {
|
||||
"self": Object {
|
||||
"href": "https://dev-36006609.okta.com/api/v1/users/00ud9ohoh9ww644Px5d7",
|
||||
},
|
||||
},
|
||||
],
|
||||
"entityanalytics_okta.user.type": Array [
|
||||
Object {
|
||||
"id": "otyf1r6hlGf9AXhZ95d6",
|
||||
},
|
||||
],
|
||||
"event.action": Array [
|
||||
"user-modified",
|
||||
],
|
||||
"event.agent_id_status": Array [
|
||||
"verified",
|
||||
],
|
||||
"event.category": Array [
|
||||
"iam",
|
||||
],
|
||||
"event.dataset": Array [
|
||||
"entityanalytics_okta.user",
|
||||
],
|
||||
"event.ingested": Array [
|
||||
"2023-11-28T13:33:04Z",
|
||||
],
|
||||
"event.kind": Array [
|
||||
"asset",
|
||||
],
|
||||
"event.module": Array [
|
||||
"entityanalytics_okta",
|
||||
],
|
||||
"event.type": Array [
|
||||
"user",
|
||||
"info",
|
||||
],
|
||||
"input.type": Array [
|
||||
"entity-analytics",
|
||||
],
|
||||
"labels.identity_source": Array [
|
||||
"entity-analytics-entityanalytics_okta.user-be940503-bec8-4849-8ec7-2b526d6f2609",
|
||||
],
|
||||
"related.user": Array [
|
||||
"00ud9ohoh9ww644Px5d7",
|
||||
"test@elastic.co",
|
||||
"Test",
|
||||
"User",
|
||||
],
|
||||
"tags": Array [
|
||||
"forwarded",
|
||||
"entityanalytics_okta-user",
|
||||
],
|
||||
"user.account.activated_date": Array [
|
||||
"2023-11-14T16:33:54.000Z",
|
||||
],
|
||||
"user.account.change_date": Array [
|
||||
"2023-11-15T07:09:05.000Z",
|
||||
],
|
||||
"user.account.create_date": Array [
|
||||
"2023-11-14T16:33:53.000Z",
|
||||
],
|
||||
"user.account.password_change_date": Array [
|
||||
"2023-11-15T07:09:05.000Z",
|
||||
],
|
||||
"user.account.status.deprovisioned": Array [
|
||||
false,
|
||||
],
|
||||
"user.account.status.locked_out": Array [
|
||||
false,
|
||||
],
|
||||
"user.account.status.password_expired": Array [
|
||||
false,
|
||||
],
|
||||
"user.account.status.recovery": Array [
|
||||
false,
|
||||
],
|
||||
"user.account.status.suspended": Array [
|
||||
false,
|
||||
],
|
||||
"user.email": Array [
|
||||
"test@elastic.co",
|
||||
],
|
||||
"user.geo.city_name": Array [
|
||||
"Adam",
|
||||
],
|
||||
"user.geo.country_iso_code": Array [
|
||||
"NL",
|
||||
],
|
||||
"user.id": Array [
|
||||
"00ud9ohoh9ww644Px5d7",
|
||||
],
|
||||
"user.name": Array [
|
||||
"test@elastic.co",
|
||||
],
|
||||
"user.name.text": Array [
|
||||
"test@elastic.co",
|
||||
],
|
||||
"user.profile.first_name": Array [
|
||||
"User First Name",
|
||||
],
|
||||
"user.profile.job_title": Array [
|
||||
"Unit Test Writer",
|
||||
],
|
||||
"user.profile.last_name": Array [
|
||||
"Test Last Name",
|
||||
],
|
||||
"user.profile.mobile_phone": Array [
|
||||
"99999999",
|
||||
],
|
||||
"user.profile.primaryPhone": Array [
|
||||
"99999999",
|
||||
],
|
||||
"user.profile.status": Array [
|
||||
"ACTIVE",
|
||||
],
|
||||
},
|
||||
},
|
||||
"user": Object {
|
||||
"email": "tes.user@elastic.co",
|
||||
"first_name": "Taylor",
|
||||
"full_name": "Test user",
|
||||
"id": "39fac578-91fb-47f6-8f7a-fab05ce70d8b",
|
||||
"last_name": "Test last name",
|
||||
"phone": Array [
|
||||
"1235559999",
|
||||
"sort": Array [
|
||||
1700574447551,
|
||||
],
|
||||
},
|
||||
},
|
||||
|
|
|
@ -4,12 +4,42 @@ exports[`buildManagedUserDetailsQuery build query from options correctly 1`] = `
|
|||
Object {
|
||||
"allow_no_indices": true,
|
||||
"body": Object {
|
||||
"aggs": Object {
|
||||
"datasets": Object {
|
||||
"aggs": Object {
|
||||
"latest_hit": Object {
|
||||
"top_hits": Object {
|
||||
"_source": false,
|
||||
"fields": Array [
|
||||
"*",
|
||||
"_index",
|
||||
"_id",
|
||||
],
|
||||
"size": 1,
|
||||
"sort": Array [
|
||||
Object {
|
||||
"@timestamp": Object {
|
||||
"order": "desc",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
"terms": Object {
|
||||
"field": "event.dataset",
|
||||
},
|
||||
},
|
||||
},
|
||||
"query": Object {
|
||||
"bool": Object {
|
||||
"filter": Array [
|
||||
Object {
|
||||
"term": Object {
|
||||
"user.name": "test-user-name",
|
||||
"terms": Object {
|
||||
"event.dataset": Array [
|
||||
"entityanalytics_okta.user",
|
||||
"entityanalytics_entra_id.user",
|
||||
],
|
||||
},
|
||||
},
|
||||
Object {
|
||||
|
@ -18,9 +48,24 @@ Object {
|
|||
},
|
||||
},
|
||||
],
|
||||
"minimum_should_match": 1,
|
||||
"should": Array [
|
||||
Object {
|
||||
"term": Object {
|
||||
"user.name": "test-user-name",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"terms": Object {
|
||||
"user.email": Array [
|
||||
"test-user-name@mail.com",
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
"size": 1,
|
||||
"size": 0,
|
||||
},
|
||||
"ignore_unavailable": true,
|
||||
"index": Array [
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import * as buildQuery from './query.managed_user_details.dsl';
|
||||
import { managedUserDetails } from '.';
|
||||
import type { AzureManagedUser } from '../../../../../../common/search_strategy/security_solution/users/managed_details';
|
||||
import type { ManagedUserFields } from '../../../../../../common/search_strategy/security_solution/users/managed_details';
|
||||
import type { IEsSearchResponse } from '@kbn/data-plugin/public';
|
||||
import type { ManagedUserDetailsRequestOptionsInput } from '../../../../../../common/api/search_strategy';
|
||||
import { UsersQueries } from '../../../../../../common/api/search_strategy';
|
||||
|
@ -15,74 +15,126 @@ import { UsersQueries } from '../../../../../../common/api/search_strategy';
|
|||
export const mockOptions: ManagedUserDetailsRequestOptionsInput = {
|
||||
defaultIndex: ['logs-*'],
|
||||
userName: 'test-user-name',
|
||||
userEmail: ['test-user-name@mail.com'],
|
||||
factoryQueryType: UsersQueries.managedDetails,
|
||||
};
|
||||
|
||||
export const mockSearchStrategyResponse: IEsSearchResponse<AzureManagedUser> = {
|
||||
export const mockSearchStrategyResponse: IEsSearchResponse<ManagedUserFields> = {
|
||||
isPartial: false,
|
||||
isRunning: false,
|
||||
rawResponse: {
|
||||
took: 124,
|
||||
took: 5,
|
||||
timed_out: false,
|
||||
_shards: {
|
||||
total: 2,
|
||||
successful: 2,
|
||||
skipped: 1,
|
||||
skipped: 0,
|
||||
failed: 0,
|
||||
},
|
||||
hits: {
|
||||
max_score: null,
|
||||
hits: [
|
||||
{
|
||||
_index: '.test',
|
||||
_id: '9AxbIocB-WLv2258YZtS',
|
||||
_score: null,
|
||||
_source: {
|
||||
agent: {
|
||||
name: 'docker-fleet-agent',
|
||||
id: '9528bb69-1511-4631-a5af-1d7e93c02009',
|
||||
type: 'filebeat',
|
||||
ephemeral_id: '914fd1fa-aa37-4ab4-b36d-972ab9b19cde',
|
||||
version: '8.8.0',
|
||||
},
|
||||
'@timestamp': '2023-02-23T20:03:17.489Z',
|
||||
host: {
|
||||
hostname: 'docker-fleet-agent',
|
||||
os: {
|
||||
kernel: '5.10.47-linuxkit',
|
||||
name: 'Ubuntu',
|
||||
type: 'linux',
|
||||
family: 'debian',
|
||||
version: '20.04.5 LTS (Focal Fossa)',
|
||||
platform: 'ubuntu',
|
||||
hits: [],
|
||||
},
|
||||
aggregations: {
|
||||
datasets: {
|
||||
doc_count_error_upper_bound: 0,
|
||||
sum_other_doc_count: 0,
|
||||
buckets: [
|
||||
{
|
||||
key: 'entityanalytics_okta.user',
|
||||
doc_count: 122,
|
||||
latest_hit: {
|
||||
hits: {
|
||||
total: {
|
||||
value: 122,
|
||||
relation: 'eq',
|
||||
},
|
||||
max_score: null,
|
||||
hits: [
|
||||
{
|
||||
_index: '.ds-logs-entityanalytics_okta.user-default-2023.11.15-000001',
|
||||
_id: 'Bnwi8osBcjOsowlA78aM',
|
||||
_score: null,
|
||||
fields: {
|
||||
'elastic_agent.version': ['8.12.0'],
|
||||
'event.category': ['iam'],
|
||||
'entityanalytics_okta.user.type': [
|
||||
{
|
||||
id: 'otyf1r6hlGf9AXhZ95d6',
|
||||
},
|
||||
],
|
||||
'user.profile.last_name': ['Test Last Name'],
|
||||
'asset.last_seen': ['2023-11-21T08:08:46.000Z'],
|
||||
'user.profile.status': ['ACTIVE'],
|
||||
'asset.status': ['ACTIVE'],
|
||||
'asset.id': ['00ud9ohoh9ww644Px5d7'],
|
||||
'user.account.status.deprovisioned': [false],
|
||||
'agent.name': ['docker-fleet-agent'],
|
||||
'user.account.status.locked_out': [false],
|
||||
'event.agent_id_status': ['verified'],
|
||||
'event.kind': ['asset'],
|
||||
'user.profile.job_title': ['Unit Test Writer'],
|
||||
'user.profile.first_name': ['User First Name'],
|
||||
'user.id': ['00ud9ohoh9ww644Px5d7'],
|
||||
'user.account.status.suspended': [false],
|
||||
'input.type': ['entity-analytics'],
|
||||
'entityanalytics_okta.user._links': [
|
||||
{
|
||||
self: {
|
||||
href: 'https://dev-36006609.okta.com/api/v1/users/00ud9ohoh9ww644Px5d7',
|
||||
},
|
||||
},
|
||||
],
|
||||
'data_stream.type': ['logs'],
|
||||
'related.user': ['00ud9ohoh9ww644Px5d7', 'test@elastic.co', 'Test', 'User'],
|
||||
tags: ['forwarded', 'entityanalytics_okta-user'],
|
||||
'agent.id': ['ced095f0-df97-4bdc-86a9-25cc11238317'],
|
||||
'ecs.version': ['8.11.0'],
|
||||
'asset.last_updated': ['2023-11-21T08:14:56.000Z'],
|
||||
'labels.identity_source': [
|
||||
'entity-analytics-entityanalytics_okta.user-be940503-bec8-4849-8ec7-2b526d6f2609',
|
||||
],
|
||||
'agent.version': ['8.12.0'],
|
||||
_id: ['XoEiFowBcjOsowlAIN1T'],
|
||||
'asset.category': ['entity'],
|
||||
'user.account.status.recovery': [false],
|
||||
_index: ['.ds-logs-entityanalytics_okta.user-default-2023.11.15-000001'],
|
||||
'user.account.activated_date': ['2023-11-14T16:33:54.000Z'],
|
||||
'asset.type': ['okta_user'],
|
||||
'user.name': ['test@elastic.co'],
|
||||
'asset.last_status_change_date': ['2023-11-15T07:09:05.000Z'],
|
||||
'user.profile.mobile_phone': ['99999999'],
|
||||
'user.geo.city_name': ['Adam'],
|
||||
'agent.type': ['filebeat'],
|
||||
'event.module': ['entityanalytics_okta'],
|
||||
'user.email': ['test@elastic.co'],
|
||||
'user.profile.primaryPhone': ['99999999'],
|
||||
'elastic_agent.snapshot': [true],
|
||||
'asset.create_date': ['2023-11-14T16:33:53.000Z'],
|
||||
'elastic_agent.id': ['ced095f0-df97-4bdc-86a9-25cc11238317'],
|
||||
'user.account.create_date': ['2023-11-14T16:33:53.000Z'],
|
||||
'data_stream.namespace': ['default'],
|
||||
'user.account.change_date': ['2023-11-15T07:09:05.000Z'],
|
||||
'user.geo.country_iso_code': ['NL'],
|
||||
'event.action': ['user-modified'],
|
||||
'event.ingested': ['2023-11-28T13:33:04Z'],
|
||||
'@timestamp': ['2023-11-28T13:32:54.446Z'],
|
||||
'data_stream.dataset': ['entityanalytics_okta.user'],
|
||||
'event.type': ['user', 'info'],
|
||||
'agent.ephemeral_id': ['7ddc108f-026a-4a20-afc1-ebc983145df4'],
|
||||
'user.account.status.password_expired': [false],
|
||||
'user.account.password_change_date': ['2023-11-15T07:09:05.000Z'],
|
||||
'event.dataset': ['entityanalytics_okta.user'],
|
||||
'user.name.text': ['test@elastic.co'],
|
||||
},
|
||||
sort: [1700574447551],
|
||||
},
|
||||
],
|
||||
},
|
||||
ip: ['172.26.0.7'],
|
||||
name: 'docker-fleet-agent',
|
||||
id: 'cff3d165179d4aef9596ddbb263e3adb',
|
||||
mac: ['02-42-AC-1A-00-07'],
|
||||
architecture: 'x86_64',
|
||||
},
|
||||
event: {
|
||||
agent_id_status: 'verified',
|
||||
ingested: '2023-02-23T20:03:18Z',
|
||||
provider: 'Azure AD',
|
||||
kind: 'asset',
|
||||
action: 'user-discovered',
|
||||
type: ['user', 'info'],
|
||||
dataset: 'entityanalytics_azure.users',
|
||||
},
|
||||
user: {
|
||||
full_name: 'Test user',
|
||||
phone: ['1235559999'],
|
||||
last_name: 'Test last name',
|
||||
id: '39fac578-91fb-47f6-8f7a-fab05ce70d8b',
|
||||
first_name: 'Taylor',
|
||||
email: 'tes.user@elastic.co',
|
||||
},
|
||||
},
|
||||
sort: [1677182597489],
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
total: 21,
|
||||
|
|
|
@ -7,33 +7,65 @@
|
|||
|
||||
import type { IEsSearchResponse } from '@kbn/data-plugin/common';
|
||||
|
||||
import type { SearchResponse } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { getOr } from 'lodash/fp';
|
||||
import { inspectStringifyObject } from '../../../../../utils/build_query';
|
||||
import type { SecuritySolutionFactory } from '../../types';
|
||||
import { buildManagedUserDetailsQuery } from './query.managed_user_details.dsl';
|
||||
|
||||
import type { UsersQueries } from '../../../../../../common/search_strategy/security_solution/users';
|
||||
import type {
|
||||
AzureManagedUser,
|
||||
ManagedUserHits,
|
||||
ManagedUserHit,
|
||||
ManagedUserDetailsStrategyResponse,
|
||||
ManagedUserFields,
|
||||
} from '../../../../../../common/search_strategy/security_solution/users/managed_details';
|
||||
import { ManagedUserDatasetKey } from '../../../../../../common/search_strategy/security_solution/users/managed_details';
|
||||
|
||||
interface ManagedUserBucket {
|
||||
key: ManagedUserDatasetKey;
|
||||
latest_hit: SearchResponse<ManagedUserFields | ManagedUserFields>;
|
||||
}
|
||||
|
||||
export const managedUserDetails: SecuritySolutionFactory<UsersQueries.managedDetails> = {
|
||||
buildDsl: (options) => buildManagedUserDetailsQuery(options),
|
||||
parse: async (
|
||||
options,
|
||||
response: IEsSearchResponse<AzureManagedUser>
|
||||
response: IEsSearchResponse<ManagedUserFields>
|
||||
): Promise<ManagedUserDetailsStrategyResponse> => {
|
||||
const inspect = {
|
||||
dsl: [inspectStringifyObject(buildManagedUserDetailsQuery(options))],
|
||||
};
|
||||
|
||||
const hits = response.rawResponse.hits.hits;
|
||||
const userDetails = hits.length > 0 ? hits[0]._source : undefined;
|
||||
const buckets: ManagedUserBucket[] = getOr(
|
||||
[],
|
||||
'aggregations.datasets.buckets',
|
||||
response.rawResponse
|
||||
);
|
||||
|
||||
const managedUsers: ManagedUserHits = buckets.reduce(
|
||||
(acc: ManagedUserHits, bucket: ManagedUserBucket) => {
|
||||
acc[bucket.key] = bucket.latest_hit.hits.hits[0] as unknown as ManagedUserHit;
|
||||
return acc;
|
||||
},
|
||||
{} as ManagedUserHits
|
||||
);
|
||||
|
||||
if (buckets.length === 0) {
|
||||
return {
|
||||
...response,
|
||||
inspect,
|
||||
users: {
|
||||
[ManagedUserDatasetKey.ENTRA]: undefined,
|
||||
[ManagedUserDatasetKey.OKTA]: undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...response,
|
||||
inspect,
|
||||
userDetails,
|
||||
users: managedUsers,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
|
|
@ -11,8 +11,9 @@ import { buildManagedUserDetailsQuery } from './query.managed_user_details.dsl';
|
|||
|
||||
export const mockOptions: ManagedUserDetailsRequestOptions = {
|
||||
defaultIndex: ['logs-*'],
|
||||
userName: 'test-user-name',
|
||||
userEmail: ['test-user-name@mail.com'],
|
||||
factoryQueryType: UsersQueries.managedDetails,
|
||||
userName: 'test-user-name',
|
||||
};
|
||||
|
||||
describe('buildManagedUserDetailsQuery', () => {
|
||||
|
|
|
@ -6,14 +6,29 @@
|
|||
*/
|
||||
|
||||
import type { ISearchRequestParams } from '@kbn/data-plugin/common';
|
||||
import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { ManagedUserDatasetKey } from '../../../../../../common/search_strategy/security_solution/users/managed_details';
|
||||
import type { ManagedUserDetailsRequestOptions } from '../../../../../../common/api/search_strategy';
|
||||
import { EVENT_KIND_ASSET_FILTER } from '../../../../../../common/search_strategy';
|
||||
|
||||
export const buildManagedUserDetailsQuery = ({
|
||||
userEmail,
|
||||
userName,
|
||||
defaultIndex,
|
||||
}: ManagedUserDetailsRequestOptions): ISearchRequestParams => {
|
||||
const filter = [{ term: { 'user.name': userName } }, EVENT_KIND_ASSET_FILTER];
|
||||
const should: QueryDslQueryContainer[] = [{ term: { 'user.name': userName } }];
|
||||
|
||||
if (userEmail) {
|
||||
const emailQuery = { terms: { 'user.email': userEmail } };
|
||||
should.push(emailQuery);
|
||||
}
|
||||
|
||||
const filter = [
|
||||
{
|
||||
terms: { 'event.dataset': [ManagedUserDatasetKey.OKTA, ManagedUserDatasetKey.ENTRA] },
|
||||
},
|
||||
EVENT_KIND_ASSET_FILTER,
|
||||
];
|
||||
|
||||
const dslQuery = {
|
||||
allow_no_indices: true,
|
||||
|
@ -21,8 +36,31 @@ export const buildManagedUserDetailsQuery = ({
|
|||
ignore_unavailable: true,
|
||||
track_total_hits: false,
|
||||
body: {
|
||||
query: { bool: { filter } },
|
||||
size: 1,
|
||||
query: { bool: { filter, should, minimum_should_match: 1 } },
|
||||
size: 0,
|
||||
aggs: {
|
||||
datasets: {
|
||||
terms: {
|
||||
field: 'event.dataset',
|
||||
},
|
||||
aggs: {
|
||||
latest_hit: {
|
||||
top_hits: {
|
||||
fields: ['*', '_index', '_id'], // '_index' and '_id' are not returned by default
|
||||
_source: false,
|
||||
size: 1,
|
||||
sort: [
|
||||
{
|
||||
'@timestamp': {
|
||||
order: 'desc' as const,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
sort: [{ '@timestamp': 'desc' }],
|
||||
};
|
||||
|
|
|
@ -36612,13 +36612,9 @@
|
|||
"xpack.securitySolution.timeline.userDetails.failManagedUserDescription": "Impossible de lancer la recherche sur des données gérées par l'utilisateur",
|
||||
"xpack.securitySolution.timeline.userDetails.familyLabel": "Famille",
|
||||
"xpack.securitySolution.timeline.userDetails.fieldColumnTitle": "Champ",
|
||||
"xpack.securitySolution.timeline.userDetails.firstNameLabel": "Prénom",
|
||||
"xpack.securitySolution.timeline.userDetails.firstSeenLabel": "Vu en premier",
|
||||
"xpack.securitySolution.timeline.userDetails.fullNameLabel": "Nom complet",
|
||||
"xpack.securitySolution.timeline.userDetails.hideManagedDataButton": "Masquer les données Azure AD",
|
||||
"xpack.securitySolution.timeline.userDetails.hostOsNameLabel": "Système d'exploitation",
|
||||
"xpack.securitySolution.timeline.userDetails.ipAddressesLabel": "Adresses IP",
|
||||
"xpack.securitySolution.timeline.userDetails.lastNameLabel": "Nom",
|
||||
"xpack.securitySolution.timeline.userDetails.lastSeenLabel": "Vu en dernier",
|
||||
"xpack.securitySolution.timeline.userDetails.managedBadge": "GÉRÉ",
|
||||
"xpack.securitySolution.timeline.userDetails.managedDataTitle": "Données gérées",
|
||||
|
@ -36631,9 +36627,7 @@
|
|||
"xpack.securitySolution.timeline.userDetails.observedBadge": "OBSERVÉ",
|
||||
"xpack.securitySolution.timeline.userDetails.observedDataTitle": "Données observées",
|
||||
"xpack.securitySolution.timeline.userDetails.observedUserInspectTitle": "Utilisateur observé",
|
||||
"xpack.securitySolution.timeline.userDetails.phoneLabel": "Téléphone",
|
||||
"xpack.securitySolution.timeline.userDetails.riskScoreLabel": "Score de risque",
|
||||
"xpack.securitySolution.timeline.userDetails.showManagedDataButton": "Afficher les données Azure AD",
|
||||
"xpack.securitySolution.timeline.userDetails.userIdLabel": "ID utilisateur",
|
||||
"xpack.securitySolution.timeline.userDetails.userLabel": "Utilisateur",
|
||||
"xpack.securitySolution.timeline.userDetails.valuesColumnTitle": "Valeurs",
|
||||
|
|
|
@ -36611,13 +36611,9 @@
|
|||
"xpack.securitySolution.timeline.userDetails.failManagedUserDescription": "ユーザーが管理するデータで検索を実行できませんでした",
|
||||
"xpack.securitySolution.timeline.userDetails.familyLabel": "ファミリー",
|
||||
"xpack.securitySolution.timeline.userDetails.fieldColumnTitle": "フィールド",
|
||||
"xpack.securitySolution.timeline.userDetails.firstNameLabel": "名",
|
||||
"xpack.securitySolution.timeline.userDetails.firstSeenLabel": "初回の認識",
|
||||
"xpack.securitySolution.timeline.userDetails.fullNameLabel": "フルネーム",
|
||||
"xpack.securitySolution.timeline.userDetails.hideManagedDataButton": "Azure ADデータを非表示",
|
||||
"xpack.securitySolution.timeline.userDetails.hostOsNameLabel": "オペレーティングシステム",
|
||||
"xpack.securitySolution.timeline.userDetails.ipAddressesLabel": "IP アドレス",
|
||||
"xpack.securitySolution.timeline.userDetails.lastNameLabel": "姓",
|
||||
"xpack.securitySolution.timeline.userDetails.lastSeenLabel": "前回の認識",
|
||||
"xpack.securitySolution.timeline.userDetails.managedBadge": "管理対象",
|
||||
"xpack.securitySolution.timeline.userDetails.managedDataTitle": "管理対象のデータ",
|
||||
|
@ -36630,9 +36626,7 @@
|
|||
"xpack.securitySolution.timeline.userDetails.observedBadge": "観測済み",
|
||||
"xpack.securitySolution.timeline.userDetails.observedDataTitle": "観測されたデータ",
|
||||
"xpack.securitySolution.timeline.userDetails.observedUserInspectTitle": "観測されたユーザー",
|
||||
"xpack.securitySolution.timeline.userDetails.phoneLabel": "電話",
|
||||
"xpack.securitySolution.timeline.userDetails.riskScoreLabel": "リスクスコア",
|
||||
"xpack.securitySolution.timeline.userDetails.showManagedDataButton": "Azure ADデータを表示",
|
||||
"xpack.securitySolution.timeline.userDetails.userIdLabel": "ユーザーID",
|
||||
"xpack.securitySolution.timeline.userDetails.userLabel": "ユーザー",
|
||||
"xpack.securitySolution.timeline.userDetails.valuesColumnTitle": "値",
|
||||
|
|
|
@ -36606,13 +36606,9 @@
|
|||
"xpack.securitySolution.timeline.userDetails.failManagedUserDescription": "无法对用户托管数据执行搜索",
|
||||
"xpack.securitySolution.timeline.userDetails.familyLabel": "系列",
|
||||
"xpack.securitySolution.timeline.userDetails.fieldColumnTitle": "字段",
|
||||
"xpack.securitySolution.timeline.userDetails.firstNameLabel": "名字",
|
||||
"xpack.securitySolution.timeline.userDetails.firstSeenLabel": "首次看到时间",
|
||||
"xpack.securitySolution.timeline.userDetails.fullNameLabel": "全名",
|
||||
"xpack.securitySolution.timeline.userDetails.hideManagedDataButton": "隐藏 Azure AD 数据",
|
||||
"xpack.securitySolution.timeline.userDetails.hostOsNameLabel": "操作系统",
|
||||
"xpack.securitySolution.timeline.userDetails.ipAddressesLabel": "IP 地址",
|
||||
"xpack.securitySolution.timeline.userDetails.lastNameLabel": "姓氏",
|
||||
"xpack.securitySolution.timeline.userDetails.lastSeenLabel": "最后看到时间",
|
||||
"xpack.securitySolution.timeline.userDetails.managedBadge": "托管",
|
||||
"xpack.securitySolution.timeline.userDetails.managedDataTitle": "托管数据",
|
||||
|
@ -36625,9 +36621,7 @@
|
|||
"xpack.securitySolution.timeline.userDetails.observedBadge": "已观察",
|
||||
"xpack.securitySolution.timeline.userDetails.observedDataTitle": "观察数据",
|
||||
"xpack.securitySolution.timeline.userDetails.observedUserInspectTitle": "已观察用户",
|
||||
"xpack.securitySolution.timeline.userDetails.phoneLabel": "电话",
|
||||
"xpack.securitySolution.timeline.userDetails.riskScoreLabel": "风险分数",
|
||||
"xpack.securitySolution.timeline.userDetails.showManagedDataButton": "显示 Azure AD 数据",
|
||||
"xpack.securitySolution.timeline.userDetails.userIdLabel": "用户 ID",
|
||||
"xpack.securitySolution.timeline.userDetails.userLabel": "用户",
|
||||
"xpack.securitySolution.timeline.userDetails.valuesColumnTitle": "值",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue