mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 10:40:07 -04:00
Security Entity Analytics - Privileged user monitoring - Privileged access detection support (#224008)
# Overview This pull request adds capabilities associated with the [Privileged Access Detection (PAD) anomaly integration package](https://www.elastic.co/docs/reference/integrations/pad) as a first-class-citizen into the Entity Analytics Privileged User Monitoring feature. # How to test - Pull this branch into your local machine - Ensure that the security experimental flag `privilegeMonitoringEnabled` has been enabled, for example by setting `xpack.securitySolution.enableExperimental: [privilegeMonitoringEnabled]` in your `config/kibana.dev.yml` file - Start Elasticsearch and Kibana - From the [security-documents-generator](https://github.com/elastic/security-documents-generator) repository, run the following command: `yarn start privileged-user-monitoring` (ensuring your config is pointing to your locally running Elastic cluster). This will load "source" events that are anomalous in nature. You can run this command more than once if desired to upload more than 10 users. - Grab the CSV file that the generator created in its console output. - Open the Entity analytics page and navigate to dashboards (by clicking "Go to dashboards") - Add the privileged users from the previous step to the privileged users index using the CSV File Upload option. > [!NOTE] > Any errors regarding risk scoring are unrelated to this PR, and are being resolved separately - You should see a panel that says "Enable Privileged access detection". Click "Install", and you'll meet a loading state. - Once complete, you shouldn't see any results. That's because, even though we **install** the ML jobs by default, we don't **run** them by default. - Click "ML Job Settings", and note that only `pad` jobs should be displaying in this callout. Feel free to test this callout's links and filtering options. - Click "Run job" next to the job called `pad_linux_high_count_privileged_process_events_by_user`. This is the job for which we have anomaly data. - Click away from the callout. **You still shouldn't see data.** That's because there just aren't any anomalies "today". - Change the global date filter at the top of the screen to "Last 30 days". - You should see something similar to this: <img width="1441" alt="Screenshot 2025-06-16 at 12 50 25 AM" src="https://github.com/user-attachments/assets/2b3f11f2-f45d-4716-bb8e-79d2b585aa3e" /> - Congrats for making it this far! Some things to play around with: - Click around on the anomaly filters (i.e., click 25-50), and notice that the results will change. - Ensure that the ordering of the users is based on the **highest single anomaly score in any visible bucket**. Meaning, if user `samwise` has only one anomaly, but its score is 99, and user `frodo` has dozens of anomalies, but no higher than, say, 80, `samwise` will be on the top of the list. - Click the user names to open the appropriate user flyout - Change the global time filter to change ranges, and ensure the data shows up appropriately. The buckets will try to roughly show 30 buckets total, but will have a maximum of 3 hours, meaning if your range is too small, it may show fewer than 30 buckets. This is intentional behavior, as the PAD jobs have an anomaly job window of 3 hours. - Click the button that says "View all in Anomaly Explorer", which uses the currently selected global time range, and compare results > [!WARNING] > Remember that the users in the privileged user monitoring table are only those that you designated as privileged users in a previous step. In contrast, the Anomaly Explorer page shows **all** users. Additionally, note that there may be very slight differences between the swimlanes, because ES|QL calculates the bucket dates slightly differently than the Anomaly Explorer. **This should not affect the results themselves, only the buckets that an individual anomaly score might fall in.** You might see a single anomaly fall into one visual bucket instead of another, but the date should be correct. # Helpful hints If you'd like an easy way to "reset" the Privileged Access Detection package and delete its associated jobs (and anomaly data), so that you can redo the onboarding flow, you can run the below commands (changing any credentials as necessary): ```shell curl "http://localhost:5601/api/fleet/epm/packages/pad/0.5.0" \ -X 'DELETE' \ -H 'elastic-api-version: 2023-10-31' \ -H 'kbn-xsrf:true' \ --user elastic:changeme curl "http://localhost:5601/internal/ml/jobs/delete_jobs" \ -H 'kbn-xsrf:true' \ --user elastic:changeme \ -X 'POST' \ -H 'elastic-api-version: 1' \ -H 'x-elastic-internal-origin:kibana' \ -H 'Content-Type: application/json' \ --data-raw '{ "jobIds": [ "pad_linux_high_count_privileged_process_events_by_user", "pad_linux_high_median_process_command_line_entropy_by_user", "pad_linux_rare_process_executed_by_user", "pad_okta_high_sum_concurrent_sessions_by_user", "pad_okta_rare_host_name_by_user", "pad_okta_rare_region_name_by_user", "pad_okta_rare_source_ip_by_user", "pad_okta_spike_in_group_application_assignment_changes", "pad_okta_spike_in_group_lifecycle_changes", "pad_okta_spike_in_group_membership_changes", "pad_okta_spike_in_group_privilege_changes", "pad_okta_spike_in_user_lifecycle_management_changes", "pad_windows_high_count_group_management_events", "pad_windows_high_count_special_logon_events", "pad_windows_high_count_special_privilege_use_events", "pad_windows_high_count_user_account_management_events", "pad_windows_rare_device_by_user", "pad_windows_rare_group_name_by_user", "pad_windows_rare_privilege_assigned_to_user", "pad_windows_rare_region_name_by_user", "pad_windows_rare_source_ip_by_user" ], "deleteUserAnnotations": true, "deleteAlertingRules": false } ' ``` # What's left? - This PR does not implement "proactive" permissions checks. Instead, relevant permission issues will cause requests/queries to fail, which are appropriately shown in the UI for troubleshooting. This proactive check will be tackled in [this (private) followup issue](https://github.com/elastic/security-team/issues/12822). - There is not yet a way to "upgrade" the package directly from the Privileged User Monitoring screen. This would be a nice addition later on, instead of requiring users to navigate to the integration page. This behavior will be tackled in [this (private) followup issue](https://github.com/elastic/security-team/issues/12823). --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
5d696d579d
commit
1b7cb0f29b
42 changed files with 2645 additions and 35 deletions
|
@ -11317,6 +11317,72 @@ paths:
|
|||
summary: List all monitored users
|
||||
tags:
|
||||
- Security Entity Analytics API
|
||||
/api/entity_analytics/privileged_user_monitoring/pad/install:
|
||||
post:
|
||||
operationId: InstallPrivilegedAccessDetectionPackage
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
required:
|
||||
- message
|
||||
description: Successful response
|
||||
summary: Installs the privileged access detection package for the Entity Analytics privileged user monitoring experience
|
||||
tags:
|
||||
- Security Entity Analytics API
|
||||
/api/entity_analytics/privileged_user_monitoring/pad/status:
|
||||
get:
|
||||
operationId: GetPrivilegedAccessDetectionPackageStatus
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
jobs:
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
description:
|
||||
type: string
|
||||
job_id:
|
||||
type: string
|
||||
state:
|
||||
enum:
|
||||
- closing
|
||||
- closed
|
||||
- opened
|
||||
- failed
|
||||
- opening
|
||||
type: string
|
||||
required:
|
||||
- job_id
|
||||
- state
|
||||
type: array
|
||||
ml_module_setup_status:
|
||||
enum:
|
||||
- complete
|
||||
- incomplete
|
||||
type: string
|
||||
package_installation_status:
|
||||
enum:
|
||||
- complete
|
||||
- incomplete
|
||||
type: string
|
||||
required:
|
||||
- package_installation_status
|
||||
- ml_module_setup_status
|
||||
- jobs
|
||||
description: Privileged access detection status retrieved
|
||||
summary: Gets the status of the privileged access detection package for the Entity Analytics privileged user monitoring experience
|
||||
tags:
|
||||
- Security Entity Analytics API
|
||||
/api/entity_store/enable:
|
||||
post:
|
||||
operationId: InitEntityStore
|
||||
|
|
|
@ -13476,6 +13476,72 @@ paths:
|
|||
summary: List all monitored users
|
||||
tags:
|
||||
- Security Entity Analytics API
|
||||
/api/entity_analytics/privileged_user_monitoring/pad/install:
|
||||
post:
|
||||
operationId: InstallPrivilegedAccessDetectionPackage
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
required:
|
||||
- message
|
||||
description: Successful response
|
||||
summary: Installs the privileged access detection package for the Entity Analytics privileged user monitoring experience
|
||||
tags:
|
||||
- Security Entity Analytics API
|
||||
/api/entity_analytics/privileged_user_monitoring/pad/status:
|
||||
get:
|
||||
operationId: GetPrivilegedAccessDetectionPackageStatus
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
jobs:
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
description:
|
||||
type: string
|
||||
job_id:
|
||||
type: string
|
||||
state:
|
||||
enum:
|
||||
- closing
|
||||
- closed
|
||||
- opened
|
||||
- failed
|
||||
- opening
|
||||
type: string
|
||||
required:
|
||||
- job_id
|
||||
- state
|
||||
type: array
|
||||
ml_module_setup_status:
|
||||
enum:
|
||||
- complete
|
||||
- incomplete
|
||||
type: string
|
||||
package_installation_status:
|
||||
enum:
|
||||
- complete
|
||||
- incomplete
|
||||
type: string
|
||||
required:
|
||||
- package_installation_status
|
||||
- ml_module_setup_status
|
||||
- jobs
|
||||
description: Privileged access detection status retrieved
|
||||
summary: Gets the status of the privileged access detection package for the Entity Analytics privileged user monitoring experience
|
||||
tags:
|
||||
- Security Entity Analytics API
|
||||
/api/entity_store/enable:
|
||||
post:
|
||||
operationId: InitEntityStore
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
||||
/*
|
||||
* NOTICE: Do not edit this file manually.
|
||||
* This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator.
|
||||
*
|
||||
* info:
|
||||
* title: Install privileged access detection package
|
||||
* version: 2023-10-31
|
||||
*/
|
||||
|
||||
import { z } from '@kbn/zod';
|
||||
|
||||
export type InstallPrivilegedAccessDetectionPackageResponse = z.infer<
|
||||
typeof InstallPrivilegedAccessDetectionPackageResponse
|
||||
>;
|
||||
export const InstallPrivilegedAccessDetectionPackageResponse = z.object({
|
||||
message: z.string(),
|
||||
});
|
|
@ -0,0 +1,24 @@
|
|||
openapi: 3.0.0
|
||||
info:
|
||||
title: Install privileged access detection package
|
||||
version: "2023-10-31"
|
||||
paths:
|
||||
/api/entity_analytics/privileged_user_monitoring/pad/install:
|
||||
post:
|
||||
x-labels: [ess, serverless]
|
||||
x-codegen-enabled: true
|
||||
operationId: InstallPrivilegedAccessDetectionPackage
|
||||
summary: Installs the privileged access detection package for the Entity Analytics privileged user monitoring experience
|
||||
|
||||
responses:
|
||||
"200":
|
||||
description: Successful response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- message
|
||||
properties:
|
||||
message:
|
||||
type: string
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/*
|
||||
* NOTICE: Do not edit this file manually.
|
||||
* This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator.
|
||||
*
|
||||
* info:
|
||||
* title: Get the status of the privileged access detection package
|
||||
* version: 2023-10-31
|
||||
*/
|
||||
|
||||
import { z } from '@kbn/zod';
|
||||
|
||||
export type GetPrivilegedAccessDetectionPackageStatusResponse = z.infer<
|
||||
typeof GetPrivilegedAccessDetectionPackageStatusResponse
|
||||
>;
|
||||
export const GetPrivilegedAccessDetectionPackageStatusResponse = z.object({
|
||||
package_installation_status: z.enum(['complete', 'incomplete']),
|
||||
ml_module_setup_status: z.enum(['complete', 'incomplete']),
|
||||
jobs: z.array(
|
||||
z.object({
|
||||
job_id: z.string(),
|
||||
description: z.string().optional(),
|
||||
state: z.enum(['closing', 'closed', 'opened', 'failed', 'opening']),
|
||||
})
|
||||
),
|
||||
});
|
|
@ -0,0 +1,45 @@
|
|||
openapi: 3.0.0
|
||||
info:
|
||||
title: Get the status of the privileged access detection package
|
||||
version: "2023-10-31"
|
||||
paths:
|
||||
/api/entity_analytics/privileged_user_monitoring/pad/status:
|
||||
get:
|
||||
x-labels: [ess, serverless]
|
||||
x-codegen-enabled: true
|
||||
operationId: GetPrivilegedAccessDetectionPackageStatus
|
||||
summary: Gets the status of the privileged access detection package for the Entity Analytics privileged user monitoring experience
|
||||
|
||||
responses:
|
||||
"200":
|
||||
description: Privileged access detection status retrieved
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- package_installation_status
|
||||
- ml_module_setup_status
|
||||
- jobs
|
||||
properties:
|
||||
package_installation_status:
|
||||
type: string
|
||||
enum: [complete, incomplete]
|
||||
ml_module_setup_status:
|
||||
type: string
|
||||
enum: [complete, incomplete]
|
||||
jobs:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
required:
|
||||
- job_id
|
||||
- state
|
||||
properties:
|
||||
job_id:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
state:
|
||||
type: string
|
||||
enum: [closing, closed, opened, failed, opening]
|
|
@ -265,6 +265,8 @@ import type {
|
|||
} from './entity_analytics/monitoring/search_indices.gen';
|
||||
import type { InitMonitoringEngineResponse } from './entity_analytics/privilege_monitoring/engine/init.gen';
|
||||
import type { PrivMonHealthResponse } from './entity_analytics/privilege_monitoring/health.gen';
|
||||
import type { InstallPrivilegedAccessDetectionPackageResponse } from './entity_analytics/privilege_monitoring/privileged_access_detection/install.gen';
|
||||
import type { GetPrivilegedAccessDetectionPackageStatusResponse } from './entity_analytics/privilege_monitoring/privileged_access_detection/status.gen';
|
||||
import type {
|
||||
CreatePrivMonUserRequestBodyInput,
|
||||
CreatePrivMonUserResponse,
|
||||
|
@ -1429,6 +1431,20 @@ finalize it.
|
|||
})
|
||||
.catch(catchAxiosErrorFormatAndThrow);
|
||||
}
|
||||
async getPrivilegedAccessDetectionPackageStatus() {
|
||||
this.log.info(
|
||||
`${new Date().toISOString()} Calling API GetPrivilegedAccessDetectionPackageStatus`
|
||||
);
|
||||
return this.kbnClient
|
||||
.request<GetPrivilegedAccessDetectionPackageStatusResponse>({
|
||||
path: '/api/entity_analytics/privileged_user_monitoring/pad/status',
|
||||
headers: {
|
||||
[ELASTIC_HTTP_VERSION_HEADER]: '2023-10-31',
|
||||
},
|
||||
method: 'GET',
|
||||
})
|
||||
.catch(catchAxiosErrorFormatAndThrow);
|
||||
}
|
||||
async getProtectionUpdatesNote(props: GetProtectionUpdatesNoteProps) {
|
||||
this.log.info(`${new Date().toISOString()} Calling API GetProtectionUpdatesNote`);
|
||||
return this.kbnClient
|
||||
|
@ -1867,6 +1883,20 @@ providing you with the most current and effective threat detection capabilities.
|
|||
})
|
||||
.catch(catchAxiosErrorFormatAndThrow);
|
||||
}
|
||||
async installPrivilegedAccessDetectionPackage() {
|
||||
this.log.info(
|
||||
`${new Date().toISOString()} Calling API InstallPrivilegedAccessDetectionPackage`
|
||||
);
|
||||
return this.kbnClient
|
||||
.request<InstallPrivilegedAccessDetectionPackageResponse>({
|
||||
path: '/api/entity_analytics/privileged_user_monitoring/pad/install',
|
||||
headers: {
|
||||
[ELASTIC_HTTP_VERSION_HEADER]: '2023-10-31',
|
||||
},
|
||||
method: 'POST',
|
||||
})
|
||||
.catch(catchAxiosErrorFormatAndThrow);
|
||||
}
|
||||
async internalUploadAssetCriticalityRecords(props: InternalUploadAssetCriticalityRecordsProps) {
|
||||
this.log.info(`${new Date().toISOString()} Calling API InternalUploadAssetCriticalityRecords`);
|
||||
return this.kbnClient
|
||||
|
|
|
@ -476,6 +476,76 @@ paths:
|
|||
summary: List all monitored users
|
||||
tags:
|
||||
- Security Entity Analytics API
|
||||
/api/entity_analytics/privileged_user_monitoring/pad/install:
|
||||
post:
|
||||
operationId: InstallPrivilegedAccessDetectionPackage
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
required:
|
||||
- message
|
||||
description: Successful response
|
||||
summary: >-
|
||||
Installs the privileged access detection package for the Entity
|
||||
Analytics privileged user monitoring experience
|
||||
tags:
|
||||
- Security Entity Analytics API
|
||||
/api/entity_analytics/privileged_user_monitoring/pad/status:
|
||||
get:
|
||||
operationId: GetPrivilegedAccessDetectionPackageStatus
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
jobs:
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
description:
|
||||
type: string
|
||||
job_id:
|
||||
type: string
|
||||
state:
|
||||
enum:
|
||||
- closing
|
||||
- closed
|
||||
- opened
|
||||
- failed
|
||||
- opening
|
||||
type: string
|
||||
required:
|
||||
- job_id
|
||||
- state
|
||||
type: array
|
||||
ml_module_setup_status:
|
||||
enum:
|
||||
- complete
|
||||
- incomplete
|
||||
type: string
|
||||
package_installation_status:
|
||||
enum:
|
||||
- complete
|
||||
- incomplete
|
||||
type: string
|
||||
required:
|
||||
- package_installation_status
|
||||
- ml_module_setup_status
|
||||
- jobs
|
||||
description: Privileged access detection status retrieved
|
||||
summary: >-
|
||||
Gets the status of the privileged access detection package for the
|
||||
Entity Analytics privileged user monitoring experience
|
||||
tags:
|
||||
- Security Entity Analytics API
|
||||
/api/entity_store/enable:
|
||||
post:
|
||||
operationId: InitEntityStore
|
||||
|
|
|
@ -476,6 +476,76 @@ paths:
|
|||
summary: List all monitored users
|
||||
tags:
|
||||
- Security Entity Analytics API
|
||||
/api/entity_analytics/privileged_user_monitoring/pad/install:
|
||||
post:
|
||||
operationId: InstallPrivilegedAccessDetectionPackage
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
required:
|
||||
- message
|
||||
description: Successful response
|
||||
summary: >-
|
||||
Installs the privileged access detection package for the Entity
|
||||
Analytics privileged user monitoring experience
|
||||
tags:
|
||||
- Security Entity Analytics API
|
||||
/api/entity_analytics/privileged_user_monitoring/pad/status:
|
||||
get:
|
||||
operationId: GetPrivilegedAccessDetectionPackageStatus
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
jobs:
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
description:
|
||||
type: string
|
||||
job_id:
|
||||
type: string
|
||||
state:
|
||||
enum:
|
||||
- closing
|
||||
- closed
|
||||
- opened
|
||||
- failed
|
||||
- opening
|
||||
type: string
|
||||
required:
|
||||
- job_id
|
||||
- state
|
||||
type: array
|
||||
ml_module_setup_status:
|
||||
enum:
|
||||
- complete
|
||||
- incomplete
|
||||
type: string
|
||||
package_installation_status:
|
||||
enum:
|
||||
- complete
|
||||
- incomplete
|
||||
type: string
|
||||
required:
|
||||
- package_installation_status
|
||||
- ml_module_setup_status
|
||||
- jobs
|
||||
description: Privileged access detection status retrieved
|
||||
summary: >-
|
||||
Gets the status of the privileged access detection package for the
|
||||
Entity Analytics privileged user monitoring experience
|
||||
tags:
|
||||
- Security Entity Analytics API
|
||||
/api/entity_store/enable:
|
||||
post:
|
||||
operationId: InitEntityStore
|
||||
|
|
|
@ -0,0 +1,127 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { PrivilegedAccessDetectionsPanel } from '.';
|
||||
import { TestProviders } from '../../../../../common/mock';
|
||||
import { useQueryToggle } from '../../../../../common/containers/query_toggle';
|
||||
import { usePrivilegedAccessDetectionRoutes } from './pad_routes';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
jest.mock('../../../../../common/containers/query_toggle', () => ({
|
||||
useQueryToggle: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('./pad_routes', () => ({
|
||||
usePrivilegedAccessDetectionRoutes: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockAllRoutes = {
|
||||
getPrivilegedAccessDetectionStatus: jest.fn(),
|
||||
setupPrivilegedAccessDetectionMlModule: jest.fn(),
|
||||
installPrivilegedAccessDetectionPackage: jest.fn(),
|
||||
};
|
||||
|
||||
const promiseThatNeverSettles = () => new Promise(() => {});
|
||||
|
||||
describe('PrivilegedAccessDetectionsPanel', () => {
|
||||
const mockUseQueryToggle = useQueryToggle as jest.Mock;
|
||||
const mockUsePrivilegedAccessDetectionRoutes = usePrivilegedAccessDetectionRoutes as jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockUseQueryToggle.mockReturnValue({
|
||||
toggleStatus: true,
|
||||
setToggleStatus: jest.fn(),
|
||||
});
|
||||
mockUsePrivilegedAccessDetectionRoutes.mockReturnValue({
|
||||
...mockAllRoutes,
|
||||
});
|
||||
});
|
||||
|
||||
it(`renders a loading element when we don't yet have a status`, async () => {
|
||||
mockUsePrivilegedAccessDetectionRoutes.mockReturnValue({
|
||||
...mockAllRoutes,
|
||||
getPrivilegedAccessDetectionStatus: () => promiseThatNeverSettles(),
|
||||
});
|
||||
render(<PrivilegedAccessDetectionsPanel spaceId={'default'} />, { wrapper: TestProviders });
|
||||
|
||||
await waitFor(() => expect(screen.getByTestId('pad-loading-status')).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it(`renders the enablement prompt if the package hasn't been installed`, async () => {
|
||||
mockUsePrivilegedAccessDetectionRoutes.mockReturnValue({
|
||||
...mockAllRoutes,
|
||||
getPrivilegedAccessDetectionStatus: () => ({
|
||||
package_installation_status: 'incomplete',
|
||||
ml_module_setup_status: 'incomplete',
|
||||
jobs: [],
|
||||
}),
|
||||
});
|
||||
render(<PrivilegedAccessDetectionsPanel spaceId={'default'} />, { wrapper: TestProviders });
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText('Enable Privileged access detection.')).toBeInTheDocument()
|
||||
);
|
||||
});
|
||||
|
||||
it(`renders enablement even if only the ML module hasn't been set up`, async () => {
|
||||
mockUsePrivilegedAccessDetectionRoutes.mockReturnValue({
|
||||
...mockAllRoutes,
|
||||
getPrivilegedAccessDetectionStatus: () => ({
|
||||
package_installation_status: 'complete',
|
||||
ml_module_setup_status: 'incomplete',
|
||||
jobs: [],
|
||||
}),
|
||||
});
|
||||
render(<PrivilegedAccessDetectionsPanel spaceId={'default'} />, { wrapper: TestProviders });
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText('Enable Privileged access detection.')).toBeInTheDocument()
|
||||
);
|
||||
});
|
||||
|
||||
it(`once installation is complete, ensure the header is visible`, async () => {
|
||||
mockUsePrivilegedAccessDetectionRoutes.mockReturnValue({
|
||||
...mockAllRoutes,
|
||||
getPrivilegedAccessDetectionStatus: () => ({
|
||||
package_installation_status: 'complete',
|
||||
ml_module_setup_status: 'complete',
|
||||
jobs: [],
|
||||
}),
|
||||
});
|
||||
render(<PrivilegedAccessDetectionsPanel spaceId={'default'} />, { wrapper: TestProviders });
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText('Top privileged access detection anomalies')).toBeInTheDocument()
|
||||
);
|
||||
});
|
||||
|
||||
it('shows a loading state while installation is in progress', async () => {
|
||||
mockUsePrivilegedAccessDetectionRoutes.mockReturnValue({
|
||||
...mockAllRoutes,
|
||||
getPrivilegedAccessDetectionStatus: () => ({
|
||||
package_installation_status: 'incomplete',
|
||||
ml_module_setup_status: 'incomplete',
|
||||
jobs: [],
|
||||
}),
|
||||
installPrivilegedAccessDetectionPackage: () => promiseThatNeverSettles(),
|
||||
});
|
||||
render(<PrivilegedAccessDetectionsPanel spaceId={'default'} />, { wrapper: TestProviders });
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText('Enable Privileged access detection.')).toBeInTheDocument()
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByTestId('privilegedUserMonitoringEnablementButton'));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText('Installing Privileged access detection package')).toBeInTheDocument()
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,155 @@
|
|||
/*
|
||||
* 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 { EuiCallOut, EuiEmptyPrompt, EuiFlexGroup, EuiPanel, EuiProgress } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { PrivilegedAccessDetectionMLPopover } from './pad_ml_popover';
|
||||
import { HeaderSection } from '../../../../../common/components/header_section';
|
||||
import { PRIVILEGED_USER_ACTIVITY_QUERY_ID } from '../privileged_user_activity/constants';
|
||||
import { useQueryToggle } from '../../../../../common/containers/query_toggle';
|
||||
import { PrivilegedAccessDetectionChart } from './pad_chart';
|
||||
import { usePrivilegedAccessDetectionRoutes } from './pad_routes';
|
||||
import { PrivilegedAccessDetectionInstallPrompt } from './pad_install_prompt';
|
||||
import { PrivilegedAccessDetectionViewAllAnomaliesButton } from './pad_view_all_anomalies_button';
|
||||
|
||||
const TITLE = i18n.translate(
|
||||
'xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.topPrivilegedAccessDetectionAnomalies.title',
|
||||
{ defaultMessage: 'Top privileged access detection anomalies' }
|
||||
);
|
||||
|
||||
const PRIVILEGED_ACCESS_DETECTIONS_QUERY_ID = 'privileged-access-detection-query';
|
||||
|
||||
const PRIVILEGED_ACCESS_DETECTIONS_STATUS_REFRESH_INTERVAL_IN_MS = 10_000;
|
||||
|
||||
export const PrivilegedAccessDetectionsPanel: React.FC<{ spaceId: string }> = ({ spaceId }) => {
|
||||
const { toggleStatus, setToggleStatus } = useQueryToggle(PRIVILEGED_USER_ACTIVITY_QUERY_ID);
|
||||
|
||||
const {
|
||||
getPrivilegedAccessDetectionStatus,
|
||||
setupPrivilegedAccessDetectionMlModule,
|
||||
installPrivilegedAccessDetectionPackage,
|
||||
} = usePrivilegedAccessDetectionRoutes();
|
||||
|
||||
const {
|
||||
data: padInstallationStatus,
|
||||
error: padInstallationStatusError,
|
||||
refetch: refetchInstallationStatus,
|
||||
} = useQuery(['padInstallationStatus'], getPrivilegedAccessDetectionStatus, {
|
||||
refetchInterval: PRIVILEGED_ACCESS_DETECTIONS_STATUS_REFRESH_INTERVAL_IN_MS,
|
||||
});
|
||||
|
||||
const setupMlModuleMutation = useMutation({ mutationFn: setupPrivilegedAccessDetectionMlModule });
|
||||
const installPrivilegedAccessDetectionPackageMutation = useMutation({
|
||||
mutationFn: installPrivilegedAccessDetectionPackage,
|
||||
});
|
||||
|
||||
const currentlyInstalling =
|
||||
installPrivilegedAccessDetectionPackageMutation.isLoading || setupMlModuleMutation.isLoading;
|
||||
|
||||
const install = async () => {
|
||||
await installPrivilegedAccessDetectionPackageMutation.mutateAsync();
|
||||
await setupMlModuleMutation.mutateAsync();
|
||||
await refetchInstallationStatus();
|
||||
};
|
||||
|
||||
const packageInstallationComplete =
|
||||
padInstallationStatus?.package_installation_status === 'complete' &&
|
||||
padInstallationStatus?.ml_module_setup_status === 'complete';
|
||||
|
||||
return (
|
||||
<>
|
||||
{padInstallationStatusError && (
|
||||
<EuiCallOut
|
||||
title={i18n.translate(
|
||||
'xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.privilegedAccessDetection.errorStatus',
|
||||
{
|
||||
defaultMessage:
|
||||
'There was an error retrieving the status of the privileged access detection package.',
|
||||
}
|
||||
)}
|
||||
color="danger"
|
||||
iconType="error"
|
||||
/>
|
||||
)}
|
||||
{!padInstallationStatus && !padInstallationStatusError && (
|
||||
<EuiPanel data-test-subj={'pad-loading-status'} hasShadow={false} hasBorder={true}>
|
||||
<EuiProgress size="xs" color="accent" />
|
||||
</EuiPanel>
|
||||
)}
|
||||
{padInstallationStatus && !packageInstallationComplete && !currentlyInstalling && (
|
||||
<PrivilegedAccessDetectionInstallPrompt
|
||||
installationErrorOccurred={
|
||||
!!installPrivilegedAccessDetectionPackageMutation.error || !!setupMlModuleMutation.error
|
||||
}
|
||||
install={install}
|
||||
/>
|
||||
)}
|
||||
|
||||
{currentlyInstalling && (
|
||||
<>
|
||||
<EuiPanel hasShadow={false} hasBorder={true}>
|
||||
<EuiEmptyPrompt
|
||||
hasBorder={false}
|
||||
iconType="logoSecurity"
|
||||
title={
|
||||
<h2>
|
||||
{i18n.translate(
|
||||
'xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.topPrivilegedAccessDetectionAnomalies.privilegedAccessDetection',
|
||||
{ defaultMessage: 'Privileged access detection' }
|
||||
)}
|
||||
</h2>
|
||||
}
|
||||
body={
|
||||
<>
|
||||
<p>
|
||||
{i18n.translate(
|
||||
'xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.topPrivilegedAccessDetectionAnomalies.installingPrivilegedAccessDetection',
|
||||
{ defaultMessage: 'Installing Privileged access detection package' }
|
||||
)}
|
||||
</p>
|
||||
<EuiProgress size="s" color="accent" />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</EuiPanel>
|
||||
</>
|
||||
)}
|
||||
|
||||
{packageInstallationComplete && (
|
||||
<EuiPanel hasBorder hasShadow={false} data-test-subj="privileged-access-detections-panel">
|
||||
<HeaderSection
|
||||
toggleStatus={toggleStatus}
|
||||
toggleQuery={setToggleStatus}
|
||||
id={PRIVILEGED_ACCESS_DETECTIONS_QUERY_ID}
|
||||
showInspectButton={false}
|
||||
title={TITLE}
|
||||
titleSize="s"
|
||||
outerDirection="column"
|
||||
hideSubtitle
|
||||
>
|
||||
{toggleStatus && (
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
<PrivilegedAccessDetectionMLPopover />
|
||||
<PrivilegedAccessDetectionViewAllAnomaliesButton />
|
||||
</EuiFlexGroup>
|
||||
)}
|
||||
</HeaderSection>
|
||||
{toggleStatus && (
|
||||
<>
|
||||
<PrivilegedAccessDetectionChart
|
||||
jobIds={padInstallationStatus.jobs.map((eachJob) => eachJob.job_id)}
|
||||
spaceId={spaceId}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</EuiPanel>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,171 @@
|
|||
/*
|
||||
* 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 {
|
||||
usePadTopAnomalousUsersEsqlSource,
|
||||
usePadAnomalyDataEsqlSource,
|
||||
} from './pad_esql_source_query_hooks';
|
||||
|
||||
const trimEsql = (str: string) => str.replace(/[\n]/g, '').replace(/\s\s+/g, ' ').trim();
|
||||
|
||||
jest.mock('./pad_heatmap_interval_hooks', () => ({
|
||||
useIntervalForHeatmap: () => 24,
|
||||
}));
|
||||
|
||||
describe('the source queries for privileged access detection', () => {
|
||||
describe('the top anomalous users ESQL query', () => {
|
||||
it('includes all of the jobs passed in, and respects the provided usersLimit', () => {
|
||||
const query = usePadTopAnomalousUsersEsqlSource({
|
||||
jobIds: ['jobOne', 'jobTwo'],
|
||||
anomalyBands: [],
|
||||
spaceId: 'default',
|
||||
usersLimit: 100,
|
||||
});
|
||||
|
||||
expect(trimEsql(query)).toEqual(
|
||||
trimEsql(`
|
||||
FROM .ml-anomalies-shared
|
||||
| WHERE job_id IN ("jobOne", "jobTwo")
|
||||
| WHERE record_score IS NOT NULL AND user.name IS NOT NULL
|
||||
| RENAME @timestamp AS event_timestamp
|
||||
| LOOKUP JOIN .entity_analytics.monitoring.users-default ON user.name
|
||||
| RENAME event_timestamp AS @timestamp
|
||||
| WHERE user.is_privileged == true
|
||||
| STATS max_record_score = MAX(record_score), user.is_privileged = TOP(user.is_privileged, 100, "desc") by user.name
|
||||
| WHERE user.is_privileged == true
|
||||
| SORT max_record_score DESC
|
||||
| KEEP user.name
|
||||
| LIMIT 100
|
||||
`)
|
||||
);
|
||||
});
|
||||
|
||||
it('has only the hidden anomaly bands', () => {
|
||||
const query = usePadTopAnomalousUsersEsqlSource({
|
||||
jobIds: ['job'],
|
||||
anomalyBands: [
|
||||
{
|
||||
start: 0,
|
||||
end: 25,
|
||||
color: '#ffffff',
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
start: 25,
|
||||
end: 50,
|
||||
color: '#ffffff',
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
start: 50,
|
||||
end: 75,
|
||||
color: '#ffffff',
|
||||
hidden: false,
|
||||
},
|
||||
],
|
||||
spaceId: 'default',
|
||||
usersLimit: 100,
|
||||
});
|
||||
|
||||
expect(trimEsql(query)).toEqual(
|
||||
trimEsql(`
|
||||
FROM .ml-anomalies-shared
|
||||
| WHERE job_id IN ("job")
|
||||
| WHERE record_score IS NOT NULL AND user.name IS NOT NULL
|
||||
| WHERE record_score < 0 OR record_score >= 25
|
||||
| WHERE record_score < 25 OR record_score >= 50
|
||||
| RENAME @timestamp AS event_timestamp
|
||||
| LOOKUP JOIN .entity_analytics.monitoring.users-default ON user.name
|
||||
| RENAME event_timestamp AS @timestamp
|
||||
| WHERE user.is_privileged == true
|
||||
| STATS max_record_score = MAX(record_score), user.is_privileged = TOP(user.is_privileged, 100, "desc") by user.name
|
||||
| WHERE user.is_privileged == true
|
||||
| SORT max_record_score DESC
|
||||
| KEEP user.name
|
||||
| LIMIT 100
|
||||
`)
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('the anomalies query', () => {
|
||||
it('has the provided jobs and usernames, and uses the 24h interval coming from the heatmap interval', () => {
|
||||
const query = usePadAnomalyDataEsqlSource({
|
||||
jobIds: ['jobOne', 'jobTwo'],
|
||||
anomalyBands: [],
|
||||
spaceId: 'default',
|
||||
userNames: ['cloud', 'squall', 'zidane'],
|
||||
});
|
||||
expect(trimEsql(query ?? fail('Query must not be undefined'))).toEqual(
|
||||
trimEsql(`
|
||||
FROM .ml-anomalies-shared
|
||||
| WHERE job_id IN ("jobOne", "jobTwo")
|
||||
| WHERE record_score IS NOT NULL AND user.name IS NOT NULL AND user.name IN ("cloud", "squall", "zidane")
|
||||
| RENAME @timestamp AS event_timestamp
|
||||
| LOOKUP JOIN .entity_analytics.monitoring.users-default ON user.name
|
||||
| RENAME event_timestamp AS @timestamp
|
||||
| WHERE user.is_privileged == true
|
||||
| EVAL user_name_to_record_score = CONCAT(user.name, " : ", TO_STRING(record_score))
|
||||
| STATS user_name_to_record_score = VALUES(user_name_to_record_score) BY @timestamp = BUCKET(@timestamp, 24h)
|
||||
| MV_EXPAND user_name_to_record_score
|
||||
| DISSECT user_name_to_record_score """%{user.name} : %{record_score}"""
|
||||
| EVAL record_score = TO_DOUBLE(record_score)
|
||||
| KEEP @timestamp, user.name, record_score
|
||||
| STATS record_score = MAX(record_score) BY @timestamp, user.name
|
||||
| SORT record_score DESC
|
||||
`)
|
||||
);
|
||||
});
|
||||
it('has only the hidden anomaly bands', () => {
|
||||
const query = usePadAnomalyDataEsqlSource({
|
||||
jobIds: ['job'],
|
||||
anomalyBands: [
|
||||
{
|
||||
start: 0,
|
||||
end: 25,
|
||||
color: '#ffffff',
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
start: 25,
|
||||
end: 50,
|
||||
color: '#ffffff',
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
start: 50,
|
||||
end: 75,
|
||||
color: '#ffffff',
|
||||
hidden: false,
|
||||
},
|
||||
],
|
||||
spaceId: 'default',
|
||||
userNames: ['ramza'],
|
||||
});
|
||||
expect(trimEsql(query ?? fail('Query must not be undefined'))).toEqual(
|
||||
trimEsql(`
|
||||
FROM .ml-anomalies-shared
|
||||
| WHERE job_id IN ("job")
|
||||
| WHERE record_score IS NOT NULL AND user.name IS NOT NULL AND user.name IN ("ramza")
|
||||
| WHERE record_score < 0 OR record_score >= 25
|
||||
| WHERE record_score < 25 OR record_score >= 50
|
||||
| RENAME @timestamp AS event_timestamp
|
||||
| LOOKUP JOIN .entity_analytics.monitoring.users-default ON user.name
|
||||
| RENAME event_timestamp AS @timestamp
|
||||
| WHERE user.is_privileged == true
|
||||
| EVAL user_name_to_record_score = CONCAT(user.name, " : ", TO_STRING(record_score))
|
||||
| STATS user_name_to_record_score = VALUES(user_name_to_record_score) BY @timestamp = BUCKET(@timestamp, 24h)
|
||||
| MV_EXPAND user_name_to_record_score
|
||||
| DISSECT user_name_to_record_score """%{user.name} : %{record_score}"""
|
||||
| EVAL record_score = TO_DOUBLE(record_score)
|
||||
| KEEP @timestamp, user.name, record_score
|
||||
| STATS record_score = MAX(record_score) BY @timestamp, user.name
|
||||
| SORT record_score DESC
|
||||
`)
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
* 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 { useIntervalForHeatmap } from './pad_heatmap_interval_hooks';
|
||||
import { getPrivilegedMonitorUsersJoin } from '../../../../helpers';
|
||||
import type { AnomalyBand } from '../pad_anomaly_bands';
|
||||
|
||||
const getHiddenBandsFilters = (anomalyBands: AnomalyBand[]) => {
|
||||
const hiddenBands = anomalyBands.filter((each) => each.hidden);
|
||||
const recordScoreFilterClause = (eachHiddenBand: AnomalyBand) =>
|
||||
`| WHERE record_score < ${eachHiddenBand.start} OR record_score >= ${eachHiddenBand.end} `;
|
||||
return hiddenBands.map(recordScoreFilterClause).join('');
|
||||
};
|
||||
|
||||
/**
|
||||
* Currently, this query utilizes the `TOP` ES|QL command to filter for privileged users, and this effectively puts a cap on the number of data
|
||||
* sources, per `user.name`, this query supports. Right now, we have a total of 3 possible values ('integration', 'api', and 'csv'), so 100
|
||||
* is more than enough, and should likely suffice with the current architecture. If the `VALUES` ES|QL command is ever officially supported, this limitation would go away.
|
||||
*/
|
||||
const numberOfSupportedDataSources = 100;
|
||||
|
||||
export const usePadTopAnomalousUsersEsqlSource = ({
|
||||
jobIds,
|
||||
anomalyBands,
|
||||
spaceId,
|
||||
usersLimit,
|
||||
}: {
|
||||
jobIds: string[];
|
||||
anomalyBands: AnomalyBand[];
|
||||
spaceId: string;
|
||||
usersLimit: number;
|
||||
}) => {
|
||||
const formattedJobIds = jobIds.map((each) => `"${each}"`).join(', ');
|
||||
|
||||
return `FROM .ml-anomalies-shared
|
||||
| WHERE job_id IN (${formattedJobIds})
|
||||
| WHERE record_score IS NOT NULL AND user.name IS NOT NULL
|
||||
${getHiddenBandsFilters(anomalyBands)}
|
||||
${getPrivilegedMonitorUsersJoin(spaceId)}
|
||||
| STATS max_record_score = MAX(record_score), user.is_privileged = TOP(user.is_privileged, ${numberOfSupportedDataSources}, "desc") by user.name
|
||||
| WHERE user.is_privileged == true
|
||||
| SORT max_record_score DESC
|
||||
| KEEP user.name
|
||||
| LIMIT ${usersLimit}`;
|
||||
// NOTE: the final `WHERE user.is_privileged == true` should not be necessary, as we've already performed the join and filtered by privileged users by that point. I believe this is a bug in ES|QL. This workaround doesn't cause any issues, however.
|
||||
};
|
||||
|
||||
export const usePadAnomalyDataEsqlSource = ({
|
||||
jobIds,
|
||||
anomalyBands,
|
||||
spaceId,
|
||||
userNames,
|
||||
}: {
|
||||
jobIds: string[];
|
||||
anomalyBands: AnomalyBand[];
|
||||
spaceId: string;
|
||||
userNames?: string[];
|
||||
}) => {
|
||||
const interval = useIntervalForHeatmap();
|
||||
|
||||
if (!userNames) return undefined;
|
||||
const formattedJobIds = jobIds.map((each) => `"${each}"`).join(', ');
|
||||
const formattedUserNames = userNames.map((each) => `"${each}"`).join(', ');
|
||||
|
||||
return `FROM .ml-anomalies-shared
|
||||
| WHERE job_id IN (${formattedJobIds})
|
||||
| WHERE record_score IS NOT NULL AND user.name IS NOT NULL AND user.name IN (${formattedUserNames})
|
||||
${getHiddenBandsFilters(anomalyBands)}
|
||||
${getPrivilegedMonitorUsersJoin(spaceId)}
|
||||
| EVAL user_name_to_record_score = CONCAT(user.name, " : ", TO_STRING(record_score))
|
||||
| STATS user_name_to_record_score = VALUES(user_name_to_record_score) BY @timestamp = BUCKET(@timestamp, ${interval}h)
|
||||
| MV_EXPAND user_name_to_record_score
|
||||
| DISSECT user_name_to_record_score """%{user.name} : %{record_score}"""
|
||||
| EVAL record_score = TO_DOUBLE(record_score)
|
||||
| KEEP @timestamp, user.name, record_score
|
||||
| STATS record_score = MAX(record_score) BY @timestamp, user.name
|
||||
| SORT record_score DESC
|
||||
`;
|
||||
};
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* 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 { useGlobalTime } from '../../../../../../../common/containers/use_global_time';
|
||||
import moment from 'moment';
|
||||
import { useIntervalForHeatmap } from './pad_heatmap_interval_hooks';
|
||||
|
||||
jest.mock('../../../../../../../common/containers/use_global_time', () => ({
|
||||
useGlobalTime: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('useIntervalForHeatmap', () => {
|
||||
const mockUseGlobalTime = useGlobalTime as jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockUseGlobalTime.mockReturnValue({
|
||||
from: 0,
|
||||
to: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns a minimum of three hours for our bucket ranges, no matter how short the query range', () => {
|
||||
const now = moment.now().valueOf();
|
||||
mockUseGlobalTime.mockReturnValue({
|
||||
from: now,
|
||||
to: now,
|
||||
});
|
||||
const interval = useIntervalForHeatmap();
|
||||
expect(interval).toEqual(3);
|
||||
});
|
||||
|
||||
it('in the case of a 30 day interval, returns buckets that are 24 hours in length, as we want to compute 30 buckets', () => {
|
||||
const now = moment.now().valueOf();
|
||||
const thirtyDaysAgo = moment().subtract(30, 'days').valueOf();
|
||||
mockUseGlobalTime.mockReturnValue({
|
||||
from: thirtyDaysAgo,
|
||||
to: now,
|
||||
});
|
||||
const interval = useIntervalForHeatmap();
|
||||
expect(interval).toEqual(24);
|
||||
});
|
||||
|
||||
it('in the case of a 90 day interval, returns buckets that are 24 hours in length, as we still want to compute 30 buckets', () => {
|
||||
const now = moment.now().valueOf();
|
||||
const thirtyDaysAgo = moment().subtract(90, 'days').valueOf();
|
||||
mockUseGlobalTime.mockReturnValue({
|
||||
from: thirtyDaysAgo,
|
||||
to: now,
|
||||
});
|
||||
const interval = useIntervalForHeatmap();
|
||||
expect(interval).toEqual(72);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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 { useGlobalTime } from '../../../../../../../common/containers/use_global_time';
|
||||
|
||||
/**
|
||||
* This function computes the appropriate interval (length of time, in hours) of each bucket of the heatmap in a given timerange.
|
||||
* At most, we will compute 30 buckets, and the interval will be evenly distributed across those 30.
|
||||
* However, the lowest possible interval that will return is 3 (which equates to 3 hours), which means it is possible
|
||||
* for fewer buckets than 30.
|
||||
*
|
||||
* @return a number representing the number of hours per interval.
|
||||
*/
|
||||
export const useIntervalForHeatmap = () => {
|
||||
const { from, to } = useGlobalTime();
|
||||
|
||||
const millisecondsToHours = (millis: number) => {
|
||||
return Number((millis / (1000 * 60 * 60)).toFixed(0));
|
||||
};
|
||||
|
||||
const maximumNumberOfBuckets = 30;
|
||||
const minimumNumberOfBucketInterval = 3;
|
||||
const hoursInRange = millisecondsToHours(new Date(to).getTime() - new Date(from).getTime());
|
||||
const bucketInterval = Number((hoursInRange / maximumNumberOfBuckets).toFixed(0));
|
||||
return bucketInterval < minimumNumberOfBucketInterval
|
||||
? minimumNumberOfBucketInterval
|
||||
: bucketInterval;
|
||||
};
|
|
@ -0,0 +1,144 @@
|
|||
/*
|
||||
* 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 { useQuery } from '@tanstack/react-query';
|
||||
import { getESQLResults } from '@kbn/esql-utils';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useEsqlGlobalFilterQuery } from '../../../../../../../common/hooks/esql/use_esql_global_filter';
|
||||
import { esqlResponseToRecords } from '../../../../../../../common/utils/esql';
|
||||
import { useKibana } from '../../../../../../../common/lib/kibana';
|
||||
import { useErrorToast } from '../../../../../../../common/hooks/use_error_toast';
|
||||
import type { AnomalyBand } from '../pad_anomaly_bands';
|
||||
import {
|
||||
usePadAnomalyDataEsqlSource,
|
||||
usePadTopAnomalousUsersEsqlSource,
|
||||
} from './pad_esql_source_query_hooks';
|
||||
|
||||
interface ESQLRawAnomalyRecord extends Record<string, string | number> {
|
||||
'@timestamp': number | string;
|
||||
record_score: number;
|
||||
'user.name': string;
|
||||
}
|
||||
|
||||
/**
|
||||
* An Anomaly record that ensures consistent timestamps as milliseconds since Epoch time.
|
||||
*/
|
||||
export interface ESQLAnomalyRecord {
|
||||
'@timestamp': number;
|
||||
record_score: number;
|
||||
'user.name': string;
|
||||
}
|
||||
|
||||
const usePrivilegedAccessDetectionTopUsersQuery = (params: {
|
||||
jobIds: string[];
|
||||
spaceId: string;
|
||||
anomalyBands: AnomalyBand[];
|
||||
}) => {
|
||||
const search = useKibana().services.data.search.search;
|
||||
|
||||
const filterQuery = useEsqlGlobalFilterQuery();
|
||||
|
||||
const padTopAnomalousUsersEsqlSource = usePadTopAnomalousUsersEsqlSource({
|
||||
...params,
|
||||
usersLimit: 10,
|
||||
});
|
||||
|
||||
const { isLoading, data, isError } = useQuery(
|
||||
[filterQuery, padTopAnomalousUsersEsqlSource],
|
||||
async ({ signal }) => {
|
||||
return esqlResponseToRecords<{ 'user.name': string }>(
|
||||
(
|
||||
await getESQLResults({
|
||||
esqlQuery: padTopAnomalousUsersEsqlSource,
|
||||
search,
|
||||
signal,
|
||||
filter: filterQuery,
|
||||
})
|
||||
)?.response
|
||||
);
|
||||
}
|
||||
);
|
||||
return {
|
||||
isLoading,
|
||||
userNames: data?.map((each) => each['user.name']),
|
||||
isError,
|
||||
};
|
||||
};
|
||||
|
||||
export const usePrivilegedAccessDetectionAnomaliesQuery = (params: {
|
||||
jobIds: string[];
|
||||
spaceId: string;
|
||||
anomalyBands: AnomalyBand[];
|
||||
}) => {
|
||||
const search = useKibana().services.data.search.search;
|
||||
const filterQuery = useEsqlGlobalFilterQuery();
|
||||
|
||||
const {
|
||||
userNames,
|
||||
isError: isTopUsersError,
|
||||
isLoading: isTopUsersLoading,
|
||||
} = usePrivilegedAccessDetectionTopUsersQuery(params);
|
||||
|
||||
const padAnomalyDataEsqlSource = usePadAnomalyDataEsqlSource({ ...params, userNames });
|
||||
|
||||
const {
|
||||
isLoading: isAnomaliesLoading,
|
||||
data,
|
||||
error,
|
||||
isError: isAnomaliesError,
|
||||
refetch,
|
||||
} = useQuery<{
|
||||
anomalyRecords: ESQLAnomalyRecord[];
|
||||
userNames: string[];
|
||||
}>(
|
||||
[filterQuery, padAnomalyDataEsqlSource, userNames],
|
||||
async ({ signal }) => {
|
||||
if (!padAnomalyDataEsqlSource || !userNames || userNames.length === 0) {
|
||||
return { anomalyRecords: [], userNames: [] };
|
||||
}
|
||||
const anomalyRecords = esqlResponseToRecords<ESQLRawAnomalyRecord>(
|
||||
(
|
||||
await getESQLResults({
|
||||
esqlQuery: padAnomalyDataEsqlSource,
|
||||
search,
|
||||
signal,
|
||||
filter: filterQuery,
|
||||
})
|
||||
).response
|
||||
).map((eachRawRecord) => ({
|
||||
...eachRawRecord,
|
||||
'@timestamp': new Date(eachRawRecord['@timestamp']).getTime(),
|
||||
}));
|
||||
return {
|
||||
anomalyRecords: anomalyRecords ?? [],
|
||||
userNames: userNames ?? [],
|
||||
};
|
||||
},
|
||||
{
|
||||
enabled: !!padAnomalyDataEsqlSource && !!userNames,
|
||||
keepPreviousData: true,
|
||||
}
|
||||
);
|
||||
|
||||
useErrorToast(
|
||||
i18n.translate(
|
||||
'xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.privilegedAccessDetection.queryError',
|
||||
{
|
||||
defaultMessage: 'There was an error loading privileged access detection data',
|
||||
}
|
||||
),
|
||||
error
|
||||
);
|
||||
|
||||
return {
|
||||
data,
|
||||
isLoading: isTopUsersLoading || isAnomaliesLoading,
|
||||
isError: isTopUsersError || isAnomaliesError,
|
||||
refetch,
|
||||
error,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* 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 { EuiFlexGroup } from '@elastic/eui';
|
||||
import { PrivilegedAccessDetectionSeverityFilter } from './pad_chart_severity_filter';
|
||||
import { usePrivilegedAccessDetectionAnomaliesQuery } from './hooks/pad_query_hooks';
|
||||
import { useAnomalyBands } from './pad_anomaly_bands';
|
||||
import { UserNameList } from './pad_user_name_list';
|
||||
import { PrivilegedAccessDetectionHeatmap } from './pad_heatmap';
|
||||
|
||||
export interface PrivilegedAccessDetectionChartProps {
|
||||
jobIds: string[];
|
||||
spaceId: string;
|
||||
}
|
||||
|
||||
export const PrivilegedAccessDetectionChart: React.FC<PrivilegedAccessDetectionChartProps> = ({
|
||||
jobIds,
|
||||
spaceId,
|
||||
}) => {
|
||||
const { bands, toggleHiddenBand } = useAnomalyBands();
|
||||
|
||||
const { data, isLoading, isError } = usePrivilegedAccessDetectionAnomaliesQuery({
|
||||
jobIds,
|
||||
anomalyBands: bands,
|
||||
spaceId,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<PrivilegedAccessDetectionSeverityFilter
|
||||
anomalyBands={bands}
|
||||
toggleHiddenBand={toggleHiddenBand}
|
||||
/>
|
||||
<EuiFlexGroup>
|
||||
<UserNameList userNames={data?.userNames ?? []} />
|
||||
<PrivilegedAccessDetectionHeatmap
|
||||
anomalyBands={bands}
|
||||
records={data?.anomalyRecords ?? []}
|
||||
userNames={data?.userNames ?? []}
|
||||
isLoading={isLoading}
|
||||
isError={isError}
|
||||
/>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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 { useEuiTheme } from '@elastic/eui';
|
||||
import { useState } from 'react';
|
||||
|
||||
export interface AnomalyBand {
|
||||
start: number;
|
||||
end: number;
|
||||
color: string;
|
||||
hidden: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses the `start` and `end` values of the band to determine equality, as the `color` value could theoretically change across renders (though not in practice)
|
||||
*/
|
||||
const bandsAreEqual = (a: AnomalyBand, b: AnomalyBand) => a.start === b.start && a.end === b.end;
|
||||
|
||||
export const useAnomalyBands: () => {
|
||||
bands: AnomalyBand[];
|
||||
toggleHiddenBand: (bandToToggle: AnomalyBand) => void;
|
||||
} = () => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
const bandDefinitions = [
|
||||
{ start: 0, end: 3, color: '#E5F6Fa' }, // TODO, this color needs to align with the usage in ML, ongoing discussion in this ticket: https://github.com/elastic/kibana/issues/217508. Issue to track this todo: https://github.com/elastic/security-team/issues/12810
|
||||
{ start: 3, end: 25, color: euiTheme.colors.severity.neutral },
|
||||
{ start: 25, end: 50, color: euiTheme.colors.severity.warning },
|
||||
{ start: 50, end: 75, color: euiTheme.colors.severity.risk },
|
||||
{ start: 75, end: 100, color: euiTheme.colors.severity.danger },
|
||||
];
|
||||
|
||||
const [bands, setBands] = useState<AnomalyBand[]>(
|
||||
bandDefinitions.map((each) => ({ ...each, hidden: false }))
|
||||
);
|
||||
|
||||
const toggleHiddenBand = (bandToToggle: AnomalyBand) => {
|
||||
setBands((currentBands) =>
|
||||
currentBands.map((band) => ({
|
||||
...band,
|
||||
hidden: bandsAreEqual(bandToToggle, band) ? !band.hidden : band.hidden,
|
||||
}))
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
bands,
|
||||
toggleHiddenBand,
|
||||
};
|
||||
};
|
|
@ -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 React from 'react';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiHealth,
|
||||
EuiIcon,
|
||||
EuiPanel,
|
||||
EuiText,
|
||||
useEuiTheme,
|
||||
} from '@elastic/eui';
|
||||
import type { AnomalyBand } from './pad_anomaly_bands';
|
||||
|
||||
interface SeverityFilterProps {
|
||||
anomalyBands: AnomalyBand[];
|
||||
toggleHiddenBand: (band: AnomalyBand) => void;
|
||||
}
|
||||
|
||||
export const PrivilegedAccessDetectionSeverityFilter: React.FC<SeverityFilterProps> = ({
|
||||
anomalyBands,
|
||||
toggleHiddenBand,
|
||||
}) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
return (
|
||||
<EuiFlexGroup>
|
||||
<EuiPanel grow={false} hasBorder hasShadow={false}>
|
||||
<EuiFlexGroup alignItems={'center'}>
|
||||
<p css={{ fontWeight: euiTheme.font.weight.bold }}>{'Anomaly score'}</p>
|
||||
{anomalyBands.map((band) => {
|
||||
if (band.hidden) {
|
||||
return (
|
||||
<EuiFlexItem
|
||||
key={`${band.start}-${band.end}`}
|
||||
css={{ cursor: 'pointer' }}
|
||||
onClick={() => toggleHiddenBand(band)}
|
||||
grow={false}
|
||||
>
|
||||
<EuiFlexGroup alignItems={'center'} gutterSize={'xs'}>
|
||||
<EuiIcon type={'eyeClosed'} />
|
||||
<EuiText size={'s'} color={euiTheme.colors.textSubdued}>
|
||||
<p>{`${band.start}-${band.end}`}</p>
|
||||
</EuiText>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<EuiHealth
|
||||
key={`${band.start}-${band.end}`}
|
||||
css={{ cursor: 'pointer' }}
|
||||
onClick={() => toggleHiddenBand(band)}
|
||||
color={band.color}
|
||||
>{`${band.start}-${band.end}`}</EuiHealth>
|
||||
);
|
||||
})}
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
<EuiFlexItem />
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
|
@ -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.
|
||||
*/
|
||||
|
||||
export const padChartStyling = {
|
||||
heightOfNoResults: 300,
|
||||
heightOfXAxisLegend: 28,
|
||||
heightOfTopLegend: 32,
|
||||
heightOfEachCell: 40,
|
||||
heightOfUserNamesList: (userNames: string[]) =>
|
||||
userNames.length > 0
|
||||
? userNames.length * padChartStyling.heightOfEachCell
|
||||
: padChartStyling.heightOfNoResults,
|
||||
heightOfHeatmap: (userNames: string[]) =>
|
||||
userNames.length > 0
|
||||
? userNames.length * padChartStyling.heightOfEachCell + padChartStyling.heightOfXAxisLegend
|
||||
: padChartStyling.heightOfNoResults,
|
||||
};
|
|
@ -0,0 +1,208 @@
|
|||
/*
|
||||
* 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 {
|
||||
Chart,
|
||||
Heatmap,
|
||||
type HeatmapStyle,
|
||||
niceTimeFormatter,
|
||||
type RecursivePartial,
|
||||
ScaleType,
|
||||
Settings,
|
||||
} from '@elastic/charts';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiImage,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
EuiLoadingChart,
|
||||
EuiCallOut,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { useIntervalForHeatmap } from './hooks/pad_heatmap_interval_hooks';
|
||||
import { padChartStyling } from './pad_chart_styling';
|
||||
import type { ESQLAnomalyRecord } from './hooks/pad_query_hooks';
|
||||
import { useGlobalTime } from '../../../../../../common/containers/use_global_time';
|
||||
import type { AnomalyBand } from './pad_anomaly_bands';
|
||||
import illustration from '../../../../../../common/images/illustration_product_no_results_magnifying_glass.svg';
|
||||
|
||||
const heatmapComponentStyle: RecursivePartial<HeatmapStyle> = {
|
||||
brushTool: {
|
||||
visible: false,
|
||||
},
|
||||
cell: {
|
||||
maxWidth: 'fill',
|
||||
label: {
|
||||
visible: false,
|
||||
},
|
||||
border: {
|
||||
stroke: 'transparent',
|
||||
strokeWidth: 0,
|
||||
},
|
||||
},
|
||||
xAxisLabel: {
|
||||
fontSize: 12,
|
||||
padding: { top: 10, bottom: 10 },
|
||||
},
|
||||
yAxisLabel: {
|
||||
visible: false, // We do not show the yAxisLabel, as we instead render the user names separately in order to link to the User flyout
|
||||
fontSize: 14,
|
||||
width: 'auto',
|
||||
padding: { left: 10, right: 10 },
|
||||
},
|
||||
};
|
||||
|
||||
interface PrivilegedAccessDetectionHeatmapProps {
|
||||
records: ESQLAnomalyRecord[];
|
||||
anomalyBands: AnomalyBand[];
|
||||
userNames: string[];
|
||||
isLoading: boolean;
|
||||
isError: boolean;
|
||||
}
|
||||
|
||||
const PrivilegedAccessDetectionHeatmapNoResults: React.FC = () => {
|
||||
return (
|
||||
<EuiFlexGroup css={{ maxWidth: '600px' }}>
|
||||
<EuiFlexItem>
|
||||
<EuiText size="s">
|
||||
<EuiTitle>
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.privilegedAccessDetection.noResultsTitle"
|
||||
defaultMessage="No privileged access detection results match your search criteria"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.privilegedAccessDetection.noResultsDescription"
|
||||
defaultMessage={`Now that you've got the privileged access detection anomaly jobs installed, you can click "ML job settings" above to configure and run them within your environment.`}
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiImage
|
||||
size="200px"
|
||||
alt={i18n.translate(
|
||||
'xpack.securitySolution.privilegedUserMonitoring.privilegedAccessDetection.emptyState.illustrationAlt',
|
||||
{
|
||||
defaultMessage: 'No results',
|
||||
}
|
||||
)}
|
||||
url={illustration}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
const useGlobalTimeInMillis = () => {
|
||||
const { from, to } = useGlobalTime();
|
||||
|
||||
return {
|
||||
from: new Date(from).getTime(),
|
||||
to: new Date(to).getTime(),
|
||||
};
|
||||
};
|
||||
|
||||
const useXDomainFromGlobalTime = () => {
|
||||
const { from, to } = useGlobalTimeInMillis();
|
||||
|
||||
return {
|
||||
min: from,
|
||||
max: to,
|
||||
};
|
||||
};
|
||||
|
||||
const useTimeFormatter = () => {
|
||||
const { from, to } = useGlobalTimeInMillis();
|
||||
|
||||
return (value: string | number) =>
|
||||
niceTimeFormatter([from, to])(value, {
|
||||
timeZone: 'UTC',
|
||||
});
|
||||
};
|
||||
|
||||
export const PrivilegedAccessDetectionHeatmap: React.FC<PrivilegedAccessDetectionHeatmapProps> = ({
|
||||
records,
|
||||
anomalyBands,
|
||||
userNames,
|
||||
isLoading,
|
||||
isError,
|
||||
}) => {
|
||||
const intervalForHeatmap = useIntervalForHeatmap();
|
||||
const timeFormatter = useTimeFormatter();
|
||||
const xDomain = useXDomainFromGlobalTime();
|
||||
|
||||
return (
|
||||
<EuiFlexItem
|
||||
css={{
|
||||
marginTop: `${padChartStyling.heightOfTopLegend}px`,
|
||||
height: `${padChartStyling.heightOfHeatmap(userNames)}px`,
|
||||
}}
|
||||
>
|
||||
{isLoading && (
|
||||
<EuiFlexGroup justifyContent={'center'} alignItems={'center'}>
|
||||
<EuiLoadingChart size="xl" />
|
||||
</EuiFlexGroup>
|
||||
)}
|
||||
{isError && (
|
||||
<EuiCallOut
|
||||
title={i18n.translate(
|
||||
'xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.privilegedAccessDetection.anomalyDetectionDataError',
|
||||
{
|
||||
defaultMessage:
|
||||
'There was an error retrieving privileged access detection anomaly data.',
|
||||
}
|
||||
)}
|
||||
color="danger"
|
||||
iconType="error"
|
||||
/>
|
||||
)}
|
||||
{!isLoading && !isError && (
|
||||
<Chart>
|
||||
<Settings
|
||||
theme={{ heatmap: heatmapComponentStyle }}
|
||||
noResults={<PrivilegedAccessDetectionHeatmapNoResults />}
|
||||
xDomain={xDomain}
|
||||
/>
|
||||
<Heatmap
|
||||
id={'privileged-access-detection-heatmap-chart'}
|
||||
xScale={{
|
||||
type: ScaleType.Time,
|
||||
interval: {
|
||||
type: 'fixed',
|
||||
value: intervalForHeatmap,
|
||||
unit: 'h',
|
||||
},
|
||||
}}
|
||||
colorScale={{
|
||||
type: 'bands',
|
||||
bands: anomalyBands,
|
||||
}}
|
||||
data={records}
|
||||
name={i18n.translate(
|
||||
'xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.topPrivilegedAccessDetections.maxAnomalyScore',
|
||||
{ defaultMessage: 'Max anomaly score' }
|
||||
)}
|
||||
xAccessor="@timestamp"
|
||||
xAxisLabelName={''}
|
||||
xAxisLabelFormatter={timeFormatter}
|
||||
yAccessor="user.name"
|
||||
yAxisLabelName={'user.name'}
|
||||
ySortPredicate="numDesc"
|
||||
valueAccessor="record_score"
|
||||
/>
|
||||
</Chart>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* 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 { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiText } from '@elastic/eui';
|
||||
import { css } from '@emotion/css';
|
||||
import { padChartStyling } from './pad_chart_styling';
|
||||
import { UserPanelKey } from '../../../../../../flyout/entity_details/shared/constants';
|
||||
|
||||
const PRIVILEGED_ACCESS_DETECTION_TABLE_ID = 'PadAnomalies-table';
|
||||
|
||||
export const UserNameList: React.FC<{ userNames: string[] }> = ({ userNames }) => {
|
||||
const { openFlyout } = useExpandableFlyoutApi();
|
||||
|
||||
const openUserFlyout = (userName: string) => {
|
||||
openFlyout({
|
||||
right: {
|
||||
id: UserPanelKey,
|
||||
params: {
|
||||
contextID: PRIVILEGED_ACCESS_DETECTION_TABLE_ID,
|
||||
userName,
|
||||
scopeId: PRIVILEGED_ACCESS_DETECTION_TABLE_ID,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiFlexItem
|
||||
css={css`
|
||||
margin-top: ${padChartStyling.heightOfTopLegend}px;
|
||||
height: ${padChartStyling.heightOfUserNamesList(userNames)}px;
|
||||
`}
|
||||
grow={false}
|
||||
>
|
||||
<EuiFlexGroup gutterSize={'none'} direction={'column'} justifyContent={'center'}>
|
||||
{userNames.map((userName) => (
|
||||
<EuiFlexItem
|
||||
key={userName}
|
||||
css={css`
|
||||
justify-content: center;
|
||||
height: ${padChartStyling.heightOfEachCell}px;
|
||||
`}
|
||||
grow={false}
|
||||
>
|
||||
<EuiText textAlign={'right'}>
|
||||
<EuiLink
|
||||
onClick={() => {
|
||||
openUserFlyout(userName);
|
||||
}}
|
||||
>
|
||||
{userName}
|
||||
</EuiLink>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,114 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiButton,
|
||||
EuiCallOut,
|
||||
EuiEmptyPrompt,
|
||||
EuiImage,
|
||||
EuiToolTip,
|
||||
EuiPanel,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import React, { useState } from 'react';
|
||||
import { MlNodeAvailableWarningShared } from '@kbn/ml-plugin/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import dashboardEnableImg from '../../../../images/entity_store_dashboard.png';
|
||||
|
||||
export const PrivilegedAccessDetectionInstallPrompt: React.FC<{
|
||||
installationErrorOccurred: boolean;
|
||||
install: () => Promise<void>;
|
||||
}> = ({ installationErrorOccurred, install }) => {
|
||||
const [mlNodesAvailable, setMlNodesAvailable] = useState(false);
|
||||
|
||||
const privilegedAccessDetectionInstallTooltipText = i18n.translate(
|
||||
'xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.topPrivilegedAccessDetectionAnomalies.installTooltip',
|
||||
{ defaultMessage: 'Install and enable privileged access detection anomaly jobs' }
|
||||
);
|
||||
|
||||
const privilegedAccessDetectionInstallTooltipUnavailableText = i18n.translate(
|
||||
'xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.topPrivilegedAccessDetectionAnomalies.installUnavailableTooltip',
|
||||
{ defaultMessage: 'Unable to install jobs due to no ML node currently available.' }
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiPanel hasShadow={false} hasBorder={true}>
|
||||
<EuiEmptyPrompt
|
||||
hasBorder={false}
|
||||
layout="horizontal"
|
||||
actions={
|
||||
<EuiToolTip
|
||||
content={
|
||||
mlNodesAvailable
|
||||
? privilegedAccessDetectionInstallTooltipText
|
||||
: privilegedAccessDetectionInstallTooltipUnavailableText
|
||||
}
|
||||
>
|
||||
<EuiButton
|
||||
color="primary"
|
||||
disabled={!mlNodesAvailable}
|
||||
fill
|
||||
onClick={() => install()}
|
||||
data-test-subj={`privilegedUserMonitoringEnablementButton`}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.privilegedAccessDetection.enableButton"
|
||||
defaultMessage="Install"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiToolTip>
|
||||
}
|
||||
icon={
|
||||
<EuiImage
|
||||
size="l"
|
||||
hasShadow
|
||||
src={dashboardEnableImg}
|
||||
alt={i18n.translate(
|
||||
'xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.topPrivilegedAccessDetectionAnomalies.installAndEnable',
|
||||
{ defaultMessage: 'Install and Enable.' }
|
||||
)}
|
||||
/>
|
||||
}
|
||||
data-test-subj="privilegedUserMonitoringEnablementPanel"
|
||||
title={
|
||||
<h2>
|
||||
{i18n.translate(
|
||||
'xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.topPrivilegedAccessDetectionAnomalies.enablePrivilegedAccessDetection',
|
||||
{ defaultMessage: 'Enable Privileged access detection.' }
|
||||
)}
|
||||
</h2>
|
||||
}
|
||||
body={
|
||||
<>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.privilegedAccessEnablementDescription"
|
||||
defaultMessage={
|
||||
'Detect anomalous privileged access activity in Windows, Linux and Okta system logs.'
|
||||
}
|
||||
/>
|
||||
</p>
|
||||
<MlNodeAvailableWarningShared size="s" nodeAvailableCallback={setMlNodesAvailable} />
|
||||
{installationErrorOccurred && (
|
||||
<EuiCallOut
|
||||
title={i18n.translate(
|
||||
'xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.privilegedAccessDetection.installErrorStatus',
|
||||
{
|
||||
defaultMessage:
|
||||
'There was an error installing the privileged access detection package.',
|
||||
}
|
||||
)}
|
||||
color="danger"
|
||||
iconType="error"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</EuiPanel>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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 { useSecurityJobs } from '../../../../../../../common/components/ml_popover/hooks/use_security_jobs';
|
||||
import { searchFilter } from '../../../../../../../common/components/ml_popover/helpers';
|
||||
|
||||
export const usePadMlJobs = (searchValue: string) => {
|
||||
const {
|
||||
isMlAdmin,
|
||||
loading: isLoadingSecurityJobs,
|
||||
jobs,
|
||||
refetch: refreshJobs,
|
||||
} = useSecurityJobs();
|
||||
|
||||
const allPrivilegedAccessDetectionJobs = jobs.filter((job) => job.groups.includes('pad'));
|
||||
|
||||
const filteredPrivilegedAccessDetectionJobs = searchFilter(
|
||||
allPrivilegedAccessDetectionJobs,
|
||||
searchValue
|
||||
);
|
||||
|
||||
return {
|
||||
jobs: filteredPrivilegedAccessDetectionJobs,
|
||||
refreshJobs,
|
||||
isMlAdmin,
|
||||
isLoadingSecurityJobs,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
* 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, { useCallback, useState } from 'react';
|
||||
import { EuiButtonEmpty, EuiFlexGroup, EuiPopover, EuiFieldSearch } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { css } from '@emotion/react';
|
||||
import { MLJobsAwaitingNodeWarning, MlNodeAvailableWarningShared } from '@kbn/ml-plugin/public';
|
||||
import { PrivilegedAccessDetectionMLPopoverHeader } from './pad_ml_popover_header';
|
||||
import { useEnableDataFeed } from '../../../../../../common/components/ml_popover/hooks/use_enable_data_feed';
|
||||
import type { SecurityJob } from '../../../../../../common/components/ml_popover/types';
|
||||
import { JobsTable } from '../../../../../../common/components/ml_popover/jobs_table/jobs_table';
|
||||
import { usePadMlJobs } from './hooks/pad_get_jobs_hooks';
|
||||
|
||||
export const PrivilegedAccessDetectionMLPopover: React.FC = () => {
|
||||
const {
|
||||
enableDatafeed,
|
||||
disableDatafeed,
|
||||
isLoading: isLoadingEnableDataFeed,
|
||||
} = useEnableDataFeed();
|
||||
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
|
||||
const { jobs, refreshJobs, isLoadingSecurityJobs, isMlAdmin } = usePadMlJobs(searchValue);
|
||||
|
||||
const handleJobStateChange = useCallback(
|
||||
async (job: SecurityJob, latestTimestampMs: number, enable: boolean) => {
|
||||
if (enable) {
|
||||
await enableDatafeed(job, latestTimestampMs);
|
||||
} else {
|
||||
await disableDatafeed(job);
|
||||
}
|
||||
|
||||
refreshJobs();
|
||||
},
|
||||
[refreshJobs, enableDatafeed, disableDatafeed]
|
||||
);
|
||||
|
||||
const [mlNodesAvailable, setMlNodesAvailable] = useState(false);
|
||||
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
|
||||
const installedJobsIds = jobs.filter((job) => job.isInstalled).map((job) => job.id);
|
||||
|
||||
if (!isMlAdmin) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiPopover
|
||||
anchorPosition="downRight"
|
||||
id="privileged-access-detections-popover"
|
||||
button={
|
||||
<EuiButtonEmpty
|
||||
aria-expanded={isPopoverOpen}
|
||||
aria-haspopup="true"
|
||||
aria-label={i18n.translate(
|
||||
'xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.privilegedAccessDetection.popupAria',
|
||||
{ defaultMessage: 'Privileged Access Detection popup' }
|
||||
)}
|
||||
color="primary"
|
||||
data-test-subj="privileged-access-detections-popover-button"
|
||||
iconType="arrowDown"
|
||||
iconSide="right"
|
||||
onClick={() => {
|
||||
setIsPopoverOpen(!isPopoverOpen);
|
||||
refreshJobs();
|
||||
}}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.privilegedAccessDetection.padMlJobsPopoverText',
|
||||
{ defaultMessage: 'Privileged access detection ML Jobs' }
|
||||
)}
|
||||
</EuiButtonEmpty>
|
||||
}
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={() => setIsPopoverOpen(!isPopoverOpen)}
|
||||
repositionOnScroll
|
||||
>
|
||||
<EuiFlexGroup
|
||||
direction={'column'}
|
||||
css={css`
|
||||
max-width: 700px;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding-bottom: 15px;
|
||||
`}
|
||||
>
|
||||
<PrivilegedAccessDetectionMLPopoverHeader />
|
||||
<EuiFieldSearch
|
||||
placeholder={i18n.translate(
|
||||
'xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.privilegedAccessDetection.searchPlaceholder',
|
||||
{ defaultMessage: 'e.g., Linux, Okta, etc.' }
|
||||
)}
|
||||
value={searchValue}
|
||||
fullWidth
|
||||
onChange={(e) => setSearchValue(e.target.value)}
|
||||
isClearable={true}
|
||||
aria-label={i18n.translate(
|
||||
'xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.privilegedAccessDetection.searchAria',
|
||||
{ defaultMessage: 'Privileged access detection search box' }
|
||||
)}
|
||||
/>
|
||||
<MLJobsAwaitingNodeWarning jobIds={installedJobsIds} />
|
||||
<MlNodeAvailableWarningShared size="s" nodeAvailableCallback={setMlNodesAvailable} />
|
||||
<JobsTable
|
||||
isLoading={isLoadingSecurityJobs || isLoadingEnableDataFeed}
|
||||
jobs={jobs}
|
||||
onJobStateChange={handleJobStateChange}
|
||||
mlNodesAvailable={mlNodesAvailable}
|
||||
/>
|
||||
</EuiFlexGroup>
|
||||
</EuiPopover>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* 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, { useCallback } from 'react';
|
||||
import { useNavigation } from '@kbn/security-solution-navigation';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { EuiLink, EuiText } from '@elastic/eui';
|
||||
import { useIntegrationLinkState } from '../../../../../../common/hooks/integrations/use_integration_link_state';
|
||||
import { ENTITY_ANALYTICS_PRIVILEGED_USER_MONITORING_PATH } from '../../../../../../../common/constants';
|
||||
import { usePrivilegedAccessDetectionIntegration } from '../../../../privileged_user_monitoring_onboarding/hooks/use_integrations';
|
||||
import { INTEGRATION_APP_ID } from '../../../../../../common/lib/integrations/constants';
|
||||
import { addPathParamToUrl } from '../../../../../../common/utils/integrations';
|
||||
|
||||
export const PrivilegedAccessDetectionMLPopoverHeader: React.FC = () => {
|
||||
const state = useIntegrationLinkState(ENTITY_ANALYTICS_PRIVILEGED_USER_MONITORING_PATH);
|
||||
const { navigateTo } = useNavigation();
|
||||
const padPackage = usePrivilegedAccessDetectionIntegration();
|
||||
const navigateToPadIntegration = useCallback(() => {
|
||||
navigateTo({
|
||||
appId: INTEGRATION_APP_ID,
|
||||
path: addPathParamToUrl(
|
||||
`/detail/${padPackage?.name}-${padPackage?.version}/overview`,
|
||||
ENTITY_ANALYTICS_PRIVILEGED_USER_MONITORING_PATH
|
||||
),
|
||||
state,
|
||||
});
|
||||
}, [navigateTo, padPackage, state]);
|
||||
|
||||
return (
|
||||
<EuiText>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.privilegedAccessDetection.padMlJobsDescription"
|
||||
defaultMessage="Run privileged access detection jobs to monitor anomalous behaviors of privileged users in your environment. Note that some jobs may require additional manual steps configured in order to fully function. See the {integrationLink} for details"
|
||||
values={{
|
||||
integrationLink: (
|
||||
<EuiLink external onClick={navigateToPadIntegration}>
|
||||
{'Privileged access detection integration'}
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* 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 { API_VERSIONS } from '../../../../../../common/entity_analytics/constants';
|
||||
import { useKibana } from '../../../../../common/lib/kibana';
|
||||
import type { GetPrivilegedAccessDetectionPackageStatusResponse } from '../../../../../../common/api/entity_analytics/privilege_monitoring/privileged_access_detection/status.gen';
|
||||
|
||||
const PRIVILEGED_ACCESS_DETECTION_INDEX_PATTERN =
|
||||
'logs-*,ml_okta_multiple_user_sessions_pad.all,ml_windows_privilege_type_pad.all';
|
||||
|
||||
export const usePrivilegedAccessDetectionRoutes = () => {
|
||||
const http = useKibana().services.http;
|
||||
|
||||
return useMemo(() => {
|
||||
const getPrivilegedAccessDetectionStatus = async () => {
|
||||
return http.fetch<GetPrivilegedAccessDetectionPackageStatusResponse>(
|
||||
'/api/entity_analytics/privileged_user_monitoring/pad/status',
|
||||
{
|
||||
method: 'GET',
|
||||
version: API_VERSIONS.public.v1,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const installPrivilegedAccessDetectionPackage = async () => {
|
||||
return http.fetch('/api/entity_analytics/privileged_user_monitoring/pad/install', {
|
||||
method: 'POST',
|
||||
version: API_VERSIONS.public.v1,
|
||||
});
|
||||
};
|
||||
|
||||
const setupPrivilegedAccessDetectionMlModule = async () => {
|
||||
return http.fetch('/internal/ml/modules/setup/pad-ml', {
|
||||
version: '1',
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
indexPatternName: PRIVILEGED_ACCESS_DETECTION_INDEX_PATTERN,
|
||||
useDedicatedIndex: false,
|
||||
startDatafeed: false,
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
getPrivilegedAccessDetectionStatus,
|
||||
installPrivilegedAccessDetectionPackage,
|
||||
setupPrivilegedAccessDetectionMlModule,
|
||||
};
|
||||
}, [http]);
|
||||
};
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* 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 { EuiButton, EuiLink } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import React from 'react';
|
||||
import { ML_PAGES, useMlHref } from '@kbn/ml-plugin/public';
|
||||
import { useKibana } from '../../../../../common/lib/kibana';
|
||||
import { useGlobalTime } from '../../../../../common/containers/use_global_time';
|
||||
|
||||
const usePadMlAnomalyExplorerUrl = () => {
|
||||
const { from, to } = useGlobalTime();
|
||||
const { services } = useKibana();
|
||||
|
||||
return useMlHref(
|
||||
services.ml,
|
||||
services.http.basePath.get(),
|
||||
{
|
||||
page: ML_PAGES.ANOMALY_EXPLORER,
|
||||
pageState: {
|
||||
jobIds: ['pad'],
|
||||
timeRange: { from, to },
|
||||
mlExplorerSwimlane: {
|
||||
viewByFieldName: 'user.name',
|
||||
},
|
||||
},
|
||||
},
|
||||
[from, to]
|
||||
);
|
||||
};
|
||||
|
||||
export const PrivilegedAccessDetectionViewAllAnomaliesButton: React.FC = () => {
|
||||
const anomalyExplorerUrl = usePadMlAnomalyExplorerUrl();
|
||||
|
||||
return (
|
||||
<EuiButton color={'primary'} fill={false} iconType={'anomalySwimLane'}>
|
||||
<EuiLink href={anomalyExplorerUrl} external={false} target="_blank">
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.privilegedAccessDetection.anomalyExplorer"
|
||||
defaultMessage="View all in Anomaly Explorer"
|
||||
/>
|
||||
</EuiLink>
|
||||
</EuiButton>
|
||||
);
|
||||
};
|
|
@ -11,6 +11,7 @@ import { FormattedMessage } from '@kbn/i18n-react';
|
|||
import { useSpaceId } from '../../../common/hooks/use_space_id';
|
||||
import { RiskLevelsPrivilegedUsersPanel } from './components/risk_level_panel';
|
||||
import { UserActivityPrivilegedUsersPanel } from './components/privileged_user_activity';
|
||||
import { PrivilegedAccessDetectionsPanel } from './components/privileged_access_detection';
|
||||
|
||||
export interface OnboardingCallout {
|
||||
userCount: number;
|
||||
|
@ -82,11 +83,7 @@ export const PrivilegedUserMonitoring = ({
|
|||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiPanel hasShadow={false} hasBorder={true}>
|
||||
<span>{'TODO: Top privileged access detections'}</span>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
{spaceId && <PrivilegedAccessDetectionsPanel spaceId={spaceId} />}
|
||||
<EuiFlexItem>
|
||||
<UserActivityPrivilegedUsersPanel />
|
||||
</EuiFlexItem>
|
||||
|
|
|
@ -8,6 +8,10 @@
|
|||
import { useGetPackageInfoByKeyQuery } from '@kbn/fleet-plugin/public';
|
||||
import type { GetInfoResponse } from '@kbn/fleet-plugin/common';
|
||||
|
||||
const isGetInfoResponse = (
|
||||
integration: GetInfoResponse | undefined
|
||||
): integration is GetInfoResponse => integration !== undefined;
|
||||
|
||||
export const useEntityAnalyticsIntegrations = () => {
|
||||
const { data: okta } = useGetPackageInfoByKeyQuery(
|
||||
'entityanalytics_okta',
|
||||
|
@ -28,11 +32,20 @@ export const useEntityAnalyticsIntegrations = () => {
|
|||
}
|
||||
);
|
||||
|
||||
const integrations = [okta, ad]
|
||||
.filter<GetInfoResponse>(
|
||||
(integration): integration is GetInfoResponse => integration !== undefined
|
||||
)
|
||||
.map(({ item }) => item);
|
||||
|
||||
return integrations;
|
||||
return [okta, ad].filter<GetInfoResponse>(isGetInfoResponse).map(({ item }) => item);
|
||||
};
|
||||
|
||||
export const usePrivilegedAccessDetectionIntegration = () => {
|
||||
const { data: pad } = useGetPackageInfoByKeyQuery(
|
||||
'pad',
|
||||
undefined, // When package version is undefined it gets the latest version
|
||||
{
|
||||
prerelease: true, // This is a technical preview package, delete this line when it is GA
|
||||
},
|
||||
{
|
||||
suspense: false,
|
||||
}
|
||||
);
|
||||
|
||||
return isGetInfoResponse(pad) ? pad.item : undefined;
|
||||
};
|
||||
|
|
|
@ -38,7 +38,7 @@ export const PrivilegedUserMonitoringOnboardingPanel = ({
|
|||
gutterSize="xl"
|
||||
alignItems="center"
|
||||
>
|
||||
<EuiFlexItem grow={1} paddingSize="xl">
|
||||
<EuiFlexItem grow={1}>
|
||||
<EuiPanel paddingSize="s" hasShadow={false} hasBorder={false} color="subdued">
|
||||
<EuiFlexGroup justifyContent="spaceBetween" direction="column">
|
||||
<EuiFlexGroup gutterSize={'m'} alignItems={'center'}>
|
||||
|
|
|
@ -47,6 +47,7 @@ import { AssetInventoryDataClientMock } from '../../../asset_inventory/asset_inv
|
|||
import { privilegeMonitorDataClientMock } from '../../../entity_analytics/privilege_monitoring/privilege_monitoring_data_client.mock';
|
||||
import { createProductFeaturesServiceMock } from '../../../product_features_service/mocks';
|
||||
import type { EndpointAppContextService } from '../../../../endpoint/endpoint_app_context_services';
|
||||
import { padPackageInstallationClientMock } from '../../../entity_analytics/privilege_monitoring/privileged_access_detection/pad_package_installation_client.mock';
|
||||
|
||||
export const createMockClients = () => {
|
||||
const core = coreMock.createRequestHandlerContext();
|
||||
|
@ -80,6 +81,7 @@ export const createMockClients = () => {
|
|||
assetCriticalityDataClient: assetCriticalityDataClientMock.create(),
|
||||
entityStoreDataClient: entityStoreDataClientMock.create(),
|
||||
privilegeMonitorDataClient: privilegeMonitorDataClientMock.create(),
|
||||
padPackageInstallationClient: padPackageInstallationClientMock.create(),
|
||||
|
||||
internalFleetServices: {
|
||||
packages: packageServiceMock.createClient(),
|
||||
|
@ -191,6 +193,7 @@ const createSecuritySolutionRequestContextMock = (
|
|||
getEntityStoreApiKeyManager: jest.fn(),
|
||||
getEntityStoreDataClient: jest.fn(() => clients.entityStoreDataClient),
|
||||
getPrivilegeMonitoringDataClient: jest.fn(() => clients.privilegeMonitorDataClient),
|
||||
getPadPackageInstallationClient: jest.fn(() => clients.padPackageInstallationClient),
|
||||
getMonitoringEntitySourceDataClient: jest.fn(),
|
||||
getSiemRuleMigrationsClient: jest.fn(() => clients.siemRuleMigrationsClient),
|
||||
getInferenceClient: jest.fn(() => clients.getInferenceClient()),
|
||||
|
|
|
@ -19,28 +19,7 @@ export const PRIVILEGED_MONITOR_USERS_INDEX_MAPPING: MappingProperties = {
|
|||
'user.name': {
|
||||
type: 'keyword',
|
||||
},
|
||||
'labels.is_privileged': {
|
||||
type: 'boolean',
|
||||
},
|
||||
};
|
||||
|
||||
export const PRIVILEGED_MONITOR_GROUPS_INDEX_MAPPING: MappingProperties = {
|
||||
'event.ingested': {
|
||||
type: 'date',
|
||||
},
|
||||
'@timestamp': {
|
||||
type: 'date',
|
||||
},
|
||||
'group.name': {
|
||||
type: 'keyword',
|
||||
},
|
||||
indexPattern: {
|
||||
type: 'keyword',
|
||||
},
|
||||
nameMatcher: {
|
||||
type: 'keyword',
|
||||
},
|
||||
'labels.is_privileged': {
|
||||
'user.is_privileged': {
|
||||
type: 'boolean',
|
||||
},
|
||||
};
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* 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 { PadPackageInstallationClient } from './pad_package_installation_client';
|
||||
|
||||
const createPadPackageInstallationClientMock = () =>
|
||||
({
|
||||
init: jest.fn(),
|
||||
} as unknown as jest.Mocked<PadPackageInstallationClient>);
|
||||
|
||||
export const padPackageInstallationClientMock = { create: createPadPackageInstallationClientMock };
|
|
@ -0,0 +1,191 @@
|
|||
/*
|
||||
* 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 {
|
||||
BulkInstallResponse,
|
||||
IBulkInstallPackageError,
|
||||
} from '@kbn/fleet-plugin/server/services/epm/packages';
|
||||
import {
|
||||
bulkInstallPackages,
|
||||
getInstalledPackages,
|
||||
getPackages,
|
||||
} from '@kbn/fleet-plugin/server/services/epm/packages';
|
||||
|
||||
import type {
|
||||
ElasticsearchClient,
|
||||
IScopedClusterClient,
|
||||
Logger,
|
||||
SavedObjectsClientContract,
|
||||
} from '@kbn/core/server';
|
||||
import type { DataViewsService } from '@kbn/data-views-plugin/common';
|
||||
import type { MlJobStats } from '@elastic/elasticsearch/lib/api/types';
|
||||
import type { Installable, RegistrySearchResult } from '@kbn/fleet-plugin/common';
|
||||
import type { GetPrivilegedAccessDetectionPackageStatusResponse } from '../../../../../common/api/entity_analytics/privilege_monitoring/privileged_access_detection/status.gen';
|
||||
|
||||
interface PadPackageInstallationClientOpts {
|
||||
logger: Logger;
|
||||
clusterClient: IScopedClusterClient;
|
||||
namespace: string;
|
||||
soClient: SavedObjectsClientContract;
|
||||
dataViewsService: DataViewsService;
|
||||
}
|
||||
|
||||
export interface PadMlJob {
|
||||
jobId: string;
|
||||
description: string | undefined;
|
||||
state: string;
|
||||
}
|
||||
|
||||
interface JobStatsByJobId {
|
||||
[key: string]: MlJobStats;
|
||||
}
|
||||
|
||||
export class PadPackageInstallationClient {
|
||||
private readonly esClient: ElasticsearchClient;
|
||||
private readonly soClient: SavedObjectsClientContract;
|
||||
|
||||
constructor(private readonly opts: PadPackageInstallationClientOpts) {
|
||||
this.esClient = opts.clusterClient.asCurrentUser;
|
||||
this.soClient = opts.soClient;
|
||||
}
|
||||
|
||||
private log(level: Exclude<keyof Logger, 'get' | 'log' | 'isLevelEnabled'>, msg: string) {
|
||||
this.opts.logger[level](
|
||||
`[Privileged access detection] [namespace: ${this.opts.namespace}] ${msg}`
|
||||
);
|
||||
}
|
||||
|
||||
private async getCurrentlyInstalledPADPackage() {
|
||||
const installedPadPackages = await getInstalledPackages({
|
||||
savedObjectsClient: this.soClient,
|
||||
esClient: this.esClient,
|
||||
nameQuery: 'pad',
|
||||
perPage: 100,
|
||||
sortOrder: 'asc',
|
||||
});
|
||||
return installedPadPackages.items.find((installedPackage) => installedPackage.name === 'pad');
|
||||
}
|
||||
|
||||
private async getJobs() {
|
||||
const jobs = (await this.esClient.ml.getJobs({ job_id: 'pad' })).jobs.filter(
|
||||
(each) => each.custom_settings.created_by === 'ml-module-pad'
|
||||
);
|
||||
|
||||
const jobStatsByJobId = (await this.esClient.ml.getJobStats({ job_id: 'pad' })).jobs.reduce(
|
||||
(accumulator, nextJobStats) => ({ ...accumulator, [nextJobStats.job_id]: nextJobStats }),
|
||||
{} as JobStatsByJobId
|
||||
);
|
||||
|
||||
return jobs.map((eachJob) => ({
|
||||
job_id: eachJob.job_id,
|
||||
description: eachJob.description,
|
||||
state: jobStatsByJobId[eachJob.job_id].state,
|
||||
}));
|
||||
}
|
||||
|
||||
public async getStatus(): Promise<GetPrivilegedAccessDetectionPackageStatusResponse> {
|
||||
const packageInstalled = !!(await this.getCurrentlyInstalledPADPackage());
|
||||
const packageInstallationStatus = packageInstalled ? 'complete' : 'incomplete';
|
||||
if (!packageInstalled) {
|
||||
// even if there happen to be jobs that match our search criteria, if the package is not installed, we consider the ML installation incomplete and the jobs to not be associated with our privileged access detection usage
|
||||
return {
|
||||
package_installation_status: packageInstallationStatus,
|
||||
ml_module_setup_status: 'incomplete',
|
||||
jobs: [],
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const jobs = await this.getJobs();
|
||||
|
||||
const mlModuleSetupStatus = jobs.length > 0 ? 'complete' : 'incomplete';
|
||||
|
||||
return {
|
||||
package_installation_status: packageInstallationStatus,
|
||||
ml_module_setup_status: mlModuleSetupStatus,
|
||||
jobs,
|
||||
};
|
||||
} catch (e) {
|
||||
this.log(
|
||||
'info',
|
||||
'The privileged access detection package is installed, but the ML jobs are not yet set up.'
|
||||
);
|
||||
return {
|
||||
package_installation_status: packageInstallationStatus,
|
||||
ml_module_setup_status: 'incomplete',
|
||||
jobs: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private async getPrivilegedAccessDetectionPackageFromRegistry() {
|
||||
return (
|
||||
await getPackages({
|
||||
savedObjectsClient: this.soClient,
|
||||
category: 'security',
|
||||
prerelease: true,
|
||||
})
|
||||
).find((availablePackage) => availablePackage.name === 'pad');
|
||||
}
|
||||
|
||||
public async installPrivilegedAccessDetectionPackage() {
|
||||
const alreadyInstalledPadPackage = await this.getCurrentlyInstalledPADPackage();
|
||||
if (alreadyInstalledPadPackage) {
|
||||
return {
|
||||
message: 'Privileged access detection package was already installed.',
|
||||
};
|
||||
}
|
||||
|
||||
const availablePadPackage = await this.getPrivilegedAccessDetectionPackageFromRegistry();
|
||||
|
||||
if (!availablePadPackage) {
|
||||
this.log('info', 'Privileged access detection package was not found');
|
||||
throw new Error('Privileged access detection package was not found.');
|
||||
}
|
||||
|
||||
const installationResponse = await this.installPackage(availablePadPackage);
|
||||
|
||||
if (!installationResponse || this.isInstallError(installationResponse)) {
|
||||
throw new Error(
|
||||
`Failed to install privileged access detection package. ${installationResponse?.error}`
|
||||
);
|
||||
}
|
||||
return {
|
||||
message: 'Successfully installed privileged access detection package.',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* A type guard function to determine if the bulk package installation response is an error type
|
||||
*
|
||||
* @param potentialInstallResponseError the `BulkInstallResponse` to check type of
|
||||
*/
|
||||
private isInstallError = (
|
||||
potentialInstallResponseError: BulkInstallResponse
|
||||
): potentialInstallResponseError is IBulkInstallPackageError => {
|
||||
return (potentialInstallResponseError as IBulkInstallPackageError).error !== undefined;
|
||||
};
|
||||
|
||||
private async installPackage(installablePackage: Installable<RegistrySearchResult>) {
|
||||
this.log(
|
||||
'info',
|
||||
`Installing Privileged Access Detection package: ${installablePackage.name} ${installablePackage.version}`
|
||||
);
|
||||
|
||||
const bulkInstallResponse = await bulkInstallPackages({
|
||||
savedObjectsClient: this.soClient,
|
||||
packagesToInstall: [
|
||||
{ name: installablePackage.name, version: installablePackage.version, prerelease: true },
|
||||
],
|
||||
esClient: this.esClient,
|
||||
spaceId: this.opts.namespace,
|
||||
skipIfInstalled: true,
|
||||
force: true,
|
||||
});
|
||||
return bulkInstallResponse.length > 0 ? bulkInstallResponse[0] : undefined;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* 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 { IKibanaResponse, Logger } from '@kbn/core/server';
|
||||
import { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils';
|
||||
import { transformError } from '@kbn/securitysolution-es-utils';
|
||||
|
||||
import type { GetPrivilegedAccessDetectionPackageStatusResponse } from '../../../../../../common/api/entity_analytics/privilege_monitoring/privileged_access_detection/status.gen';
|
||||
import { API_VERSIONS, APP_ID } from '../../../../../../common/constants';
|
||||
|
||||
import type { EntityAnalyticsRoutesDeps } from '../../../types';
|
||||
|
||||
export const padGetStatusRoute = (
|
||||
router: EntityAnalyticsRoutesDeps['router'],
|
||||
logger: Logger,
|
||||
config: EntityAnalyticsRoutesDeps['config']
|
||||
) => {
|
||||
router.versioned
|
||||
.get({
|
||||
access: 'public',
|
||||
path: '/api/entity_analytics/privileged_user_monitoring/pad/status',
|
||||
security: {
|
||||
authz: {
|
||||
requiredPrivileges: ['securitySolution', `${APP_ID}-entity-analytics`],
|
||||
},
|
||||
},
|
||||
})
|
||||
.addVersion(
|
||||
{
|
||||
version: API_VERSIONS.public.v1,
|
||||
validate: {},
|
||||
},
|
||||
|
||||
async (
|
||||
context,
|
||||
request,
|
||||
response
|
||||
): Promise<IKibanaResponse<GetPrivilegedAccessDetectionPackageStatusResponse>> => {
|
||||
const siemResponse = buildSiemResponse(response);
|
||||
const secSol = await context.securitySolution;
|
||||
|
||||
try {
|
||||
const clientResponse = await secSol.getPadPackageInstallationClient().getStatus();
|
||||
return response.ok({
|
||||
body: {
|
||||
...clientResponse,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
const error = transformError(e);
|
||||
logger.error(`Error with PAD installation: ${error.message}`);
|
||||
return siemResponse.error({
|
||||
statusCode: error.statusCode,
|
||||
body: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* 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 { IKibanaResponse, Logger } from '@kbn/core/server';
|
||||
import { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils';
|
||||
import { transformError } from '@kbn/securitysolution-es-utils';
|
||||
|
||||
import type { InstallPrivilegedAccessDetectionPackageResponse } from '../../../../../../common/api/entity_analytics/privilege_monitoring/privileged_access_detection/install.gen';
|
||||
import { API_VERSIONS, APP_ID } from '../../../../../../common/constants';
|
||||
|
||||
import type { EntityAnalyticsRoutesDeps } from '../../../types';
|
||||
|
||||
export const padInstallRoute = (
|
||||
router: EntityAnalyticsRoutesDeps['router'],
|
||||
logger: Logger,
|
||||
config: EntityAnalyticsRoutesDeps['config']
|
||||
) => {
|
||||
router.versioned
|
||||
.post({
|
||||
access: 'public',
|
||||
path: '/api/entity_analytics/privileged_user_monitoring/pad/install',
|
||||
security: {
|
||||
authz: {
|
||||
requiredPrivileges: ['securitySolution', `${APP_ID}-entity-analytics`],
|
||||
},
|
||||
},
|
||||
})
|
||||
.addVersion(
|
||||
{
|
||||
version: API_VERSIONS.public.v1,
|
||||
validate: {},
|
||||
},
|
||||
|
||||
async (
|
||||
context,
|
||||
request,
|
||||
response
|
||||
): Promise<IKibanaResponse<InstallPrivilegedAccessDetectionPackageResponse>> => {
|
||||
const siemResponse = buildSiemResponse(response);
|
||||
const secSol = await context.securitySolution;
|
||||
|
||||
try {
|
||||
const clientResponse = await secSol
|
||||
.getPadPackageInstallationClient()
|
||||
.installPrivilegedAccessDetectionPackage();
|
||||
return response.ok({
|
||||
body: {
|
||||
...clientResponse,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
const error = transformError(e);
|
||||
logger.error(`Error PAD installation: ${error.message}`);
|
||||
return siemResponse.error({
|
||||
statusCode: error.statusCode,
|
||||
body: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
|
@ -19,6 +19,9 @@ import {
|
|||
uploadUsersCSVRoute,
|
||||
} from './users';
|
||||
|
||||
import { padInstallRoute } from './privileged_access_detection/pad_install';
|
||||
import { padGetStatusRoute } from './privileged_access_detection/pad_get_installation_status';
|
||||
|
||||
export const registerPrivilegeMonitoringRoutes = ({
|
||||
router,
|
||||
logger,
|
||||
|
@ -26,6 +29,8 @@ export const registerPrivilegeMonitoringRoutes = ({
|
|||
}: EntityAnalyticsRoutesDeps) => {
|
||||
initPrivilegeMonitoringEngineRoute(router, logger, config);
|
||||
healthCheckPrivilegeMonitoringRoute(router, logger, config);
|
||||
padInstallRoute(router, logger, config);
|
||||
padGetStatusRoute(router, logger, config);
|
||||
searchPrivilegeMonitoringIndicesRoute(router, logger, config);
|
||||
monitoringEntitySourceRoute(router, logger, config);
|
||||
createUserRoute(router, logger);
|
||||
|
|
|
@ -12,6 +12,7 @@ import type { KibanaRequest, Logger, RequestHandlerContext } from '@kbn/core/ser
|
|||
import type { BuildFlavor } from '@kbn/config';
|
||||
import { EntityDiscoveryApiKeyType } from '@kbn/entityManager-plugin/server/saved_objects';
|
||||
import { MonitoringEntitySourceDataClient } from './lib/entity_analytics/privilege_monitoring/monitoring_entity_source_data_client';
|
||||
import { PadPackageInstallationClient } from './lib/entity_analytics/privilege_monitoring/privileged_access_detection/pad_package_installation_client';
|
||||
import { DEFAULT_SPACE_ID } from '../common/constants';
|
||||
import type { Immutable } from '../common/endpoint/types';
|
||||
import type { EndpointAuthz } from '../common/endpoint/types/authz';
|
||||
|
@ -275,6 +276,15 @@ export class RequestContextFactory implements IRequestContextFactory {
|
|||
soClient: coreContext.savedObjects.client,
|
||||
});
|
||||
}),
|
||||
getPadPackageInstallationClient: memoize(() => {
|
||||
return new PadPackageInstallationClient({
|
||||
clusterClient: coreContext.elasticsearch.client,
|
||||
soClient: coreContext.savedObjects.client,
|
||||
logger: options.logger,
|
||||
namespace: getSpaceId(),
|
||||
dataViewsService,
|
||||
});
|
||||
}),
|
||||
getEntityStoreDataClient: memoize(() => {
|
||||
// why are we defining this here, but other places we do it inline?
|
||||
const clusterClient = coreContext.elasticsearch.client;
|
||||
|
|
|
@ -22,6 +22,7 @@ import type { Readable } from 'stream';
|
|||
import type { AuditLogger } from '@kbn/security-plugin-types-server';
|
||||
import type { InferenceClient } from '@kbn/inference-common';
|
||||
import type { DataViewsService } from '@kbn/data-views-plugin/common';
|
||||
import type { PadPackageInstallationClient } from './lib/entity_analytics/privilege_monitoring/privileged_access_detection/pad_package_installation_client';
|
||||
import type { EndpointAppContextService } from './endpoint/endpoint_app_context_services';
|
||||
import type { Immutable } from '../common/endpoint/types';
|
||||
import { AppClient } from './client';
|
||||
|
@ -71,6 +72,7 @@ export interface SecuritySolutionApiRequestHandlerContext {
|
|||
getEntityStoreDataClient: () => EntityStoreDataClient;
|
||||
getPrivilegeMonitoringDataClient: () => PrivilegeMonitoringDataClient;
|
||||
getMonitoringEntitySourceDataClient: () => MonitoringEntitySourceDataClient;
|
||||
getPadPackageInstallationClient: () => PadPackageInstallationClient;
|
||||
getSiemRuleMigrationsClient: () => SiemRuleMigrationsClient;
|
||||
getInferenceClient: () => InferenceClient;
|
||||
getAssetInventoryClient: () => AssetInventoryDataClient;
|
||||
|
|
|
@ -966,6 +966,18 @@ finalize it.
|
|||
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
|
||||
.query(props.query);
|
||||
},
|
||||
getPrivilegedAccessDetectionPackageStatus(kibanaSpace: string = 'default') {
|
||||
return supertest
|
||||
.get(
|
||||
routeWithNamespace(
|
||||
'/api/entity_analytics/privileged_user_monitoring/pad/status',
|
||||
kibanaSpace
|
||||
)
|
||||
)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.set(ELASTIC_HTTP_VERSION_HEADER, '2023-10-31')
|
||||
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana');
|
||||
},
|
||||
getProtectionUpdatesNote(
|
||||
props: GetProtectionUpdatesNoteProps,
|
||||
kibanaSpace: string = 'default'
|
||||
|
@ -1341,6 +1353,18 @@ providing you with the most current and effective threat detection capabilities.
|
|||
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
|
||||
.send(props.body as object);
|
||||
},
|
||||
installPrivilegedAccessDetectionPackage(kibanaSpace: string = 'default') {
|
||||
return supertest
|
||||
.post(
|
||||
routeWithNamespace(
|
||||
'/api/entity_analytics/privileged_user_monitoring/pad/install',
|
||||
kibanaSpace
|
||||
)
|
||||
)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.set(ELASTIC_HTTP_VERSION_HEADER, '2023-10-31')
|
||||
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana');
|
||||
},
|
||||
internalUploadAssetCriticalityRecords(kibanaSpace: string = 'default') {
|
||||
return supertest
|
||||
.post(routeWithNamespace('/internal/asset_criticality/upload_csv', kibanaSpace))
|
||||
|
|
|
@ -0,0 +1,160 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../../../../ftr_provider_context';
|
||||
import { dataViewRouteHelpersFactory } from '../../../utils/data_view';
|
||||
|
||||
export default ({ getService }: FtrProviderContext) => {
|
||||
const api = getService('securitySolutionApi');
|
||||
const supertest = getService('supertest');
|
||||
const log = getService('log');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
|
||||
const uninstallPackage = async () => {
|
||||
try {
|
||||
await kibanaServer.request({
|
||||
method: 'DELETE',
|
||||
path: '/api/fleet/epm/packages/pad/0.5.0',
|
||||
retries: 1,
|
||||
});
|
||||
} catch (e) {
|
||||
log.info('No existing package found for deletion, continuing');
|
||||
}
|
||||
};
|
||||
|
||||
const deleteMLJobs = async () => {
|
||||
try {
|
||||
return await kibanaServer.request({
|
||||
method: 'POST',
|
||||
path: '/internal/ml/jobs/delete_jobs',
|
||||
body: {
|
||||
jobIds: [
|
||||
'pad_linux_high_count_privileged_process_events_by_user',
|
||||
'pad_linux_high_median_process_command_line_entropy_by_user',
|
||||
'pad_linux_rare_process_executed_by_user',
|
||||
'pad_okta_high_sum_concurrent_sessions_by_user',
|
||||
'pad_okta_rare_host_name_by_user',
|
||||
'pad_okta_rare_region_name_by_user',
|
||||
'pad_okta_rare_source_ip_by_user',
|
||||
'pad_okta_spike_in_group_application_assignment_changes',
|
||||
'pad_okta_spike_in_group_lifecycle_changes',
|
||||
'pad_okta_spike_in_group_membership_changes',
|
||||
'pad_okta_spike_in_group_privilege_changes',
|
||||
'pad_okta_spike_in_user_lifecycle_management_changes',
|
||||
'pad_windows_high_count_group_management_events',
|
||||
'pad_windows_high_count_special_logon_events',
|
||||
'pad_windows_high_count_special_privilege_use_events',
|
||||
'pad_windows_high_count_user_account_management_events',
|
||||
'pad_windows_rare_device_by_user',
|
||||
'pad_windows_rare_group_name_by_user',
|
||||
'pad_windows_rare_privilege_assigned_to_user',
|
||||
'pad_windows_rare_region_name_by_user',
|
||||
'pad_windows_rare_source_ip_by_user',
|
||||
],
|
||||
deleteUserAnnotations: true,
|
||||
deleteAlertingRules: false,
|
||||
},
|
||||
headers: {
|
||||
'elastic-api-version': '1',
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
log.info('Job deletion unsuccessful, but continuing');
|
||||
}
|
||||
};
|
||||
|
||||
const simulateMlModuleSetupFromUI = async () => {
|
||||
try {
|
||||
return await kibanaServer.request({
|
||||
method: 'POST',
|
||||
path: '/internal/ml/modules/setup/pad-ml',
|
||||
body: {
|
||||
indexPatternName:
|
||||
'logs-*,ml_okta_multiple_user_sessions_pad.all,ml_windows_privilege_type_pad.all',
|
||||
useDedicatedIndex: false,
|
||||
startDatafeed: false,
|
||||
},
|
||||
headers: {
|
||||
'elastic-api-version': '1',
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
log.error(e);
|
||||
throw Error('Failed to setup ML module');
|
||||
}
|
||||
};
|
||||
|
||||
describe('@ess @serverless @skipInServerlessMKI Entity Privilege Monitoring APIs', () => {
|
||||
const dataView = dataViewRouteHelpersFactory(supertest);
|
||||
before(async () => {
|
||||
await dataView.create('security-solution');
|
||||
await uninstallPackage();
|
||||
await deleteMLJobs();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await dataView.delete('security-solution');
|
||||
await uninstallPackage();
|
||||
await deleteMLJobs();
|
||||
});
|
||||
|
||||
describe('privileged access detection status and installation APIs', () => {
|
||||
it('should be able to successfully install the package', async () => {
|
||||
const statusResponseBeforeInstallation =
|
||||
await api.getPrivilegedAccessDetectionPackageStatus();
|
||||
|
||||
if (statusResponseBeforeInstallation.status !== 200) {
|
||||
log.error(`Retrieving status failed`);
|
||||
log.error(JSON.stringify(statusResponseBeforeInstallation.body));
|
||||
}
|
||||
|
||||
expect(statusResponseBeforeInstallation.status).eql(200);
|
||||
|
||||
const {
|
||||
package_installation_status: packageInstallationStatusBeforeInstallation,
|
||||
ml_module_setup_status: mlModuleSetupStatusBeforeInstallation,
|
||||
} = statusResponseBeforeInstallation.body;
|
||||
|
||||
expect(packageInstallationStatusBeforeInstallation).eql('incomplete');
|
||||
expect(mlModuleSetupStatusBeforeInstallation).eql('incomplete');
|
||||
|
||||
const installationResponse = await api.installPrivilegedAccessDetectionPackage('default');
|
||||
|
||||
expect(installationResponse.status).eql(200);
|
||||
expect(installationResponse.body.message).eql(
|
||||
'Successfully installed privileged access detection package.'
|
||||
);
|
||||
|
||||
const mlModuleSetupResponse = await simulateMlModuleSetupFromUI();
|
||||
expect(mlModuleSetupResponse.status).eql(200);
|
||||
|
||||
log.info('Privileged access detection installation was successful');
|
||||
|
||||
const statusResponseAfterInstallation =
|
||||
await api.getPrivilegedAccessDetectionPackageStatus();
|
||||
|
||||
if (statusResponseAfterInstallation.status !== 200) {
|
||||
log.error(`Retrieving status failed`);
|
||||
log.error(JSON.stringify(statusResponseAfterInstallation.body));
|
||||
}
|
||||
|
||||
expect(statusResponseAfterInstallation.status).eql(200);
|
||||
|
||||
const {
|
||||
package_installation_status: packageInstallationStatusAfterInstallation,
|
||||
ml_module_setup_status: mlModuleSetupStatusAfterInstallation,
|
||||
jobs: jobsAfterInstallation,
|
||||
} = statusResponseAfterInstallation.body;
|
||||
|
||||
expect(packageInstallationStatusAfterInstallation).eql('complete');
|
||||
expect(mlModuleSetupStatusAfterInstallation).eql('complete');
|
||||
expect(jobsAfterInstallation.length).greaterThan(20);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue