mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
Risk engine initialisation, update from legacy risk engine workflow and status change (#162400)
## Risk engine initialisation, update from legacy risk engine workflow and status changedfb75d4a
-f447-4346-9760-d0e9685cce39 Green areas it is what was implemented <img width="1449" alt="Screenshot 2023-08-01 at 15 07 01" src="4d87887f
-1163-45eb-a4e9-a77a685f6565"> This pr has: - Upgrade workflow. If the user has a risk host or user transforms, we will show the panel with a call to action for the upgrade. - Introduce new Saved object to save the configuration of risk engine - API which is described bellow It required experiment enabled - **riskScoringRoutesEnabled** ## New API ### /engine/status #### GET Get the status of the Risk Engine ##### Description: Returns the status of both the legacy transform-based risk engine, as well as the new risk engine ##### Responses ```json { "legacy_risk_engine_status": "NOT_INSTALLED" , "ENABLED" , "risk_engine_status": "NOT_INSTALLED" , "ENABLED" , "DISABLED" } ``` ### /engine/init #### POST Initialize the Risk Engine ##### Description: Initializes the Risk Engine by creating the necessary indices and mappings, removing old transforms, creating saved object configuration ##### Responses ```json { "result": { "risk_engine_enabled": true, "risk_engine_resources_installed": true, "risk_engine_configuration_created": true, "legacy_risk_engine_disabled": true, "errors": [ "string" ] } } ``` ### /engine/enable #### POST Enable the Risk Engine ##### Description: Change saved object configuration and in the future here we will start task ### /engine/disable #### POST Disable the Risk Engine Change saved object configuration and in the future here we will stop task --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Ryland Herrick <ryalnd@gmail.com>
This commit is contained in:
parent
0144696e73
commit
2bd52fc421
48 changed files with 2507 additions and 369 deletions
|
@ -42,6 +42,25 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"dynamic": false,
|
||||
"properties": {
|
||||
"slug": {
|
||||
"type": "text",
|
||||
"fields": {
|
||||
"keyword": {
|
||||
"type": "keyword"
|
||||
}
|
||||
}
|
||||
},
|
||||
"accessDate": {
|
||||
"type": "date"
|
||||
},
|
||||
"createDate": {
|
||||
"type": "date"
|
||||
}
|
||||
}
|
||||
},
|
||||
"usage-counters": {
|
||||
"dynamic": false,
|
||||
"properties": {
|
||||
|
@ -131,25 +150,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"dynamic": false,
|
||||
"properties": {
|
||||
"slug": {
|
||||
"type": "text",
|
||||
"fields": {
|
||||
"keyword": {
|
||||
"type": "keyword"
|
||||
}
|
||||
}
|
||||
},
|
||||
"accessDate": {
|
||||
"type": "date"
|
||||
},
|
||||
"createDate": {
|
||||
"type": "date"
|
||||
}
|
||||
}
|
||||
},
|
||||
"index-pattern": {
|
||||
"dynamic": false,
|
||||
"properties": {
|
||||
|
@ -1407,6 +1407,14 @@
|
|||
"dynamic": false,
|
||||
"properties": {}
|
||||
},
|
||||
"infrastructure-monitoring-log-view": {
|
||||
"dynamic": false,
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"canvas-element": {
|
||||
"dynamic": false,
|
||||
"properties": {
|
||||
|
@ -2262,14 +2270,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"infrastructure-monitoring-log-view": {
|
||||
"dynamic": false,
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ml-job": {
|
||||
"properties": {
|
||||
"job_id": {
|
||||
|
@ -2938,6 +2938,14 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"risk-engine-configuration": {
|
||||
"dynamic": false,
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"infrastructure-ui-source": {
|
||||
"dynamic": false,
|
||||
"properties": {}
|
||||
|
|
|
@ -8,7 +8,10 @@
|
|||
|
||||
import { chunk } from 'lodash';
|
||||
import type { ToolingLog } from '@kbn/tooling-log';
|
||||
import type { SavedObjectsBulkDeleteResponse } from '@kbn/core-saved-objects-api-server';
|
||||
import type {
|
||||
SavedObjectsBulkDeleteResponse,
|
||||
SavedObjectsFindResponse,
|
||||
} from '@kbn/core-saved-objects-api-server';
|
||||
|
||||
import { KbnClientRequester, uriencode } from './kbn_client_requester';
|
||||
|
||||
|
@ -30,6 +33,11 @@ interface SavedObjectResponse<Attributes extends Record<string, any>> {
|
|||
version?: string;
|
||||
}
|
||||
|
||||
interface FindOptions {
|
||||
type: string;
|
||||
space?: string;
|
||||
}
|
||||
|
||||
interface GetOptions {
|
||||
type: string;
|
||||
id: string;
|
||||
|
@ -152,6 +160,22 @@ export class KbnClientSavedObjects {
|
|||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find saved objects
|
||||
*/
|
||||
public async find<Attributes extends Record<string, any>>(options: FindOptions) {
|
||||
this.log.debug('Find saved objects: %j', options);
|
||||
|
||||
const { data } = await this.requester.request<SavedObjectsFindResponse<Attributes>>({
|
||||
description: 'find saved objects',
|
||||
path: options.space
|
||||
? uriencode`/s/${options.space}/internal/ftr/kbn_client_so/_find?type=${options.type}`
|
||||
: uriencode`/internal/ftr/kbn_client_so/_find?type=${options.type}`,
|
||||
method: 'GET',
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a saved object
|
||||
*/
|
||||
|
|
|
@ -126,6 +126,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
|
|||
"osquery-pack-asset": "b14101d3172c4b60eb5404696881ce5275c84152",
|
||||
"osquery-saved-query": "44f1161e165defe3f9b6ad643c68c542a765fcdb",
|
||||
"query": "8db5d48c62d75681d80d82a42b5642f60d068202",
|
||||
"risk-engine-configuration": "1b8b175e29ea5311408125c92c6247f502b2d79d",
|
||||
"rules-settings": "892a2918ebaeba809a612b8d97cec0b07c800b5f",
|
||||
"sample-data-telemetry": "37441b12f5b0159c2d6d5138a494c9f440e950b5",
|
||||
"search": "8d5184dd5b986d57250b6ffd9ae48a1925e4c7a3",
|
||||
|
|
|
@ -104,6 +104,7 @@ const previouslyRegisteredTypes = [
|
|||
'search-telemetry',
|
||||
'security-rule',
|
||||
'security-solution-signals-migration',
|
||||
'risk-engine-configuration',
|
||||
'server',
|
||||
'siem-detection-engine-rule-actions',
|
||||
'siem-detection-engine-rule-execution-info',
|
||||
|
|
|
@ -246,6 +246,7 @@ describe('split .kibana index into multiple system indices', () => {
|
|||
"osquery-pack-asset",
|
||||
"osquery-saved-query",
|
||||
"query",
|
||||
"risk-engine-configuration",
|
||||
"rules-settings",
|
||||
"sample-data-telemetry",
|
||||
"search-session",
|
||||
|
|
|
@ -253,6 +253,12 @@ export const RISK_SCORE_CREATE_STORED_SCRIPT = `${INTERNAL_RISK_SCORE_URL}/store
|
|||
export const RISK_SCORE_DELETE_STORED_SCRIPT = `${INTERNAL_RISK_SCORE_URL}/stored_scripts/delete`;
|
||||
export const RISK_SCORE_PREVIEW_URL = `${INTERNAL_RISK_SCORE_URL}/preview`;
|
||||
|
||||
export const RISK_ENGINE_URL = `${INTERNAL_RISK_SCORE_URL}/engine`;
|
||||
export const RISK_ENGINE_STATUS_URL = `${RISK_ENGINE_URL}/status`;
|
||||
export const RISK_ENGINE_INIT_URL = `${RISK_ENGINE_URL}/init`;
|
||||
export const RISK_ENGINE_ENABLE_URL = `${RISK_ENGINE_URL}/enable`;
|
||||
export const RISK_ENGINE_DISABLE_URL = `${RISK_ENGINE_URL}/disable`;
|
||||
|
||||
/**
|
||||
* Public Risk Score routes
|
||||
*/
|
||||
|
|
|
@ -9,3 +9,17 @@ export enum RiskScoreEntity {
|
|||
host = 'host',
|
||||
user = 'user',
|
||||
}
|
||||
|
||||
export enum RiskEngineStatus {
|
||||
NOT_INSTALLED = 'NOT_INSTALLED',
|
||||
DISABLED = 'DISABLED',
|
||||
ENABLED = 'ENABLED',
|
||||
}
|
||||
|
||||
export interface InitRiskEngineResult {
|
||||
legacyRiskEngineDisabled: boolean;
|
||||
riskEngineResourcesInstalled: boolean;
|
||||
riskEngineConfigurationCreated: boolean;
|
||||
riskEngineEnabled: boolean;
|
||||
errors: string[];
|
||||
}
|
||||
|
|
|
@ -12,83 +12,142 @@ import {
|
|||
USER_RISK_PREVIEW_TABLE,
|
||||
USER_RISK_PREVIEW_TABLE_ROWS,
|
||||
RISK_PREVIEW_ERROR,
|
||||
RISK_PREVIEW_ERROR_BUTTON,
|
||||
LOCAL_QUERY_BAR_SELECTOR,
|
||||
RISK_SCORE_ERROR_PANEL,
|
||||
RISK_SCORE_STATUS,
|
||||
} from '../../screens/entity_analytics_management';
|
||||
|
||||
import { deleteRiskScore, installRiskScoreModule } from '../../tasks/api_calls/risk_scores';
|
||||
import { RiskScoreEntity } from '../../tasks/risk_scores/common';
|
||||
import { login, visit, visitWithoutDateRange } from '../../tasks/login';
|
||||
import { cleanKibana } from '../../tasks/common';
|
||||
import { ENTITY_ANALYTICS_MANAGEMENT_URL, ALERTS_URL } from '../../urls/navigation';
|
||||
import { getNewRule } from '../../objects/rule';
|
||||
import { createRule } from '../../tasks/api_calls/rules';
|
||||
import {
|
||||
deleteConfiguration,
|
||||
interceptRiskPreviewError,
|
||||
interceptRiskPreviewSuccess,
|
||||
interceptRiskInitError,
|
||||
} from '../../tasks/api_calls/risk_engine';
|
||||
import { updateDateRangeInLocalDatePickers } from '../../tasks/date_picker';
|
||||
import { fillLocalSearchBar, submitLocalSearch } from '../../tasks/search_bar';
|
||||
import {
|
||||
riskEngineStatusChange,
|
||||
updateRiskEngine,
|
||||
updateRiskEngineConfirm,
|
||||
previewErrorButtonClick,
|
||||
} from '../../tasks/entity_analytics';
|
||||
|
||||
describe('Entity analytics management page', () => {
|
||||
before(() => {
|
||||
cleanKibana();
|
||||
cy.task('esArchiverLoad', 'all_users');
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
login();
|
||||
visitWithoutDateRange(ALERTS_URL);
|
||||
createRule(getNewRule({ query: 'user.name:* or host.name:*', risk_score: 70 }));
|
||||
visit(ENTITY_ANALYTICS_MANAGEMENT_URL);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
cy.task('esArchiverUnload', 'all_users');
|
||||
});
|
||||
|
||||
it('renders page as expected', () => {
|
||||
cy.get(PAGE_TITLE).should('have.text', 'Entity Risk Score');
|
||||
});
|
||||
|
||||
describe('Risk preview', () => {
|
||||
it('risk scores reacts on change in datepicker', () => {
|
||||
const START_DATE = 'Jan 18, 2019 @ 20:33:29.186';
|
||||
const END_DATE = 'Jan 19, 2019 @ 20:33:29.186';
|
||||
|
||||
cy.get(HOST_RISK_PREVIEW_TABLE_ROWS).should('have.length', 5);
|
||||
cy.get(USER_RISK_PREVIEW_TABLE_ROWS).should('have.length', 5);
|
||||
|
||||
updateDateRangeInLocalDatePickers(LOCAL_QUERY_BAR_SELECTOR, START_DATE, END_DATE);
|
||||
|
||||
cy.get(HOST_RISK_PREVIEW_TABLE).contains('No items found');
|
||||
cy.get(USER_RISK_PREVIEW_TABLE).contains('No items found');
|
||||
describe(
|
||||
'Entity analytics management page',
|
||||
{ env: { ftrConfig: { enableExperimental: ['riskScoringRoutesEnabled'] } } },
|
||||
() => {
|
||||
before(() => {
|
||||
cleanKibana();
|
||||
cy.task('esArchiverLoad', 'all_users');
|
||||
});
|
||||
|
||||
it('risk scores reacts on change in search bar query', () => {
|
||||
cy.get(HOST_RISK_PREVIEW_TABLE_ROWS).should('have.length', 5);
|
||||
cy.get(USER_RISK_PREVIEW_TABLE_ROWS).should('have.length', 5);
|
||||
|
||||
fillLocalSearchBar('host.name: "test-host1"');
|
||||
submitLocalSearch(LOCAL_QUERY_BAR_SELECTOR);
|
||||
|
||||
cy.get(HOST_RISK_PREVIEW_TABLE_ROWS).should('have.length', 1);
|
||||
cy.get(HOST_RISK_PREVIEW_TABLE_ROWS).contains('test-host1');
|
||||
cy.get(USER_RISK_PREVIEW_TABLE_ROWS).should('have.length', 1);
|
||||
cy.get(USER_RISK_PREVIEW_TABLE_ROWS).contains('test1');
|
||||
beforeEach(() => {
|
||||
login();
|
||||
visitWithoutDateRange(ALERTS_URL);
|
||||
createRule(getNewRule({ query: 'user.name:* or host.name:*', risk_score: 70 }));
|
||||
deleteConfiguration();
|
||||
visit(ENTITY_ANALYTICS_MANAGEMENT_URL);
|
||||
});
|
||||
|
||||
it('show error panel if API returns error and then try to refetch data', () => {
|
||||
cy.intercept('POST', '/internal/risk_score/preview', {
|
||||
statusCode: 500,
|
||||
after(() => {
|
||||
cy.task('esArchiverUnload', 'all_users');
|
||||
});
|
||||
|
||||
it('renders page as expected', () => {
|
||||
cy.get(PAGE_TITLE).should('have.text', 'Entity Risk Score');
|
||||
});
|
||||
|
||||
describe('Risk preview', () => {
|
||||
it('risk scores reacts on change in datepicker', () => {
|
||||
const START_DATE = 'Jan 18, 2019 @ 20:33:29.186';
|
||||
const END_DATE = 'Jan 19, 2019 @ 20:33:29.186';
|
||||
|
||||
cy.get(HOST_RISK_PREVIEW_TABLE_ROWS).should('have.length', 5);
|
||||
cy.get(USER_RISK_PREVIEW_TABLE_ROWS).should('have.length', 5);
|
||||
|
||||
updateDateRangeInLocalDatePickers(LOCAL_QUERY_BAR_SELECTOR, START_DATE, END_DATE);
|
||||
|
||||
cy.get(HOST_RISK_PREVIEW_TABLE).contains('No items found');
|
||||
cy.get(USER_RISK_PREVIEW_TABLE).contains('No items found');
|
||||
});
|
||||
|
||||
cy.get(RISK_PREVIEW_ERROR).contains('Preview failed');
|
||||
it('risk scores reacts on change in search bar query', () => {
|
||||
cy.get(HOST_RISK_PREVIEW_TABLE_ROWS).should('have.length', 5);
|
||||
cy.get(USER_RISK_PREVIEW_TABLE_ROWS).should('have.length', 5);
|
||||
|
||||
cy.intercept('POST', '/internal/risk_score/preview', {
|
||||
statusCode: 200,
|
||||
body: {
|
||||
scores: { host: [], user: [] },
|
||||
},
|
||||
fillLocalSearchBar('host.name: "test-host1"');
|
||||
submitLocalSearch(LOCAL_QUERY_BAR_SELECTOR);
|
||||
|
||||
cy.get(HOST_RISK_PREVIEW_TABLE_ROWS).should('have.length', 1);
|
||||
cy.get(HOST_RISK_PREVIEW_TABLE_ROWS).contains('test-host1');
|
||||
cy.get(USER_RISK_PREVIEW_TABLE_ROWS).should('have.length', 1);
|
||||
cy.get(USER_RISK_PREVIEW_TABLE_ROWS).contains('test1');
|
||||
});
|
||||
|
||||
cy.get(RISK_PREVIEW_ERROR_BUTTON).click();
|
||||
it('show error panel if API returns error and then try to refetch data', () => {
|
||||
interceptRiskPreviewError();
|
||||
|
||||
cy.get(RISK_PREVIEW_ERROR).should('not.exist');
|
||||
cy.get(RISK_PREVIEW_ERROR).contains('Preview failed');
|
||||
|
||||
interceptRiskPreviewSuccess();
|
||||
|
||||
previewErrorButtonClick();
|
||||
|
||||
cy.get(RISK_PREVIEW_ERROR).should('not.exist');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Risk engine', () => {
|
||||
it('should init, disable and enable risk engine', () => {
|
||||
cy.get(RISK_SCORE_STATUS).should('have.text', 'Off');
|
||||
|
||||
// init
|
||||
riskEngineStatusChange();
|
||||
|
||||
cy.get(RISK_SCORE_STATUS).should('have.text', 'On');
|
||||
|
||||
// disable
|
||||
riskEngineStatusChange();
|
||||
|
||||
cy.get(RISK_SCORE_STATUS).should('have.text', 'Off');
|
||||
|
||||
// enable
|
||||
riskEngineStatusChange();
|
||||
|
||||
cy.get(RISK_SCORE_STATUS).should('have.text', 'On');
|
||||
});
|
||||
|
||||
it('should show error panel if API returns error ', () => {
|
||||
cy.get(RISK_SCORE_STATUS).should('have.text', 'Off');
|
||||
|
||||
interceptRiskInitError();
|
||||
|
||||
// init
|
||||
riskEngineStatusChange();
|
||||
|
||||
cy.get(RISK_SCORE_ERROR_PANEL).contains('Sorry, there was an error');
|
||||
});
|
||||
|
||||
it('should update if there legacy risk score installed', () => {
|
||||
installRiskScoreModule();
|
||||
visit(ENTITY_ANALYTICS_MANAGEMENT_URL);
|
||||
|
||||
cy.get(RISK_SCORE_STATUS).should('not.exist');
|
||||
|
||||
updateRiskEngine();
|
||||
updateRiskEngineConfirm();
|
||||
|
||||
cy.get(RISK_SCORE_STATUS).should('have.text', 'On');
|
||||
|
||||
deleteRiskScore({ riskScoreEntity: RiskScoreEntity.host, spaceId: 'default' });
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
|
|
@ -22,3 +22,15 @@ export const RISK_PREVIEW_ERROR = '[data-test-subj="risk-preview-error"]';
|
|||
export const RISK_PREVIEW_ERROR_BUTTON = '[data-test-subj="risk-preview-error-button"]';
|
||||
|
||||
export const LOCAL_QUERY_BAR_SELECTOR = getDataTestSubjectSelector('risk-score-preview-search-bar');
|
||||
|
||||
export const RISK_SCORE_ERROR_PANEL = '[data-test-subj="risk-score-error-panel"]';
|
||||
|
||||
export const RISK_SCORE_UPDATE_CANCEL = '[data-test-subj="risk-score-update-cancel"]';
|
||||
|
||||
export const RISK_SCORE_UPDATE_CONFIRM = '[data-test-subj="risk-score-update-confirm"]';
|
||||
|
||||
export const RISK_SCORE_UDATE_BUTTON = '[data-test-subj="risk-score-update-button"]';
|
||||
|
||||
export const RISK_SCORE_STATUS = '[data-test-subj="risk-score-status"]';
|
||||
|
||||
export const RISK_SCORE_SWITCH = '[data-test-subj="risk-score-switch"]';
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* 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 deleteConfiguration = () => {
|
||||
cy.request({
|
||||
method: 'GET',
|
||||
url: `/api/saved_objects/_find?type=risk-engine-configuration`,
|
||||
failOnStatusCode: false,
|
||||
headers: { 'kbn-xsrf': 'cypress-creds-via-config' },
|
||||
}).then((res) => {
|
||||
const savedObjectId = res?.body?.saved_objects?.[0]?.id;
|
||||
if (savedObjectId) {
|
||||
return cy.request({
|
||||
method: 'DELETE',
|
||||
url: `/api/saved_objects/risk-engine-configuration/${savedObjectId}`,
|
||||
failOnStatusCode: false,
|
||||
headers: { 'kbn-xsrf': 'cypress-creds-via-config' },
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const interceptRiskPreviewError = () => {
|
||||
cy.intercept('POST', '/internal/risk_score/preview', {
|
||||
statusCode: 500,
|
||||
});
|
||||
};
|
||||
|
||||
export const interceptRiskPreviewSuccess = () => {
|
||||
cy.intercept('POST', '/internal/risk_score/preview', {
|
||||
statusCode: 200,
|
||||
body: {
|
||||
scores: { host: [], user: [] },
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const interceptRiskInitError = () => {
|
||||
cy.intercept('POST', '/internal/risk_score/engine/init', {
|
||||
statusCode: 500,
|
||||
});
|
||||
};
|
|
@ -290,3 +290,16 @@ export const interceptInstallRiskScoreModule = () => {
|
|||
export const waitForInstallRiskScoreModule = () => {
|
||||
cy.wait(['@install'], { requestTimeout: 50000 });
|
||||
};
|
||||
|
||||
export const installRiskScoreModule = () => {
|
||||
cy.request({
|
||||
url: RISK_SCORE_URL,
|
||||
method: 'POST',
|
||||
body: {
|
||||
riskScoreEntity: 'host',
|
||||
},
|
||||
headers: { 'kbn-xsrf': 'cypress-creds' },
|
||||
})
|
||||
.its('status')
|
||||
.should('eql', 200);
|
||||
};
|
||||
|
|
|
@ -12,6 +12,12 @@ import {
|
|||
ANOMALIES_TABLE_NEXT_PAGE_BUTTON,
|
||||
} from '../screens/entity_analytics';
|
||||
import { ENTITY_ANALYTICS_URL } from '../urls/navigation';
|
||||
import {
|
||||
RISK_SCORE_UPDATE_CONFIRM,
|
||||
RISK_SCORE_UDATE_BUTTON,
|
||||
RISK_SCORE_SWITCH,
|
||||
RISK_PREVIEW_ERROR_BUTTON,
|
||||
} from '../screens/entity_analytics_management';
|
||||
|
||||
import { visit } from './login';
|
||||
|
||||
|
@ -31,3 +37,20 @@ export const enableJob = () => {
|
|||
export const navigateToNextPage = () => {
|
||||
cy.get(ANOMALIES_TABLE_NEXT_PAGE_BUTTON).click();
|
||||
};
|
||||
|
||||
export const riskEngineStatusChange = () => {
|
||||
cy.get(RISK_SCORE_SWITCH).should('not.have.attr', 'disabled');
|
||||
cy.get(RISK_SCORE_SWITCH).click();
|
||||
};
|
||||
|
||||
export const updateRiskEngine = () => {
|
||||
cy.get(RISK_SCORE_UDATE_BUTTON).click();
|
||||
};
|
||||
|
||||
export const updateRiskEngineConfirm = () => {
|
||||
cy.get(RISK_SCORE_UPDATE_CONFIRM).click();
|
||||
};
|
||||
|
||||
export const previewErrorButtonClick = () => {
|
||||
cy.get(RISK_PREVIEW_ERROR_BUTTON).click();
|
||||
};
|
||||
|
|
|
@ -5,10 +5,22 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { RISK_SCORE_PREVIEW_URL } from '../../../common/constants';
|
||||
import {
|
||||
RISK_ENGINE_STATUS_URL,
|
||||
RISK_SCORE_PREVIEW_URL,
|
||||
RISK_ENGINE_ENABLE_URL,
|
||||
RISK_ENGINE_DISABLE_URL,
|
||||
RISK_ENGINE_INIT_URL,
|
||||
} from '../../../common/constants';
|
||||
|
||||
import { KibanaServices } from '../../common/lib/kibana';
|
||||
import type { CalculateScoresResponse } from '../../../server/lib/risk_engine/types';
|
||||
import type {
|
||||
CalculateScoresResponse,
|
||||
EnableRiskEngineResponse,
|
||||
GetRiskEngineStatusResponse,
|
||||
InitRiskEngineResponse,
|
||||
DisableRiskEngineResponse,
|
||||
} from '../../../server/lib/risk_engine/types';
|
||||
import type { RiskScorePreviewRequestSchema } from '../../../common/risk_engine/risk_score_preview/request_schema';
|
||||
|
||||
/**
|
||||
|
@ -27,3 +39,44 @@ export const fetchRiskScorePreview = async ({
|
|||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches risks engine status
|
||||
*/
|
||||
export const fetchRiskEngineStatus = async ({
|
||||
signal,
|
||||
}: {
|
||||
signal?: AbortSignal;
|
||||
}): Promise<GetRiskEngineStatusResponse> => {
|
||||
return KibanaServices.get().http.fetch<GetRiskEngineStatusResponse>(RISK_ENGINE_STATUS_URL, {
|
||||
method: 'GET',
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Init risk score engine
|
||||
*/
|
||||
export const initRiskEngine = async (): Promise<InitRiskEngineResponse> => {
|
||||
return KibanaServices.get().http.fetch<InitRiskEngineResponse>(RISK_ENGINE_INIT_URL, {
|
||||
method: 'POST',
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Enable risk score engine
|
||||
*/
|
||||
export const enableRiskEngine = async (): Promise<EnableRiskEngineResponse> => {
|
||||
return KibanaServices.get().http.fetch<EnableRiskEngineResponse>(RISK_ENGINE_ENABLE_URL, {
|
||||
method: 'POST',
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Disable risk score engine
|
||||
*/
|
||||
export const disableRiskEngine = async (): Promise<DisableRiskEngineResponse> => {
|
||||
return KibanaServices.get().http.fetch<DisableRiskEngineResponse>(RISK_ENGINE_DISABLE_URL, {
|
||||
method: 'POST',
|
||||
});
|
||||
};
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* 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 { UseMutationOptions } from '@tanstack/react-query';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { disableRiskEngine } from '../api';
|
||||
import { useInvalidateRiskEngineStatusQuery } from './use_risk_engine_status';
|
||||
import type {
|
||||
EnableRiskEngineResponse,
|
||||
EnableDisableRiskEngineErrorResponse,
|
||||
} from '../../../../server/lib/risk_engine/types';
|
||||
|
||||
export const DISABLE_RISK_ENGINE_MUTATION_KEY = ['POST', 'DISABLE_RISK_ENGINE'];
|
||||
|
||||
export const useDisableRiskEngineMutation = (options?: UseMutationOptions<{}>) => {
|
||||
const invalidateRiskEngineStatusQuery = useInvalidateRiskEngineStatusQuery();
|
||||
|
||||
return useMutation<EnableRiskEngineResponse, EnableDisableRiskEngineErrorResponse>(
|
||||
() => disableRiskEngine(),
|
||||
{
|
||||
...options,
|
||||
mutationKey: DISABLE_RISK_ENGINE_MUTATION_KEY,
|
||||
onSettled: (...args) => {
|
||||
invalidateRiskEngineStatusQuery();
|
||||
|
||||
if (options?.onSettled) {
|
||||
options.onSettled(...args);
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* 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 { UseMutationOptions } from '@tanstack/react-query';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { enableRiskEngine } from '../api';
|
||||
import { useInvalidateRiskEngineStatusQuery } from './use_risk_engine_status';
|
||||
import type {
|
||||
EnableRiskEngineResponse,
|
||||
EnableDisableRiskEngineErrorResponse,
|
||||
} from '../../../../server/lib/risk_engine/types';
|
||||
export const ENABLE_RISK_ENGINE_MUTATION_KEY = ['POST', 'ENABLE_RISK_ENGINE'];
|
||||
|
||||
export const useEnableRiskEngineMutation = (options?: UseMutationOptions<{}>) => {
|
||||
const invalidateRiskEngineStatusQuery = useInvalidateRiskEngineStatusQuery();
|
||||
|
||||
return useMutation<EnableRiskEngineResponse, EnableDisableRiskEngineErrorResponse>(
|
||||
() => enableRiskEngine(),
|
||||
{
|
||||
...options,
|
||||
mutationKey: ENABLE_RISK_ENGINE_MUTATION_KEY,
|
||||
onSettled: (...args) => {
|
||||
invalidateRiskEngineStatusQuery();
|
||||
|
||||
if (options?.onSettled) {
|
||||
options.onSettled(...args);
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
|
@ -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 type { UseMutationOptions } from '@tanstack/react-query';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { initRiskEngine } from '../api';
|
||||
import { useInvalidateRiskEngineStatusQuery } from './use_risk_engine_status';
|
||||
import type {
|
||||
InitRiskEngineResponse,
|
||||
InitRiskEngineError,
|
||||
} from '../../../../server/lib/risk_engine/types';
|
||||
|
||||
export const INIT_RISK_ENGINE_STATUS_KEY = ['POST', 'INIT_RISK_ENGINE'];
|
||||
|
||||
export const useInitRiskEngineMutation = (options?: UseMutationOptions<{}>) => {
|
||||
const invalidateRiskEngineStatusQuery = useInvalidateRiskEngineStatusQuery();
|
||||
|
||||
return useMutation<InitRiskEngineResponse, InitRiskEngineError>(() => initRiskEngine(), {
|
||||
...options,
|
||||
mutationKey: INIT_RISK_ENGINE_STATUS_KEY,
|
||||
onSettled: (...args) => {
|
||||
invalidateRiskEngineStatusQuery();
|
||||
|
||||
if (options?.onSettled) {
|
||||
options.onSettled(...args);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* 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, useQueryClient } from '@tanstack/react-query';
|
||||
import { useCallback } from 'react';
|
||||
import { fetchRiskEngineStatus } from '../api';
|
||||
import { RiskEngineStatus } from '../../../../common/risk_engine/types';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features';
|
||||
const FETCH_RISK_ENGINE_STATUS = ['GET', 'FETCH_RISK_ENGINE_STATUS'];
|
||||
|
||||
export const useInvalidateRiskEngineStatusQuery = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useCallback(() => {
|
||||
queryClient.invalidateQueries(FETCH_RISK_ENGINE_STATUS, {
|
||||
refetchType: 'active',
|
||||
});
|
||||
}, [queryClient]);
|
||||
};
|
||||
|
||||
export const useRiskEngineStatus = () => {
|
||||
const isRiskEngineEnabled = useIsExperimentalFeatureEnabled('riskScoringRoutesEnabled');
|
||||
|
||||
return useQuery(FETCH_RISK_ENGINE_STATUS, async ({ signal }) => {
|
||||
if (!isRiskEngineEnabled) {
|
||||
return null;
|
||||
}
|
||||
const response = await fetchRiskEngineStatus({ signal });
|
||||
const isUpdateAvailable =
|
||||
response?.legacy_risk_engine_status === RiskEngineStatus.ENABLED &&
|
||||
response.risk_engine_status === RiskEngineStatus.NOT_INSTALLED;
|
||||
return {
|
||||
isUpdateAvailable,
|
||||
...response,
|
||||
};
|
||||
});
|
||||
};
|
|
@ -15,6 +15,18 @@ import {
|
|||
EuiSpacer,
|
||||
EuiSwitch,
|
||||
EuiTitle,
|
||||
EuiLoadingSpinner,
|
||||
EuiBadge,
|
||||
EuiButtonEmpty,
|
||||
EuiButton,
|
||||
EuiModal,
|
||||
EuiModalBody,
|
||||
EuiModalFooter,
|
||||
EuiModalHeader,
|
||||
EuiModalHeaderTitle,
|
||||
EuiText,
|
||||
EuiCallOut,
|
||||
EuiAccordion,
|
||||
} from '@elastic/eui';
|
||||
import {
|
||||
DETECTION_ENTITY_DASHBOARD,
|
||||
|
@ -22,6 +34,11 @@ import {
|
|||
RISKY_USERS_DOC_LINK,
|
||||
} from '../../../common/constants';
|
||||
import * as i18n from '../translations';
|
||||
import { useRiskEngineStatus } from '../api/hooks/use_risk_engine_status';
|
||||
import { useInitRiskEngineMutation } from '../api/hooks/use_init_risk_engine_mutation';
|
||||
import { useEnableRiskEngineMutation } from '../api/hooks/use_enable_risk_engine_mutation';
|
||||
import { useDisableRiskEngineMutation } from '../api/hooks/use_disable_risk_engine_mutation';
|
||||
import { RiskEngineStatus } from '../../../common/risk_engine/types';
|
||||
|
||||
const docsLinks = [
|
||||
{
|
||||
|
@ -40,40 +57,205 @@ const docsLinks = [
|
|||
|
||||
const MIN_WIDTH_TO_PREVENT_LABEL_FROM_MOVING = '50px';
|
||||
|
||||
export const RiskScoreEnableSection = () => {
|
||||
const [checked, setChecked] = useState(false);
|
||||
const RiskScoreErrorPanel = ({ errors }: { errors: string[] }) => (
|
||||
<>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiCallOut
|
||||
title={i18n.ERROR_PANEL_TITLE}
|
||||
color="danger"
|
||||
iconType="error"
|
||||
data-test-subj="risk-score-error-panel"
|
||||
>
|
||||
<p>{i18n.ERROR_PANEL_MESSAGE}</p>
|
||||
|
||||
<EuiAccordion id={'risk-engine-erros'} buttonContent={i18n.ERROR_PANEL_ERRORS}>
|
||||
<>
|
||||
{errors.map((error) => (
|
||||
<div key={error}>
|
||||
<EuiText size="s">{error}</EuiText>
|
||||
<EuiSpacer size="s" />
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
</EuiAccordion>
|
||||
</EuiCallOut>
|
||||
</>
|
||||
);
|
||||
|
||||
export const RiskScoreEnableSection = () => {
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
const { data: riskEngineStatus } = useRiskEngineStatus();
|
||||
const initRiskEngineMutation = useInitRiskEngineMutation({
|
||||
onSettled: () => {
|
||||
setIsModalVisible(false);
|
||||
},
|
||||
});
|
||||
|
||||
const enableRiskEngineMutation = useEnableRiskEngineMutation();
|
||||
const disableRiskEngineMutation = useDisableRiskEngineMutation();
|
||||
|
||||
const currentRiskEngineStatus = riskEngineStatus?.risk_engine_status;
|
||||
|
||||
const closeModal = () => setIsModalVisible(false);
|
||||
const showModal = () => setIsModalVisible(true);
|
||||
|
||||
const isLoading =
|
||||
initRiskEngineMutation.isLoading ||
|
||||
enableRiskEngineMutation.isLoading ||
|
||||
disableRiskEngineMutation.isLoading;
|
||||
|
||||
const isUpdateAvailable = riskEngineStatus?.isUpdateAvailable;
|
||||
const btnIsDisabled = !currentRiskEngineStatus || isLoading;
|
||||
|
||||
const onSwitchClick = () => {
|
||||
if (btnIsDisabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentRiskEngineStatus === RiskEngineStatus.NOT_INSTALLED) {
|
||||
initRiskEngineMutation.mutate();
|
||||
} else if (currentRiskEngineStatus === RiskEngineStatus.ENABLED) {
|
||||
disableRiskEngineMutation.mutate();
|
||||
} else if (currentRiskEngineStatus === RiskEngineStatus.DISABLED) {
|
||||
enableRiskEngineMutation.mutate();
|
||||
}
|
||||
};
|
||||
|
||||
let modal;
|
||||
|
||||
if (isModalVisible) {
|
||||
modal = (
|
||||
<EuiModal onClose={closeModal}>
|
||||
{initRiskEngineMutation.isLoading ? (
|
||||
<EuiModalHeader>
|
||||
<EuiFlexGroup gutterSize="m" alignItems="center">
|
||||
<EuiLoadingSpinner size="m" />
|
||||
<EuiModalHeaderTitle>{i18n.UPDATING_RISK_ENGINE}</EuiModalHeaderTitle>
|
||||
</EuiFlexGroup>
|
||||
</EuiModalHeader>
|
||||
) : (
|
||||
<>
|
||||
<EuiModalHeader>
|
||||
<EuiModalHeaderTitle>{i18n.UPDATE_RISK_ENGINE_MODAL_TITLE}</EuiModalHeaderTitle>
|
||||
</EuiModalHeader>
|
||||
|
||||
<EuiModalBody>
|
||||
<EuiText>
|
||||
<p>
|
||||
<b>{i18n.UPDATE_RISK_ENGINE_MODAL_EXISTING_USER_HOST_1}</b>
|
||||
{i18n.UPDATE_RISK_ENGINE_MODAL_EXISTING_USER_HOST_2}
|
||||
</p>
|
||||
<EuiSpacer size="s" />
|
||||
<p>
|
||||
<b>{i18n.UPDATE_RISK_ENGINE_MODAL_EXISTING_DATA_1}</b>
|
||||
{i18n.UPDATE_RISK_ENGINE_MODAL_EXISTING_DATA_2}
|
||||
</p>
|
||||
</EuiText>
|
||||
<EuiSpacer />
|
||||
</EuiModalBody>
|
||||
|
||||
<EuiModalFooter>
|
||||
<EuiButtonEmpty
|
||||
color="primary"
|
||||
data-test-subj="risk-score-update-cancel"
|
||||
onClick={closeModal}
|
||||
>
|
||||
{i18n.UPDATE_RISK_ENGINE_MODAL_BUTTON_NO}
|
||||
</EuiButtonEmpty>
|
||||
<EuiButton
|
||||
color="primary"
|
||||
data-test-subj="risk-score-update-confirm"
|
||||
onClick={() => initRiskEngineMutation.mutate()}
|
||||
fill
|
||||
>
|
||||
{i18n.UPDATE_RISK_ENGINE_MODAL_BUTTON_YES}
|
||||
</EuiButton>
|
||||
</EuiModalFooter>
|
||||
</>
|
||||
)}
|
||||
</EuiModal>
|
||||
);
|
||||
}
|
||||
|
||||
let initRiskEngineErrors: string[] = [];
|
||||
|
||||
if (initRiskEngineMutation.isError) {
|
||||
const errorBody = initRiskEngineMutation.error.body.message;
|
||||
if (errorBody?.full_error?.errors) {
|
||||
initRiskEngineErrors = errorBody.full_error?.errors;
|
||||
} else {
|
||||
initRiskEngineErrors = [errorBody];
|
||||
}
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<>
|
||||
<EuiTitle>
|
||||
<h2>{i18n.RISK_SCORE_MODULE_STATUS}</h2>
|
||||
</EuiTitle>
|
||||
{initRiskEngineMutation.isError && <RiskScoreErrorPanel errors={initRiskEngineErrors} />}
|
||||
{disableRiskEngineMutation.isError && (
|
||||
<RiskScoreErrorPanel errors={[disableRiskEngineMutation.error.body.message.message]} />
|
||||
)}
|
||||
{enableRiskEngineMutation.isError && (
|
||||
<RiskScoreErrorPanel errors={[enableRiskEngineMutation.error.body.message.message]} />
|
||||
)}
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexItem grow={0}>
|
||||
{modal}
|
||||
<EuiHorizontalRule margin="s" />
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>{i18n.ENTITY_RISK_SCORING}</EuiFlexItem>
|
||||
|
||||
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup gutterSize="s" alignItems={'center'}>
|
||||
<EuiFlexItem css={{ minWidth: MIN_WIDTH_TO_PREVENT_LABEL_FROM_MOVING }}>
|
||||
{checked ? (
|
||||
<EuiHealth color="success">{i18n.RISK_SCORE_MODULE_STATUS_ON}</EuiHealth>
|
||||
) : (
|
||||
<EuiHealth color="subdued">{i18n.RISK_SCORE_MODULE_STATUS_OFF}</EuiHealth>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiSwitch
|
||||
label={''}
|
||||
checked={checked}
|
||||
onChange={(e) => setChecked(e.target.checked)}
|
||||
compressed
|
||||
aria-describedby={'switchRiskModule'}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexGroup gutterSize="s" alignItems={'baseline'}>
|
||||
{i18n.ENTITY_RISK_SCORING}
|
||||
{isUpdateAvailable && <EuiBadge color="success">{i18n.UPDATE_AVAILABLE}</EuiBadge>}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
{isUpdateAvailable && (
|
||||
<EuiFlexGroup gutterSize="s" alignItems={'center'}>
|
||||
<EuiFlexItem>
|
||||
{initRiskEngineMutation.isLoading && <EuiLoadingSpinner size="m" />}
|
||||
</EuiFlexItem>
|
||||
<EuiButtonEmpty
|
||||
disabled={initRiskEngineMutation.isLoading}
|
||||
color={'primary'}
|
||||
onClick={showModal}
|
||||
data-test-subj="risk-score-update-button"
|
||||
>
|
||||
{i18n.START_UPDATE}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexGroup>
|
||||
)}
|
||||
{!isUpdateAvailable && (
|
||||
<EuiFlexGroup gutterSize="s" alignItems={'center'}>
|
||||
<EuiFlexItem>{isLoading && <EuiLoadingSpinner size="m" />}</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
css={{ minWidth: MIN_WIDTH_TO_PREVENT_LABEL_FROM_MOVING }}
|
||||
data-test-subj="risk-score-status"
|
||||
>
|
||||
{currentRiskEngineStatus === RiskEngineStatus.ENABLED ? (
|
||||
<EuiHealth color="success">{i18n.RISK_SCORE_MODULE_STATUS_ON}</EuiHealth>
|
||||
) : (
|
||||
<EuiHealth color="subdued">{i18n.RISK_SCORE_MODULE_STATUS_OFF}</EuiHealth>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiSwitch
|
||||
label={''}
|
||||
data-test-subj="risk-score-switch"
|
||||
checked={currentRiskEngineStatus === RiskEngineStatus.ENABLED}
|
||||
onChange={onSwitchClick}
|
||||
compressed
|
||||
disabled={btnIsDisabled}
|
||||
aria-describedby={'switchRiskModule'}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiHorizontalRule margin="s" />
|
||||
</EuiFlexItem>
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiCallOut, EuiText, EuiFlexGroup, EuiSpacer } from '@elastic/eui';
|
||||
import * as i18n from '../translations';
|
||||
import { SecuritySolutionLinkButton } from '../../common/components/links';
|
||||
import { SecurityPageName } from '../../../common/constants';
|
||||
|
||||
export const RiskScoreUpdatePanel = () => {
|
||||
return (
|
||||
<EuiCallOut title={i18n.UPDATE_PANEL_TITLE} color="primary" iconType="starEmpty">
|
||||
<EuiText>{i18n.UPDATE_PANEL_MESSAGE}</EuiText>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexGroup gutterSize="m">
|
||||
<SecuritySolutionLinkButton
|
||||
color="primary"
|
||||
fill
|
||||
deepLinkId={SecurityPageName.entityAnalyticsManagement}
|
||||
>
|
||||
{i18n.UPDATE_PANEL_GO_TO_MANAGE}
|
||||
</SecuritySolutionLinkButton>
|
||||
</EuiFlexGroup>
|
||||
</EuiCallOut>
|
||||
);
|
||||
};
|
|
@ -130,3 +130,117 @@ export const PREVIEW_QUERY_ERROR_TITLE = i18n.translate(
|
|||
defaultMessage: 'Invalid query',
|
||||
}
|
||||
);
|
||||
|
||||
export const UPDATE_AVAILABLE = i18n.translate('xpack.securitySolution.riskScore.updateAvailable', {
|
||||
defaultMessage: 'Update available',
|
||||
});
|
||||
|
||||
export const START_UPDATE = i18n.translate('xpack.securitySolution.riskScore.startUpdate', {
|
||||
defaultMessage: 'Start update',
|
||||
});
|
||||
|
||||
export const UPDATING_RISK_ENGINE = i18n.translate(
|
||||
'xpack.securitySolution.riskScore.updatingRiskEngine',
|
||||
{
|
||||
defaultMessage: 'Updating risk engine...',
|
||||
}
|
||||
);
|
||||
|
||||
export const UPDATE_RISK_ENGINE_MODAL_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.riskScore.updateRiskEngineModa.title',
|
||||
{
|
||||
defaultMessage: 'Do you want to update the entity risk engine?',
|
||||
}
|
||||
);
|
||||
|
||||
export const UPDATE_RISK_ENGINE_MODAL_EXISTING_USER_HOST_1 = i18n.translate(
|
||||
'xpack.securitySolution.riskScore.updateRiskEngineModal.existingUserHost_1',
|
||||
{
|
||||
defaultMessage: 'Existing user and host risk score transforms will be deleted',
|
||||
}
|
||||
);
|
||||
|
||||
export const UPDATE_RISK_ENGINE_MODAL_EXISTING_USER_HOST_2 = i18n.translate(
|
||||
'xpack.securitySolution.riskScore.updateRiskEngineModal.existingUserHost_2',
|
||||
{
|
||||
defaultMessage: ', as they are no longer required.',
|
||||
}
|
||||
);
|
||||
|
||||
export const UPDATE_RISK_ENGINE_MODAL_EXISTING_DATA_1 = i18n.translate(
|
||||
'xpack.securitySolution.riskScore.updateRiskEngineModal.existingData_1',
|
||||
{
|
||||
defaultMessage: 'None of your risk score data will be deleted',
|
||||
}
|
||||
);
|
||||
|
||||
export const UPDATE_RISK_ENGINE_MODAL_EXISTING_DATA_2 = i18n.translate(
|
||||
'xpack.securitySolution.riskScore.updateRiskEngineModal.existingData_2',
|
||||
{
|
||||
defaultMessage: ', you will need to remove any old risk score data manually.',
|
||||
}
|
||||
);
|
||||
|
||||
export const UPDATE_RISK_ENGINE_MODAL_BUTTON_NO = i18n.translate(
|
||||
'xpack.securitySolution.riskScore.updateRiskEngineModal.buttonNo',
|
||||
{
|
||||
defaultMessage: 'No, not yet',
|
||||
}
|
||||
);
|
||||
|
||||
export const UPDATE_RISK_ENGINE_MODAL_BUTTON_YES = i18n.translate(
|
||||
'xpack.securitySolution.riskScore.updateRiskEngineModal.buttonYes',
|
||||
{
|
||||
defaultMessage: 'Yes, update now!',
|
||||
}
|
||||
);
|
||||
|
||||
export const ERROR_PANEL_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.riskScore.errorPanel.title',
|
||||
{
|
||||
defaultMessage: 'Sorry, there was an error',
|
||||
}
|
||||
);
|
||||
|
||||
export const ERROR_PANEL_MESSAGE = i18n.translate(
|
||||
'xpack.securitySolution.riskScore.errorPanel.message',
|
||||
{
|
||||
defaultMessage: 'Something went wrong. Try again later.',
|
||||
}
|
||||
);
|
||||
|
||||
export const ERROR_PANEL_ERRORS = i18n.translate(
|
||||
'xpack.securitySolution.riskScore.errorPanel.errors',
|
||||
{
|
||||
defaultMessage: 'Errors',
|
||||
}
|
||||
);
|
||||
|
||||
export const UPDATE_PANEL_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.riskScore.updatePanel.title',
|
||||
{
|
||||
defaultMessage: 'New entity risk scoring engine available',
|
||||
}
|
||||
);
|
||||
|
||||
export const UPDATE_PANEL_MESSAGE = i18n.translate(
|
||||
'xpack.securitySolution.riskScore.updatePanel.message',
|
||||
{
|
||||
defaultMessage:
|
||||
'A new entity risk scoring engine is available. Update now to get the latest features.',
|
||||
}
|
||||
);
|
||||
|
||||
export const UPDATE_PANEL_GO_TO_MANAGE = i18n.translate(
|
||||
'xpack.securitySolution.riskScore.updatePanel.goToManage',
|
||||
{
|
||||
defaultMessage: 'Manage',
|
||||
}
|
||||
);
|
||||
|
||||
export const UPDATE_PANEL_GO_TO_DISMISS = i18n.translate(
|
||||
'xpack.securitySolution.riskScore.updatePanel.Dismiss',
|
||||
{
|
||||
defaultMessage: 'Dismiss',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -32,6 +32,8 @@ import type { UsersComponentsQueryProps } from '../../../users/pages/navigation/
|
|||
import type { HostsComponentsQueryProps } from '../../../hosts/pages/navigation/types';
|
||||
import { useDashboardHref } from '../../../../common/hooks/use_dashboard_href';
|
||||
import { RiskScoresNoDataDetected } from '../risk_score_onboarding/risk_score_no_data_detected';
|
||||
import { useRiskEngineStatus } from '../../../../entity_analytics/api/hooks/use_risk_engine_status';
|
||||
import { RiskScoreUpdatePanel } from '../../../../entity_analytics/components/risk_score_update_panel';
|
||||
|
||||
const StyledEuiFlexGroup = styled(EuiFlexGroup)`
|
||||
margin-top: ${({ theme }) => theme.eui.euiSizeL};
|
||||
|
@ -91,6 +93,8 @@ const RiskDetailsTabBodyComponent: React.FC<
|
|||
timerange,
|
||||
});
|
||||
|
||||
const { data: riskScoreEngineStatus } = useRiskEngineStatus();
|
||||
|
||||
const rules = useMemo(() => {
|
||||
const lastRiskItem = data && data.length > 0 ? data[data.length - 1] : null;
|
||||
if (lastRiskItem) {
|
||||
|
@ -133,6 +137,10 @@ const RiskDetailsTabBodyComponent: React.FC<
|
|||
return <>{'TODO: Add RiskScore Upsell'}</>;
|
||||
}
|
||||
|
||||
if (riskScoreEngineStatus?.isUpdateAvailable) {
|
||||
return <RiskScoreUpdatePanel />;
|
||||
}
|
||||
|
||||
if (status.isDisabled || status.isDeprecated) {
|
||||
return (
|
||||
<EnableRiskScore
|
||||
|
|
|
@ -16,6 +16,9 @@ import type { inputsModel } from '../../../../common/store';
|
|||
import { REQUEST_NAMES, useFetch } from '../../../../common/hooks/use_fetch';
|
||||
import { useRiskScoreToastContent } from './use_risk_score_toast_content';
|
||||
import { installRiskScoreModule } from './utils';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
|
||||
import { SecuritySolutionLinkButton } from '../../../../common/components/links';
|
||||
import { SecurityPageName } from '../../../../../common/constants';
|
||||
|
||||
const RiskScoreEnableButtonComponent = ({
|
||||
refetch,
|
||||
|
@ -35,6 +38,7 @@ const RiskScoreEnableButtonComponent = ({
|
|||
const { http, notifications, theme, dashboard } = useKibana().services;
|
||||
const { renderDocLink, renderDashboardLink } = useRiskScoreToastContent(riskScoreEntity);
|
||||
const { fetch, isLoading } = useFetch(REQUEST_NAMES.ENABLE_RISK_SCORE, installRiskScoreModule);
|
||||
const isRiskEngineEnabled = useIsExperimentalFeatureEnabled('riskScoringRoutesEnabled');
|
||||
|
||||
const onBoardingRiskScore = useCallback(() => {
|
||||
fetch({
|
||||
|
@ -64,19 +68,34 @@ const RiskScoreEnableButtonComponent = ({
|
|||
]);
|
||||
|
||||
return (
|
||||
<EuiButton
|
||||
color="primary"
|
||||
fill
|
||||
onClick={onBoardingRiskScore}
|
||||
isLoading={isLoading}
|
||||
data-test-subj={`enable_${riskScoreEntity}_risk_score`}
|
||||
disabled={disabled}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.riskScore.enableButtonTitle"
|
||||
defaultMessage="Enable"
|
||||
/>
|
||||
</EuiButton>
|
||||
<>
|
||||
{isRiskEngineEnabled ? (
|
||||
<SecuritySolutionLinkButton
|
||||
color="primary"
|
||||
fill
|
||||
deepLinkId={SecurityPageName.entityAnalyticsManagement}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.riskScore.enableButtonTitle"
|
||||
defaultMessage="Enable"
|
||||
/>
|
||||
</SecuritySolutionLinkButton>
|
||||
) : (
|
||||
<EuiButton
|
||||
color="primary"
|
||||
fill
|
||||
onClick={onBoardingRiskScore}
|
||||
isLoading={isLoading}
|
||||
data-test-subj={`enable_${riskScoreEntity}_risk_score`}
|
||||
disabled={disabled}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.riskScore.enableButtonTitle"
|
||||
defaultMessage="Enable"
|
||||
/>
|
||||
</EuiButton>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { EuiPanel } from '@elastic/eui';
|
||||
import { noop } from 'lodash/fp';
|
||||
import { EnableRiskScore } from '../../../components/risk_score/enable_risk_score';
|
||||
import type { HostsComponentsQueryProps } from './types';
|
||||
|
@ -22,6 +23,8 @@ import {
|
|||
import { useQueryToggle } from '../../../../common/containers/query_toggle';
|
||||
import { EMPTY_SEVERITY_COUNT, RiskScoreEntity } from '../../../../../common/search_strategy';
|
||||
import { RiskScoresNoDataDetected } from '../../../components/risk_score/risk_score_onboarding/risk_score_no_data_detected';
|
||||
import { useRiskEngineStatus } from '../../../../entity_analytics/api/hooks/use_risk_engine_status';
|
||||
import { RiskScoreUpdatePanel } from '../../../../entity_analytics/components/risk_score_update_panel';
|
||||
|
||||
const HostRiskScoreTableManage = manageQuery(HostRiskScoreTable);
|
||||
|
||||
|
@ -46,6 +49,8 @@ export const HostRiskScoreQueryTabBody = ({
|
|||
getHostRiskScoreFilterQuerySelector(state, hostsModel.HostsType.page)
|
||||
);
|
||||
|
||||
const { data: riskScoreEngineStatus } = useRiskEngineStatus();
|
||||
|
||||
const pagination = useMemo(
|
||||
() => ({
|
||||
cursorStart: activePage * limit,
|
||||
|
@ -95,14 +100,20 @@ export const HostRiskScoreQueryTabBody = ({
|
|||
return <>{'TODO: Add RiskScore Upsell'}</>;
|
||||
}
|
||||
|
||||
if (riskScoreEngineStatus?.isUpdateAvailable) {
|
||||
return <RiskScoreUpdatePanel />;
|
||||
}
|
||||
|
||||
if (status.isDisabled || status.isDeprecated) {
|
||||
return (
|
||||
<EnableRiskScore
|
||||
{...status}
|
||||
entityType={RiskScoreEntity.host}
|
||||
refetch={refetch}
|
||||
timerange={timerange}
|
||||
/>
|
||||
<EuiPanel hasBorder>
|
||||
<EnableRiskScore
|
||||
{...status}
|
||||
entityType={RiskScoreEntity.host}
|
||||
refetch={refetch}
|
||||
timerange={timerange}
|
||||
/>
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -24,6 +24,8 @@ import {
|
|||
import { useQueryToggle } from '../../../../common/containers/query_toggle';
|
||||
import { EMPTY_SEVERITY_COUNT, RiskScoreEntity } from '../../../../../common/search_strategy';
|
||||
import { RiskScoresNoDataDetected } from '../../../components/risk_score/risk_score_onboarding/risk_score_no_data_detected';
|
||||
import { useRiskEngineStatus } from '../../../../entity_analytics/api/hooks/use_risk_engine_status';
|
||||
import { RiskScoreUpdatePanel } from '../../../../entity_analytics/components/risk_score_update_panel';
|
||||
|
||||
const UserRiskScoreTableManage = manageQuery(UserRiskScoreTable);
|
||||
|
||||
|
@ -36,6 +38,7 @@ export const UserRiskScoreQueryTabBody = ({
|
|||
startDate: from,
|
||||
type,
|
||||
}: UsersComponentsQueryProps) => {
|
||||
const { data: riskScoreEngineStatus } = useRiskEngineStatus();
|
||||
const getUserRiskScoreSelector = useMemo(() => usersSelectors.userRiskScoreSelector(), []);
|
||||
const { activePage, limit, sort } = useDeepEqualSelector((state: State) =>
|
||||
getUserRiskScoreSelector(state)
|
||||
|
@ -97,6 +100,10 @@ export const UserRiskScoreQueryTabBody = ({
|
|||
return <>{'TODO: Add RiskScore Upsell'}</>;
|
||||
}
|
||||
|
||||
if (riskScoreEngineStatus?.isUpdateAvailable) {
|
||||
return <RiskScoreUpdatePanel />;
|
||||
}
|
||||
|
||||
if (status.isDisabled || status.isDeprecated) {
|
||||
return (
|
||||
<EnableRiskScore
|
||||
|
|
|
@ -49,7 +49,6 @@ import { IconPipeline } from '../common/icons/pipeline';
|
|||
import { IconSavedObject } from '../common/icons/saved_object';
|
||||
import { IconDashboards } from '../common/icons/dashboards';
|
||||
import { IconEntityAnalytics } from '../common/icons/entity_analytics';
|
||||
|
||||
import { HostIsolationExceptionsApiClient } from './pages/host_isolation_exceptions/host_isolation_exceptions_api_client';
|
||||
|
||||
const categories = [
|
||||
|
@ -177,6 +176,7 @@ export const links: LinkItem = {
|
|||
path: ENTITY_ANALYTICS_MANAGEMENT_PATH,
|
||||
skipUrlState: true,
|
||||
hideTimeline: true,
|
||||
capabilities: [`${SERVER_APP_ID}.entity-analytics`],
|
||||
experimentalKey: 'riskScoringRoutesEnabled',
|
||||
},
|
||||
{
|
||||
|
@ -218,7 +218,7 @@ export const getManagementFilteredLinks = async (
|
|||
fleetAuthz && currentUser
|
||||
? calculateEndpointAuthz(licenseService, fleetAuthz, currentUser.roles)
|
||||
: getEndpointAuthzInitialState();
|
||||
|
||||
const showEntityAnalytics = licenseService.isPlatinumPlus();
|
||||
const showHostIsolationExceptions =
|
||||
canAccessHostIsolationExceptions || // access host isolation exceptions is a paid feature, always show the link.
|
||||
// read host isolation exceptions is not a paid feature, to allow deleting exceptions after a downgrade scenario.
|
||||
|
@ -256,5 +256,9 @@ export const getManagementFilteredLinks = async (
|
|||
linksToExclude.push(SecurityPageName.blocklist);
|
||||
}
|
||||
|
||||
if (!showEntityAnalytics) {
|
||||
linksToExclude.push(SecurityPageName.entityAnalyticsManagement);
|
||||
}
|
||||
|
||||
return excludeLinks(linksToExclude);
|
||||
};
|
||||
|
|
|
@ -35,6 +35,7 @@ import { useNavigateToAlertsPageWithFilters } from '../../../../common/hooks/use
|
|||
import { getRiskEntityTranslation } from './translations';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import { useGlobalFilterQuery } from '../../../../common/hooks/use_global_filter_query';
|
||||
import { useRiskEngineStatus } from '../../../../entity_analytics/api/hooks/use_risk_engine_status';
|
||||
|
||||
const EntityAnalyticsRiskScoresComponent = ({ riskEntity }: { riskEntity: RiskScoreEntity }) => {
|
||||
const { deleteQuery, setQuery, from, to } = useGlobalTime();
|
||||
|
@ -125,6 +126,8 @@ const EntityAnalyticsRiskScoresComponent = ({ riskEntity }: { riskEntity: RiskSc
|
|||
includeAlertsCount: true,
|
||||
});
|
||||
|
||||
const { data: riskScoreEngineStatus } = useRiskEngineStatus();
|
||||
|
||||
useQueryInspector({
|
||||
queryId: entity.tableQueryId,
|
||||
loading: isTableLoading,
|
||||
|
@ -149,6 +152,10 @@ const EntityAnalyticsRiskScoresComponent = ({ riskEntity }: { riskEntity: RiskSc
|
|||
isDeprecated: isDeprecated && !isTableLoading,
|
||||
};
|
||||
|
||||
if (riskScoreEngineStatus?.isUpdateAvailable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (status.isDisabled || status.isDeprecated) {
|
||||
return (
|
||||
<EnableRiskScore
|
||||
|
|
|
@ -25,10 +25,17 @@ import { EntityAnalyticsAnomalies } from '../components/entity_analytics/anomali
|
|||
import { SiemSearchBar } from '../../common/components/search_bar';
|
||||
import { InputsModelId } from '../../common/store/inputs/constants';
|
||||
import { FiltersGlobal } from '../../common/components/filters_global';
|
||||
import { useRiskEngineStatus } from '../../entity_analytics/api/hooks/use_risk_engine_status';
|
||||
import { RiskScoreUpdatePanel } from '../../entity_analytics/components/risk_score_update_panel';
|
||||
import { useHasSecurityCapability } from '../../helper_hooks';
|
||||
|
||||
const EntityAnalyticsComponent = () => {
|
||||
const { data: riskScoreEngineStatus } = useRiskEngineStatus();
|
||||
const { indicesExist, loading: isSourcererLoading, indexPattern } = useSourcererDataView();
|
||||
const { isPlatinumOrTrialLicense, capabilitiesFetched } = useMlCapabilities();
|
||||
const hasEntityAnalyticsCapability = useHasSecurityCapability('entity-analytics');
|
||||
const isRiskScoreModuleLicenseAvailable =
|
||||
isPlatinumOrTrialLicense && hasEntityAnalyticsCapability;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -48,6 +55,12 @@ const EntityAnalyticsComponent = () => {
|
|||
<EuiLoadingSpinner size="l" data-test-subj="entityAnalyticsLoader" />
|
||||
) : (
|
||||
<EuiFlexGroup direction="column" data-test-subj="entityAnalyticsSections">
|
||||
{riskScoreEngineStatus?.isUpdateAvailable && isRiskScoreModuleLicenseAvailable && (
|
||||
<EuiFlexItem>
|
||||
<RiskScoreUpdatePanel />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
|
||||
<EuiFlexItem>
|
||||
<EntityAnalyticsHeader />
|
||||
</EuiFlexItem>
|
||||
|
|
|
@ -10,9 +10,50 @@ import {
|
|||
createOrUpdateIlmPolicy,
|
||||
createOrUpdateIndexTemplate,
|
||||
} from '@kbn/alerting-plugin/server';
|
||||
import { loggingSystemMock, elasticsearchServiceMock } from '@kbn/core/server/mocks';
|
||||
import {
|
||||
loggingSystemMock,
|
||||
elasticsearchServiceMock,
|
||||
savedObjectsClientMock,
|
||||
} from '@kbn/core/server/mocks';
|
||||
import type { AuthenticatedUser } from '@kbn/security-plugin/common/model';
|
||||
import { RiskEngineDataClient } from './risk_engine_data_client';
|
||||
import { createDataStream } from './utils/create_datastream';
|
||||
import * as savedObjectConfig from './utils/saved_object_configuration';
|
||||
|
||||
const getSavedObjectConfiguration = (attributes = {}) => ({
|
||||
page: 1,
|
||||
per_page: 20,
|
||||
total: 1,
|
||||
saved_objects: [
|
||||
{
|
||||
type: 'risk-engine-configuration',
|
||||
id: 'de8ca330-2d26-11ee-bc86-f95bf6192ee6',
|
||||
namespaces: ['default'],
|
||||
attributes: {
|
||||
enabled: false,
|
||||
...attributes,
|
||||
},
|
||||
references: [],
|
||||
managed: false,
|
||||
updated_at: '2023-07-28T09:52:28.768Z',
|
||||
created_at: '2023-07-28T09:12:26.083Z',
|
||||
version: 'WzE4MzIsMV0=',
|
||||
coreMigrationVersion: '8.8.0',
|
||||
score: 0,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const transformsMock = {
|
||||
count: 1,
|
||||
transforms: [
|
||||
{
|
||||
id: 'ml_hostriskscore_pivot_transform_default',
|
||||
dest: { index: '' },
|
||||
source: { index: '' },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
jest.mock('@kbn/alerting-plugin/server', () => ({
|
||||
createOrUpdateComponentTemplate: jest.fn(),
|
||||
|
@ -28,6 +69,7 @@ describe('RiskEngineDataClient', () => {
|
|||
let riskEngineDataClient: RiskEngineDataClient;
|
||||
let logger: ReturnType<typeof loggingSystemMock.createLogger>;
|
||||
const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
|
||||
const mockSavedObjectClient = savedObjectsClientMock.create();
|
||||
const totalFieldsLimit = 1000;
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -290,11 +332,344 @@ describe('RiskEngineDataClient', () => {
|
|||
const error = new Error('There error');
|
||||
(createOrUpdateIlmPolicy as jest.Mock).mockRejectedValue(error);
|
||||
|
||||
await riskEngineDataClient.initializeResources({ namespace: 'default' });
|
||||
try {
|
||||
await riskEngineDataClient.initializeResources({ namespace: 'default' });
|
||||
} catch (e) {
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
`Error initializing risk engine resources: ${error.message}`
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
`Error initializing risk engine resources: ${error.message}`
|
||||
describe('getStatus', () => {
|
||||
it('should return initial status', async () => {
|
||||
const status = await riskEngineDataClient.getStatus({
|
||||
namespace: 'default',
|
||||
savedObjectsClient: mockSavedObjectClient,
|
||||
});
|
||||
expect(status).toEqual({
|
||||
riskEngineStatus: 'NOT_INSTALLED',
|
||||
legacyRiskEngineStatus: 'NOT_INSTALLED',
|
||||
});
|
||||
});
|
||||
|
||||
describe('saved object exists and transforms not', () => {
|
||||
beforeEach(() => {
|
||||
mockSavedObjectClient.find.mockResolvedValue(getSavedObjectConfiguration());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockSavedObjectClient.find.mockReset();
|
||||
});
|
||||
|
||||
it('should return status with enabled true', async () => {
|
||||
mockSavedObjectClient.find.mockResolvedValue(
|
||||
getSavedObjectConfiguration({
|
||||
enabled: true,
|
||||
})
|
||||
);
|
||||
|
||||
const status = await riskEngineDataClient.getStatus({
|
||||
namespace: 'default',
|
||||
savedObjectsClient: mockSavedObjectClient,
|
||||
});
|
||||
expect(status).toEqual({
|
||||
riskEngineStatus: 'ENABLED',
|
||||
legacyRiskEngineStatus: 'NOT_INSTALLED',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return status with enabled false', async () => {
|
||||
mockSavedObjectClient.find.mockResolvedValue(getSavedObjectConfiguration());
|
||||
|
||||
const status = await riskEngineDataClient.getStatus({
|
||||
namespace: 'default',
|
||||
savedObjectsClient: mockSavedObjectClient,
|
||||
});
|
||||
expect(status).toEqual({
|
||||
riskEngineStatus: 'DISABLED',
|
||||
legacyRiskEngineStatus: 'NOT_INSTALLED',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('legacy transforms', () => {
|
||||
it('should fetch transforms', async () => {
|
||||
await riskEngineDataClient.getStatus({
|
||||
namespace: 'default',
|
||||
savedObjectsClient: mockSavedObjectClient,
|
||||
});
|
||||
|
||||
expect(esClient.transform.getTransform).toHaveBeenCalledTimes(4);
|
||||
expect(esClient.transform.getTransform).toHaveBeenNthCalledWith(1, {
|
||||
transform_id: 'ml_hostriskscore_pivot_transform_default',
|
||||
});
|
||||
expect(esClient.transform.getTransform).toHaveBeenNthCalledWith(2, {
|
||||
transform_id: 'ml_hostriskscore_latest_transform_default',
|
||||
});
|
||||
expect(esClient.transform.getTransform).toHaveBeenNthCalledWith(3, {
|
||||
transform_id: 'ml_userriskscore_pivot_transform_default',
|
||||
});
|
||||
expect(esClient.transform.getTransform).toHaveBeenNthCalledWith(4, {
|
||||
transform_id: 'ml_userriskscore_latest_transform_default',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return that legacy transform enabled if at least on transform exist', async () => {
|
||||
esClient.transform.getTransform.mockResolvedValueOnce(transformsMock);
|
||||
|
||||
const status = await riskEngineDataClient.getStatus({
|
||||
namespace: 'default',
|
||||
savedObjectsClient: mockSavedObjectClient,
|
||||
});
|
||||
|
||||
expect(status).toEqual({
|
||||
riskEngineStatus: 'NOT_INSTALLED',
|
||||
legacyRiskEngineStatus: 'ENABLED',
|
||||
});
|
||||
|
||||
esClient.transform.getTransformStats.mockReset();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('enableRiskEngine', () => {
|
||||
afterEach(() => {
|
||||
mockSavedObjectClient.find.mockReset();
|
||||
});
|
||||
|
||||
it('should return error if saved object not exist', async () => {
|
||||
mockSavedObjectClient.find.mockResolvedValueOnce({
|
||||
page: 1,
|
||||
per_page: 20,
|
||||
total: 0,
|
||||
saved_objects: [],
|
||||
});
|
||||
|
||||
expect.assertions(1);
|
||||
try {
|
||||
await riskEngineDataClient.enableRiskEngine({
|
||||
savedObjectsClient: mockSavedObjectClient,
|
||||
user: { username: 'elastic' } as AuthenticatedUser,
|
||||
});
|
||||
} catch (e) {
|
||||
expect(e.message).toEqual('There no saved object configuration for risk engine');
|
||||
}
|
||||
});
|
||||
|
||||
it('should update saved object attrubute', async () => {
|
||||
mockSavedObjectClient.find.mockResolvedValueOnce(getSavedObjectConfiguration());
|
||||
|
||||
await riskEngineDataClient.enableRiskEngine({
|
||||
savedObjectsClient: mockSavedObjectClient,
|
||||
user: { username: 'elastic' } as AuthenticatedUser,
|
||||
});
|
||||
|
||||
expect(mockSavedObjectClient.update).toHaveBeenCalledWith(
|
||||
'risk-engine-configuration',
|
||||
'de8ca330-2d26-11ee-bc86-f95bf6192ee6',
|
||||
{
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
refresh: 'wait_for',
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('disableRiskEngine', () => {
|
||||
afterEach(() => {
|
||||
mockSavedObjectClient.find.mockReset();
|
||||
});
|
||||
|
||||
it('should return error if saved object not exist', async () => {
|
||||
mockSavedObjectClient.find.mockResolvedValueOnce({
|
||||
page: 1,
|
||||
per_page: 20,
|
||||
total: 0,
|
||||
saved_objects: [],
|
||||
});
|
||||
|
||||
expect.assertions(1);
|
||||
try {
|
||||
await riskEngineDataClient.disableRiskEngine({
|
||||
savedObjectsClient: mockSavedObjectClient,
|
||||
user: { username: 'elastic' } as AuthenticatedUser,
|
||||
});
|
||||
} catch (e) {
|
||||
expect(e.message).toEqual('There no saved object configuration for risk engine');
|
||||
}
|
||||
});
|
||||
|
||||
it('should update saved object attrubute', async () => {
|
||||
mockSavedObjectClient.find.mockResolvedValueOnce(getSavedObjectConfiguration());
|
||||
|
||||
await riskEngineDataClient.disableRiskEngine({
|
||||
savedObjectsClient: mockSavedObjectClient,
|
||||
user: { username: 'elastic' } as AuthenticatedUser,
|
||||
});
|
||||
|
||||
expect(mockSavedObjectClient.update).toHaveBeenCalledWith(
|
||||
'risk-engine-configuration',
|
||||
'de8ca330-2d26-11ee-bc86-f95bf6192ee6',
|
||||
{
|
||||
enabled: false,
|
||||
},
|
||||
{
|
||||
refresh: 'wait_for',
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('init', () => {
|
||||
const initializeResourcesMock = jest.spyOn(
|
||||
RiskEngineDataClient.prototype,
|
||||
'initializeResources'
|
||||
);
|
||||
const enableRiskEngineMock = jest.spyOn(RiskEngineDataClient.prototype, 'enableRiskEngine');
|
||||
|
||||
const disableLegacyRiskEngineMock = jest.spyOn(
|
||||
RiskEngineDataClient.prototype,
|
||||
'disableLegacyRiskEngine'
|
||||
);
|
||||
beforeEach(() => {
|
||||
disableLegacyRiskEngineMock.mockImplementation(() => Promise.resolve(true));
|
||||
|
||||
initializeResourcesMock.mockImplementation(() => {
|
||||
return Promise.resolve();
|
||||
});
|
||||
|
||||
enableRiskEngineMock.mockImplementation(() => {
|
||||
return Promise.resolve(getSavedObjectConfiguration().saved_objects[0]);
|
||||
});
|
||||
|
||||
jest.spyOn(savedObjectConfig, 'initSavedObjects').mockImplementation(() => {
|
||||
return Promise.resolve(getSavedObjectConfiguration().saved_objects[0]);
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
initializeResourcesMock.mockReset();
|
||||
enableRiskEngineMock.mockReset();
|
||||
disableLegacyRiskEngineMock.mockReset();
|
||||
});
|
||||
|
||||
it('success', async () => {
|
||||
const initResult = await riskEngineDataClient.init({
|
||||
savedObjectsClient: mockSavedObjectClient,
|
||||
namespace: 'default',
|
||||
user: { username: 'elastic' } as AuthenticatedUser,
|
||||
});
|
||||
|
||||
expect(initResult).toEqual({
|
||||
errors: [],
|
||||
legacyRiskEngineDisabled: true,
|
||||
riskEngineConfigurationCreated: true,
|
||||
riskEngineEnabled: true,
|
||||
riskEngineResourcesInstalled: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should catch error for disableLegacyRiskEngine, but continue', async () => {
|
||||
disableLegacyRiskEngineMock.mockImplementation(() => {
|
||||
throw new Error('Error disableLegacyRiskEngineMock');
|
||||
});
|
||||
const initResult = await riskEngineDataClient.init({
|
||||
savedObjectsClient: mockSavedObjectClient,
|
||||
namespace: 'default',
|
||||
user: { username: 'elastic' } as AuthenticatedUser,
|
||||
});
|
||||
|
||||
expect(initResult).toEqual({
|
||||
errors: ['Error disableLegacyRiskEngineMock'],
|
||||
legacyRiskEngineDisabled: false,
|
||||
riskEngineConfigurationCreated: true,
|
||||
riskEngineEnabled: true,
|
||||
riskEngineResourcesInstalled: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should catch error for resource init', async () => {
|
||||
disableLegacyRiskEngineMock.mockImplementationOnce(() => {
|
||||
throw new Error('Error disableLegacyRiskEngineMock');
|
||||
});
|
||||
|
||||
const initResult = await riskEngineDataClient.init({
|
||||
savedObjectsClient: mockSavedObjectClient,
|
||||
namespace: 'default',
|
||||
user: { username: 'elastic' } as AuthenticatedUser,
|
||||
});
|
||||
|
||||
expect(initResult).toEqual({
|
||||
errors: ['Error disableLegacyRiskEngineMock'],
|
||||
legacyRiskEngineDisabled: false,
|
||||
riskEngineConfigurationCreated: true,
|
||||
riskEngineEnabled: true,
|
||||
riskEngineResourcesInstalled: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should catch error for initializeResources and stop', async () => {
|
||||
initializeResourcesMock.mockImplementationOnce(() => {
|
||||
throw new Error('Error initializeResourcesMock');
|
||||
});
|
||||
|
||||
const initResult = await riskEngineDataClient.init({
|
||||
savedObjectsClient: mockSavedObjectClient,
|
||||
namespace: 'default',
|
||||
user: { username: 'elastic' } as AuthenticatedUser,
|
||||
});
|
||||
|
||||
expect(initResult).toEqual({
|
||||
errors: ['Error initializeResourcesMock'],
|
||||
legacyRiskEngineDisabled: true,
|
||||
riskEngineConfigurationCreated: false,
|
||||
riskEngineEnabled: false,
|
||||
riskEngineResourcesInstalled: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should catch error for initSavedObjects and stop', async () => {
|
||||
jest.spyOn(savedObjectConfig, 'initSavedObjects').mockImplementationOnce(() => {
|
||||
throw new Error('Error initSavedObjects');
|
||||
});
|
||||
|
||||
const initResult = await riskEngineDataClient.init({
|
||||
savedObjectsClient: mockSavedObjectClient,
|
||||
namespace: 'default',
|
||||
user: { username: 'elastic' } as AuthenticatedUser,
|
||||
});
|
||||
|
||||
expect(initResult).toEqual({
|
||||
errors: ['Error initSavedObjects'],
|
||||
legacyRiskEngineDisabled: true,
|
||||
riskEngineConfigurationCreated: false,
|
||||
riskEngineEnabled: false,
|
||||
riskEngineResourcesInstalled: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should catch error for enableRiskEngineMock and stop', async () => {
|
||||
enableRiskEngineMock.mockImplementationOnce(() => {
|
||||
throw new Error('Error enableRiskEngineMock');
|
||||
});
|
||||
|
||||
const initResult = await riskEngineDataClient.init({
|
||||
savedObjectsClient: mockSavedObjectClient,
|
||||
namespace: 'default',
|
||||
user: { username: 'elastic' } as AuthenticatedUser,
|
||||
});
|
||||
|
||||
expect(initResult).toEqual({
|
||||
errors: ['Error enableRiskEngineMock'],
|
||||
legacyRiskEngineDisabled: true,
|
||||
riskEngineConfigurationCreated: true,
|
||||
riskEngineEnabled: false,
|
||||
riskEngineResourcesInstalled: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import type { Metadata } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import type { AuthenticatedUser } from '@kbn/security-plugin/common/model';
|
||||
import type { ClusterPutComponentTemplateRequest } from '@elastic/elasticsearch/lib/api/types';
|
||||
import {
|
||||
createOrUpdateComponentTemplate,
|
||||
|
@ -15,6 +16,7 @@ import {
|
|||
import { mappingFromFieldMap } from '@kbn/alerting-plugin/common';
|
||||
import { DEFAULT_NAMESPACE_STRING } from '@kbn/core-saved-objects-utils-server';
|
||||
import type { Logger, ElasticsearchClient } from '@kbn/core/server';
|
||||
|
||||
import {
|
||||
riskScoreFieldMap,
|
||||
getIndexPattern,
|
||||
|
@ -26,6 +28,20 @@ import {
|
|||
import { createDataStream } from './utils/create_datastream';
|
||||
import type { RiskEngineDataWriter as Writer } from './risk_engine_data_writer';
|
||||
import { RiskEngineDataWriter } from './risk_engine_data_writer';
|
||||
import type { InitRiskEngineResult } from '../../../common/risk_engine/types';
|
||||
import { RiskEngineStatus } from '../../../common/risk_engine/types';
|
||||
import { getLegacyTransforms, removeLegacyTransforms } from './utils/risk_engine_transforms';
|
||||
import {
|
||||
updateSavedObjectAttribute,
|
||||
getConfiguration,
|
||||
initSavedObjects,
|
||||
} from './utils/saved_object_configuration';
|
||||
import type { UpdateConfigOpts, SavedObjectsClients } from './utils/saved_object_configuration';
|
||||
|
||||
interface InitOpts extends SavedObjectsClients {
|
||||
namespace: string;
|
||||
user: AuthenticatedUser | null | undefined;
|
||||
}
|
||||
|
||||
interface InitializeRiskEngineResourcesOpts {
|
||||
namespace?: string;
|
||||
|
@ -41,6 +57,49 @@ export class RiskEngineDataClient {
|
|||
private writerCache: Map<string, Writer> = new Map();
|
||||
constructor(private readonly options: RiskEngineDataClientOpts) {}
|
||||
|
||||
public async init({ namespace, savedObjectsClient, user }: InitOpts) {
|
||||
const result: InitRiskEngineResult = {
|
||||
legacyRiskEngineDisabled: false,
|
||||
riskEngineResourcesInstalled: false,
|
||||
riskEngineConfigurationCreated: false,
|
||||
riskEngineEnabled: false,
|
||||
errors: [] as string[],
|
||||
};
|
||||
|
||||
try {
|
||||
result.legacyRiskEngineDisabled = await this.disableLegacyRiskEngine({ namespace });
|
||||
} catch (e) {
|
||||
result.legacyRiskEngineDisabled = false;
|
||||
result.errors.push(e.message);
|
||||
}
|
||||
|
||||
try {
|
||||
await this.initializeResources({ namespace });
|
||||
result.riskEngineResourcesInstalled = true;
|
||||
} catch (e) {
|
||||
result.errors.push(e.message);
|
||||
return result;
|
||||
}
|
||||
|
||||
try {
|
||||
await initSavedObjects({ savedObjectsClient, user });
|
||||
result.riskEngineConfigurationCreated = true;
|
||||
} catch (e) {
|
||||
result.errors.push(e.message);
|
||||
return result;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.enableRiskEngine({ savedObjectsClient, user });
|
||||
result.riskEngineEnabled = true;
|
||||
} catch (e) {
|
||||
result.errors.push(e.message);
|
||||
return result;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async getWriter({ namespace }: { namespace: string }): Promise<Writer> {
|
||||
if (this.writerCache.get(namespace)) {
|
||||
return this.writerCache.get(namespace) as Writer;
|
||||
|
@ -57,10 +116,85 @@ export class RiskEngineDataClient {
|
|||
index,
|
||||
logger: this.options.logger,
|
||||
});
|
||||
|
||||
this.writerCache.set(namespace, writer);
|
||||
return writer;
|
||||
}
|
||||
|
||||
public async getStatus({
|
||||
savedObjectsClient,
|
||||
namespace,
|
||||
}: SavedObjectsClients & {
|
||||
namespace: string;
|
||||
}) {
|
||||
const riskEngineStatus = await this.getCurrentStatus({ savedObjectsClient });
|
||||
const legacyRiskEngineStatus = await this.getLegacyStatus({ namespace });
|
||||
return { riskEngineStatus, legacyRiskEngineStatus };
|
||||
}
|
||||
|
||||
public async enableRiskEngine({ savedObjectsClient, user }: UpdateConfigOpts) {
|
||||
// code to run task
|
||||
|
||||
return updateSavedObjectAttribute({
|
||||
savedObjectsClient,
|
||||
user,
|
||||
attributes: {
|
||||
enabled: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public async disableRiskEngine({ savedObjectsClient, user }: UpdateConfigOpts) {
|
||||
// code to stop task
|
||||
|
||||
return updateSavedObjectAttribute({
|
||||
savedObjectsClient,
|
||||
user,
|
||||
attributes: {
|
||||
enabled: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public async disableLegacyRiskEngine({ namespace }: { namespace: string }) {
|
||||
const legacyRiskEngineStatus = await this.getLegacyStatus({ namespace });
|
||||
|
||||
if (legacyRiskEngineStatus === RiskEngineStatus.NOT_INSTALLED) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const esClient = await this.options.elasticsearchClientPromise;
|
||||
await removeLegacyTransforms({
|
||||
esClient,
|
||||
namespace,
|
||||
});
|
||||
|
||||
const newlegacyRiskEngineStatus = await this.getLegacyStatus({ namespace });
|
||||
|
||||
return newlegacyRiskEngineStatus === RiskEngineStatus.NOT_INSTALLED;
|
||||
}
|
||||
|
||||
private async getCurrentStatus({ savedObjectsClient }: SavedObjectsClients) {
|
||||
const configuration = await getConfiguration({ savedObjectsClient });
|
||||
|
||||
if (configuration) {
|
||||
return configuration.enabled ? RiskEngineStatus.ENABLED : RiskEngineStatus.DISABLED;
|
||||
}
|
||||
|
||||
return RiskEngineStatus.NOT_INSTALLED;
|
||||
}
|
||||
|
||||
private async getLegacyStatus({ namespace }: { namespace: string }) {
|
||||
const esClient = await this.options.elasticsearchClientPromise;
|
||||
const transforms = await getLegacyTransforms({ namespace, esClient });
|
||||
|
||||
if (transforms.length === 0) {
|
||||
return RiskEngineStatus.NOT_INSTALLED;
|
||||
}
|
||||
|
||||
return RiskEngineStatus.ENABLED;
|
||||
}
|
||||
|
||||
public async initializeResources({
|
||||
namespace = DEFAULT_NAMESPACE_STRING,
|
||||
}: InitializeRiskEngineResourcesOpts) {
|
||||
|
@ -139,6 +273,7 @@ export class RiskEngineDataClient {
|
|||
await this.initializeWriter(namespace, indexPatterns.alias);
|
||||
} catch (error) {
|
||||
this.options.logger.error(`Error initializing risk engine resources: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,3 +6,7 @@
|
|||
*/
|
||||
|
||||
export { riskScorePreviewRoute } from './risk_score_preview_route';
|
||||
export { riskEngineInitRoute } from './risk_engine_init_route';
|
||||
export { riskEngineEnableRoute } from './risk_engine_enable_route';
|
||||
export { riskEngineDisableRoute } from './risk_engine_disable_route';
|
||||
export { riskEngineStatusRoute } from './risk_engine_status_route';
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* 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 { Logger } from '@kbn/core/server';
|
||||
import { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils';
|
||||
import { transformError } from '@kbn/securitysolution-es-utils';
|
||||
import { RISK_ENGINE_DISABLE_URL, APP_ID } from '../../../../common/constants';
|
||||
import type { SetupPlugins } from '../../../plugin';
|
||||
import type { SecuritySolutionPluginRouter } from '../../../types';
|
||||
|
||||
export const riskEngineDisableRoute = (
|
||||
router: SecuritySolutionPluginRouter,
|
||||
logger: Logger,
|
||||
security: SetupPlugins['security']
|
||||
) => {
|
||||
router.post(
|
||||
{
|
||||
path: RISK_ENGINE_DISABLE_URL,
|
||||
validate: {},
|
||||
options: {
|
||||
tags: ['access:securitySolution', `access:${APP_ID}-entity-analytics`],
|
||||
},
|
||||
},
|
||||
async (context, request, response) => {
|
||||
const siemResponse = buildSiemResponse(response);
|
||||
|
||||
const securitySolution = await context.securitySolution;
|
||||
const soClient = (await context.core).savedObjects.client;
|
||||
const riskEngineClient = securitySolution.getRiskEngineDataClient();
|
||||
const user = security?.authc.getCurrentUser(request);
|
||||
|
||||
try {
|
||||
await riskEngineClient.disableRiskEngine({
|
||||
savedObjectsClient: soClient,
|
||||
user,
|
||||
});
|
||||
return response.ok({ body: { success: true } });
|
||||
} catch (e) {
|
||||
const error = transformError(e);
|
||||
|
||||
return siemResponse.error({
|
||||
statusCode: error.statusCode,
|
||||
body: { message: error.message, full_error: JSON.stringify(e) },
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* 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 { Logger } from '@kbn/core/server';
|
||||
import { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils';
|
||||
import { transformError } from '@kbn/securitysolution-es-utils';
|
||||
import { RISK_ENGINE_ENABLE_URL, APP_ID } from '../../../../common/constants';
|
||||
import type { SetupPlugins } from '../../../plugin';
|
||||
import type { SecuritySolutionPluginRouter } from '../../../types';
|
||||
|
||||
export const riskEngineEnableRoute = (
|
||||
router: SecuritySolutionPluginRouter,
|
||||
logger: Logger,
|
||||
security: SetupPlugins['security']
|
||||
) => {
|
||||
router.post(
|
||||
{
|
||||
path: RISK_ENGINE_ENABLE_URL,
|
||||
validate: {},
|
||||
options: {
|
||||
tags: ['access:securitySolution', `access:${APP_ID}-entity-analytics`],
|
||||
},
|
||||
},
|
||||
async (context, request, response) => {
|
||||
const siemResponse = buildSiemResponse(response);
|
||||
const securitySolution = await context.securitySolution;
|
||||
const soClient = (await context.core).savedObjects.client;
|
||||
const riskEngineClient = securitySolution.getRiskEngineDataClient();
|
||||
const user = security?.authc.getCurrentUser(request);
|
||||
|
||||
try {
|
||||
await riskEngineClient.enableRiskEngine({
|
||||
savedObjectsClient: soClient,
|
||||
user,
|
||||
});
|
||||
return response.ok({ body: { success: true } });
|
||||
} catch (e) {
|
||||
const error = transformError(e);
|
||||
|
||||
return siemResponse.error({
|
||||
statusCode: error.statusCode,
|
||||
body: { message: error.message, full_error: JSON.stringify(e) },
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* 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 { Logger } from '@kbn/core/server';
|
||||
import { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils';
|
||||
import { transformError } from '@kbn/securitysolution-es-utils';
|
||||
import { RISK_ENGINE_INIT_URL, APP_ID } from '../../../../common/constants';
|
||||
import type { SetupPlugins } from '../../../plugin';
|
||||
|
||||
import type { SecuritySolutionPluginRouter } from '../../../types';
|
||||
|
||||
export const riskEngineInitRoute = (
|
||||
router: SecuritySolutionPluginRouter,
|
||||
logger: Logger,
|
||||
security: SetupPlugins['security']
|
||||
) => {
|
||||
router.post(
|
||||
{
|
||||
path: RISK_ENGINE_INIT_URL,
|
||||
validate: {},
|
||||
options: {
|
||||
tags: ['access:securitySolution', `access:${APP_ID}-entity-analytics`],
|
||||
},
|
||||
},
|
||||
async (context, request, response) => {
|
||||
const siemResponse = buildSiemResponse(response);
|
||||
const securitySolution = await context.securitySolution;
|
||||
const soClient = (await context.core).savedObjects.client;
|
||||
const riskEngineClient = securitySolution.getRiskEngineDataClient();
|
||||
const spaceId = securitySolution.getSpaceId();
|
||||
const user = security?.authc.getCurrentUser(request);
|
||||
|
||||
try {
|
||||
const initResult = await riskEngineClient.init({
|
||||
savedObjectsClient: soClient,
|
||||
namespace: spaceId,
|
||||
user,
|
||||
});
|
||||
|
||||
const initResultResponse = {
|
||||
risk_engine_enabled: initResult.riskEngineEnabled,
|
||||
risk_engine_resources_installed: initResult.riskEngineResourcesInstalled,
|
||||
risk_engine_configuration_created: initResult.riskEngineConfigurationCreated,
|
||||
legacy_risk_engine_disabled: initResult.legacyRiskEngineDisabled,
|
||||
errors: initResult.errors,
|
||||
};
|
||||
|
||||
if (
|
||||
!initResult.riskEngineEnabled ||
|
||||
!initResult.riskEngineResourcesInstalled ||
|
||||
!initResult.riskEngineConfigurationCreated
|
||||
) {
|
||||
return siemResponse.error({
|
||||
statusCode: 400,
|
||||
body: {
|
||||
message: initResultResponse.errors.join('\n'),
|
||||
full_error: initResultResponse,
|
||||
},
|
||||
});
|
||||
}
|
||||
return response.ok({ body: { result: initResultResponse } });
|
||||
} catch (e) {
|
||||
const error = transformError(e);
|
||||
|
||||
return siemResponse.error({
|
||||
statusCode: error.statusCode,
|
||||
body: { message: error.message, full_error: JSON.stringify(e) },
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { Logger } from '@kbn/core/server';
|
||||
import { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils';
|
||||
import { transformError } from '@kbn/securitysolution-es-utils';
|
||||
import { RISK_ENGINE_STATUS_URL, APP_ID } from '../../../../common/constants';
|
||||
|
||||
import type { SecuritySolutionPluginRouter } from '../../../types';
|
||||
|
||||
export const riskEngineStatusRoute = (router: SecuritySolutionPluginRouter, logger: Logger) => {
|
||||
router.get(
|
||||
{
|
||||
path: RISK_ENGINE_STATUS_URL,
|
||||
validate: {},
|
||||
options: {
|
||||
tags: ['access:securitySolution', `access:${APP_ID}-entity-analytics`],
|
||||
},
|
||||
},
|
||||
async (context, request, response) => {
|
||||
const siemResponse = buildSiemResponse(response);
|
||||
|
||||
const securitySolution = await context.securitySolution;
|
||||
const soClient = (await context.core).savedObjects.client;
|
||||
const riskEngineClient = securitySolution.getRiskEngineDataClient();
|
||||
const spaceId = securitySolution.getSpaceId();
|
||||
|
||||
try {
|
||||
const result = await riskEngineClient.getStatus({
|
||||
savedObjectsClient: soClient,
|
||||
namespace: spaceId,
|
||||
});
|
||||
return response.ok({
|
||||
body: {
|
||||
risk_engine_status: result.riskEngineStatus,
|
||||
legacy_risk_engine_status: result.legacyRiskEngineStatus,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
const error = transformError(e);
|
||||
|
||||
return siemResponse.error({
|
||||
statusCode: error.statusCode,
|
||||
body: { message: error.message, full_error: JSON.stringify(e) },
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export * from './risk_engine_configuration_type';
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* 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 { SECURITY_SOLUTION_SAVED_OBJECT_INDEX } from '@kbn/core-saved-objects-server';
|
||||
import type { SavedObjectsType } from '@kbn/core/server';
|
||||
|
||||
export const riskEngineConfigurationTypeName = 'risk-engine-configuration';
|
||||
|
||||
export const riskEngineConfigurationTypeMappings: SavedObjectsType['mappings'] = {
|
||||
dynamic: false,
|
||||
properties: {
|
||||
enabled: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const riskEngineConfigurationType: SavedObjectsType = {
|
||||
name: riskEngineConfigurationTypeName,
|
||||
indexPattern: SECURITY_SOLUTION_SAVED_OBJECT_INDEX,
|
||||
hidden: false,
|
||||
namespaceType: 'multiple-isolated',
|
||||
mappings: riskEngineConfigurationTypeMappings,
|
||||
};
|
|
@ -44,6 +44,56 @@ paths:
|
|||
$ref: '#/components/schemas/RiskScoresPreviewResponse'
|
||||
'400':
|
||||
description: Invalid request
|
||||
/engine/status:
|
||||
get:
|
||||
summary: Get the status of the Risk Engine
|
||||
description: Returns the status of both the legacy transform-based risk engine, as well as the new risk engine
|
||||
responses:
|
||||
'200':
|
||||
description: Successful response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RiskEngineStatusResponse'
|
||||
|
||||
/engine/init:
|
||||
post:
|
||||
summary: Initialize the Risk Engine
|
||||
description: Initializes the Risk Engine by creating the necessary indices and mappings, removing old transforms, and starting the new risk engine
|
||||
responses:
|
||||
'200':
|
||||
description: Successful response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RiskEngineInitResponse'
|
||||
/engine/enable:
|
||||
post:
|
||||
summary: Enable the Risk Engine
|
||||
requestBody:
|
||||
content:
|
||||
application/json: {}
|
||||
responses:
|
||||
'200':
|
||||
description: Successful response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RiskEngineEnableResponse'
|
||||
/engine/disable:
|
||||
post:
|
||||
summary: Disable the Risk Engine
|
||||
requestBody:
|
||||
content:
|
||||
application/json: {}
|
||||
responses:
|
||||
'200':
|
||||
description: Successful response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RiskEngineDisableResponse'
|
||||
|
||||
|
||||
components:
|
||||
schemas:
|
||||
|
@ -153,7 +203,46 @@ components:
|
|||
description: A list of risk scores
|
||||
items:
|
||||
$ref: '#/components/schemas/RiskScore'
|
||||
RiskEngineStatusResponse:
|
||||
type: object
|
||||
properties:
|
||||
legacy_risk_engine_status:
|
||||
$ref: '#/components/schemas/RiskEngineStatus'
|
||||
risk_engine_status:
|
||||
$ref: '#/components/schemas/RiskEngineStatus'
|
||||
RiskEngineInitResponse:
|
||||
type: object
|
||||
properties:
|
||||
result:
|
||||
type: object
|
||||
properties:
|
||||
risk_engine_enabled:
|
||||
type: boolean
|
||||
risk_engine_resources_installed:
|
||||
type: boolean
|
||||
risk_engine_configuration_created:
|
||||
type: boolean
|
||||
legacy_risk_engine_disabled:
|
||||
type: boolean
|
||||
errors:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
|
||||
|
||||
|
||||
RiskEngineEnableResponse:
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
RiskEngineDisableResponse:
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
|
||||
|
||||
AfterKeys:
|
||||
type: object
|
||||
properties:
|
||||
|
@ -326,3 +415,22 @@ components:
|
|||
- type: 'global_identifier'
|
||||
host: 0.5
|
||||
user: 0.1
|
||||
RiskEngineStatus:
|
||||
type: string
|
||||
enum:
|
||||
- 'NOT_INSTALLED'
|
||||
- 'DISABLED'
|
||||
- 'ENABLED'
|
||||
RiskEngineInitStep:
|
||||
type: object
|
||||
required:
|
||||
- type
|
||||
- success
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
success:
|
||||
type: boolean
|
||||
error:
|
||||
type: string
|
||||
|
|
@ -14,6 +14,7 @@ import type {
|
|||
RiskCategories,
|
||||
RiskWeights,
|
||||
} from '../../../common/risk_engine';
|
||||
import type { RiskEngineStatus } from '../../../common/risk_engine/types';
|
||||
|
||||
export interface CalculateScoresParams {
|
||||
afterKeys: AfterKeys;
|
||||
|
@ -57,6 +58,49 @@ export interface CalculateScoresResponse {
|
|||
};
|
||||
}
|
||||
|
||||
export interface GetRiskEngineStatusResponse {
|
||||
legacy_risk_engine_status: RiskEngineStatus;
|
||||
risk_engine_status: RiskEngineStatus;
|
||||
}
|
||||
|
||||
interface InitRiskEngineResultResponse {
|
||||
risk_engine_enabled: boolean;
|
||||
risk_engine_resources_installed: boolean;
|
||||
risk_engine_configuration_created: boolean;
|
||||
legacy_risk_engine_disabled: boolean;
|
||||
errors: string[];
|
||||
}
|
||||
|
||||
export interface InitRiskEngineResponse {
|
||||
result: InitRiskEngineResultResponse;
|
||||
}
|
||||
|
||||
export interface InitRiskEngineError {
|
||||
body: {
|
||||
message: {
|
||||
message: string;
|
||||
full_error: InitRiskEngineResultResponse | undefined;
|
||||
} & string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface EnableDisableRiskEngineErrorResponse {
|
||||
body: {
|
||||
message: {
|
||||
message: string;
|
||||
full_error: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface EnableRiskEngineResponse {
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
export interface DisableRiskEngineResponse {
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
export interface SimpleRiskInput {
|
||||
id: string;
|
||||
index: string;
|
||||
|
@ -117,3 +161,7 @@ export interface RiskScoreBucket {
|
|||
};
|
||||
inputs: SearchResponse;
|
||||
}
|
||||
|
||||
export interface RiskEngineConfiguration {
|
||||
enabled: boolean;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { ElasticsearchClient } from '@kbn/core/server';
|
||||
import type {
|
||||
TransformGetTransformResponse,
|
||||
TransformGetTransformTransformSummary,
|
||||
} from '@elastic/elasticsearch/lib/api/types';
|
||||
import { RiskScoreEntity } from '../../../../common/search_strategy';
|
||||
import {
|
||||
getRiskScorePivotTransformId,
|
||||
getRiskScoreLatestTransformId,
|
||||
} from '../../../../common/utils/risk_score_modules';
|
||||
|
||||
export const getLegacyTransforms = async ({
|
||||
namespace,
|
||||
esClient,
|
||||
}: {
|
||||
namespace: string;
|
||||
esClient: ElasticsearchClient;
|
||||
}) => {
|
||||
const getTransformStatsRequests: Array<Promise<TransformGetTransformResponse>> = [];
|
||||
[RiskScoreEntity.host, RiskScoreEntity.user].forEach((entity) => {
|
||||
getTransformStatsRequests.push(
|
||||
esClient.transform.getTransform({
|
||||
transform_id: getRiskScorePivotTransformId(entity, namespace),
|
||||
})
|
||||
);
|
||||
getTransformStatsRequests.push(
|
||||
esClient.transform.getTransform({
|
||||
transform_id: getRiskScoreLatestTransformId(entity, namespace),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
const results = await Promise.allSettled(getTransformStatsRequests);
|
||||
|
||||
const transforms = results.reduce((acc, result) => {
|
||||
if (result.status === 'fulfilled' && result.value?.transforms?.length > 0) {
|
||||
acc.push(...result.value.transforms);
|
||||
}
|
||||
return acc;
|
||||
}, [] as TransformGetTransformTransformSummary[]);
|
||||
|
||||
return transforms;
|
||||
};
|
||||
|
||||
export const removeLegacyTransforms = async ({
|
||||
namespace,
|
||||
esClient,
|
||||
}: {
|
||||
namespace: string;
|
||||
esClient: ElasticsearchClient;
|
||||
}): Promise<void> => {
|
||||
const transforms = await getLegacyTransforms({ namespace, esClient });
|
||||
|
||||
const stopTransformRequests = transforms.map((t) =>
|
||||
esClient.transform.deleteTransform({
|
||||
transform_id: t.id,
|
||||
force: true,
|
||||
})
|
||||
);
|
||||
|
||||
await Promise.allSettled(stopTransformRequests);
|
||||
};
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* 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 { SavedObject, SavedObjectsClientContract } from '@kbn/core/server';
|
||||
import type { AuthenticatedUser } from '@kbn/security-plugin/common/model';
|
||||
|
||||
import type { RiskEngineConfiguration } from '../types';
|
||||
import { riskEngineConfigurationTypeName } from '../saved_object';
|
||||
|
||||
export interface SavedObjectsClients {
|
||||
savedObjectsClient: SavedObjectsClientContract;
|
||||
}
|
||||
|
||||
export interface UpdateConfigOpts extends SavedObjectsClients {
|
||||
user: AuthenticatedUser | null | undefined;
|
||||
}
|
||||
|
||||
const getConfigurationSavedObject = async ({
|
||||
savedObjectsClient,
|
||||
}: SavedObjectsClients): Promise<SavedObject<RiskEngineConfiguration> | undefined> => {
|
||||
const savedObjectsResponse = await savedObjectsClient.find<RiskEngineConfiguration>({
|
||||
type: riskEngineConfigurationTypeName,
|
||||
});
|
||||
return savedObjectsResponse.saved_objects?.[0];
|
||||
};
|
||||
|
||||
export const updateSavedObjectAttribute = async ({
|
||||
savedObjectsClient,
|
||||
attributes,
|
||||
user,
|
||||
}: UpdateConfigOpts & {
|
||||
attributes: {
|
||||
enabled: boolean;
|
||||
};
|
||||
}) => {
|
||||
const savedObjectConfiguration = await getConfigurationSavedObject({
|
||||
savedObjectsClient,
|
||||
});
|
||||
|
||||
if (!savedObjectConfiguration) {
|
||||
throw new Error('There no saved object configuration for risk engine');
|
||||
}
|
||||
|
||||
const result = await savedObjectsClient.update(
|
||||
riskEngineConfigurationTypeName,
|
||||
savedObjectConfiguration.id,
|
||||
{
|
||||
...attributes,
|
||||
},
|
||||
{
|
||||
refresh: 'wait_for',
|
||||
}
|
||||
);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const initSavedObjects = async ({ savedObjectsClient, user }: UpdateConfigOpts) => {
|
||||
const configuration = await getConfigurationSavedObject({ savedObjectsClient });
|
||||
if (configuration) {
|
||||
return configuration;
|
||||
}
|
||||
const result = await savedObjectsClient.create(riskEngineConfigurationTypeName, {
|
||||
enabled: false,
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
export const getConfiguration = async ({
|
||||
savedObjectsClient,
|
||||
}: SavedObjectsClients): Promise<RiskEngineConfiguration | null> => {
|
||||
try {
|
||||
const savedObjectConfiguration = await getConfigurationSavedObject({
|
||||
savedObjectsClient,
|
||||
});
|
||||
const configuration = savedObjectConfiguration?.attributes;
|
||||
|
||||
if (configuration) {
|
||||
return configuration;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
};
|
|
@ -170,10 +170,6 @@ export class Plugin implements ISecuritySolutionPlugin {
|
|||
.then(([{ elasticsearch }]) => elasticsearch.client.asInternalUser),
|
||||
});
|
||||
|
||||
if (experimentalFeatures.riskScoringPersistence) {
|
||||
this.riskEngineDataClient.initializeResources({});
|
||||
}
|
||||
|
||||
const requestContextFactory = new RequestContextFactory({
|
||||
config,
|
||||
logger,
|
||||
|
|
|
@ -74,7 +74,13 @@ import { registerManageExceptionsRoutes } from '../lib/exceptions/api/register_r
|
|||
import { registerDashboardsRoutes } from '../lib/dashboards/routes';
|
||||
import { registerTagsRoutes } from '../lib/tags/routes';
|
||||
import { setAlertTagsRoute } from '../lib/detection_engine/routes/signals/set_alert_tags_route';
|
||||
import { riskScorePreviewRoute } from '../lib/risk_engine/routes';
|
||||
import {
|
||||
riskScorePreviewRoute,
|
||||
riskEngineDisableRoute,
|
||||
riskEngineInitRoute,
|
||||
riskEngineEnableRoute,
|
||||
riskEngineStatusRoute,
|
||||
} from '../lib/risk_engine/routes';
|
||||
import { riskScoreCalculationRoute } from '../lib/risk_engine/routes/risk_score_calculation_route';
|
||||
|
||||
export const initRoutes = (
|
||||
|
@ -177,5 +183,9 @@ export const initRoutes = (
|
|||
if (config.experimentalFeatures.riskScoringRoutesEnabled) {
|
||||
riskScorePreviewRoute(router, logger);
|
||||
riskScoreCalculationRoute(router, logger);
|
||||
riskEngineInitRoute(router, logger, security);
|
||||
riskEngineEnableRoute(router, logger, security);
|
||||
riskEngineStatusRoute(router, logger);
|
||||
riskEngineDisableRoute(router, logger, security);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -13,6 +13,7 @@ import { legacyType as legacyRuleActionsType } from './lib/detection_engine/rule
|
|||
import { prebuiltRuleAssetType } from './lib/detection_engine/prebuilt_rules';
|
||||
import { type as signalsMigrationType } from './lib/detection_engine/migrations/saved_objects';
|
||||
import { manifestType } from './endpoint/lib/artifacts/saved_object_mappings';
|
||||
import { riskEngineConfigurationType } from './lib/risk_engine/saved_object';
|
||||
|
||||
const types = [
|
||||
noteType,
|
||||
|
@ -22,6 +23,7 @@ const types = [
|
|||
timelineType,
|
||||
manifestType,
|
||||
signalsMigrationType,
|
||||
riskEngineConfigurationType,
|
||||
];
|
||||
|
||||
export const savedObjectTypes = types.map((type) => type.name);
|
||||
|
|
|
@ -37,7 +37,7 @@ export default ({ loadTestFile }: FtrProviderContext): void => {
|
|||
loadTestFile(require.resolve('./throttle'));
|
||||
loadTestFile(require.resolve('./ignore_fields'));
|
||||
loadTestFile(require.resolve('./migrations'));
|
||||
loadTestFile(require.resolve('./risk_engine/risk_engine_install_resources'));
|
||||
loadTestFile(require.resolve('./risk_engine/risk_engine_status'));
|
||||
loadTestFile(require.resolve('./risk_engine/risk_score_preview'));
|
||||
loadTestFile(require.resolve('./risk_engine/risk_score_calculation'));
|
||||
loadTestFile(require.resolve('./set_alert_tags'));
|
||||
|
|
|
@ -1,227 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../../common/ftr_provider_context';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default ({ getService }: FtrProviderContext) => {
|
||||
const es = getService('es');
|
||||
|
||||
describe('Risk Engine - Install Resources', () => {
|
||||
it('should install resources on startup', async () => {
|
||||
const ilmPolicyName = '.risk-score-ilm-policy';
|
||||
const componentTemplateName = '.risk-score-mappings';
|
||||
const indexTemplateName = '.risk-score.risk-score-default-index-template';
|
||||
const indexName = 'risk-score.risk-score-default';
|
||||
|
||||
const ilmPolicy = await es.ilm.getLifecycle({
|
||||
name: ilmPolicyName,
|
||||
});
|
||||
|
||||
expect(ilmPolicy[ilmPolicyName].policy).to.eql({
|
||||
_meta: {
|
||||
managed: true,
|
||||
},
|
||||
phases: {
|
||||
hot: {
|
||||
min_age: '0ms',
|
||||
actions: {
|
||||
rollover: {
|
||||
max_age: '30d',
|
||||
max_primary_shard_size: '50gb',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { component_templates: componentTemplates1 } = await es.cluster.getComponentTemplate({
|
||||
name: componentTemplateName,
|
||||
});
|
||||
|
||||
expect(componentTemplates1.length).to.eql(1);
|
||||
const componentTemplate = componentTemplates1[0];
|
||||
|
||||
expect(componentTemplate.name).to.eql(componentTemplateName);
|
||||
expect(componentTemplate.component_template.template.mappings).to.eql({
|
||||
dynamic: 'strict',
|
||||
properties: {
|
||||
'@timestamp': {
|
||||
type: 'date',
|
||||
},
|
||||
host: {
|
||||
properties: {
|
||||
name: {
|
||||
type: 'keyword',
|
||||
},
|
||||
risk: {
|
||||
properties: {
|
||||
calculated_level: {
|
||||
type: 'keyword',
|
||||
},
|
||||
calculated_score: {
|
||||
type: 'float',
|
||||
},
|
||||
calculated_score_norm: {
|
||||
type: 'float',
|
||||
},
|
||||
category_1_score: {
|
||||
type: 'float',
|
||||
},
|
||||
id_field: {
|
||||
type: 'keyword',
|
||||
},
|
||||
id_value: {
|
||||
type: 'keyword',
|
||||
},
|
||||
notes: {
|
||||
type: 'keyword',
|
||||
},
|
||||
inputs: {
|
||||
properties: {
|
||||
id: {
|
||||
type: 'keyword',
|
||||
},
|
||||
index: {
|
||||
type: 'keyword',
|
||||
},
|
||||
category: {
|
||||
type: 'keyword',
|
||||
},
|
||||
description: {
|
||||
type: 'keyword',
|
||||
},
|
||||
risk_score: {
|
||||
type: 'float',
|
||||
},
|
||||
timestamp: {
|
||||
type: 'date',
|
||||
},
|
||||
},
|
||||
type: 'object',
|
||||
},
|
||||
},
|
||||
type: 'object',
|
||||
},
|
||||
},
|
||||
},
|
||||
user: {
|
||||
properties: {
|
||||
name: {
|
||||
type: 'keyword',
|
||||
},
|
||||
risk: {
|
||||
properties: {
|
||||
calculated_level: {
|
||||
type: 'keyword',
|
||||
},
|
||||
calculated_score: {
|
||||
type: 'float',
|
||||
},
|
||||
calculated_score_norm: {
|
||||
type: 'float',
|
||||
},
|
||||
category_1_score: {
|
||||
type: 'float',
|
||||
},
|
||||
id_field: {
|
||||
type: 'keyword',
|
||||
},
|
||||
id_value: {
|
||||
type: 'keyword',
|
||||
},
|
||||
notes: {
|
||||
type: 'keyword',
|
||||
},
|
||||
inputs: {
|
||||
properties: {
|
||||
id: {
|
||||
type: 'keyword',
|
||||
},
|
||||
index: {
|
||||
type: 'keyword',
|
||||
},
|
||||
category: {
|
||||
type: 'keyword',
|
||||
},
|
||||
description: {
|
||||
type: 'keyword',
|
||||
},
|
||||
risk_score: {
|
||||
type: 'float',
|
||||
},
|
||||
timestamp: {
|
||||
type: 'date',
|
||||
},
|
||||
},
|
||||
type: 'object',
|
||||
},
|
||||
},
|
||||
type: 'object',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { index_templates: indexTemplates } = await es.indices.getIndexTemplate({
|
||||
name: indexTemplateName,
|
||||
});
|
||||
expect(indexTemplates.length).to.eql(1);
|
||||
const indexTemplate = indexTemplates[0];
|
||||
expect(indexTemplate.name).to.eql(indexTemplateName);
|
||||
expect(indexTemplate.index_template.index_patterns).to.eql(['risk-score.risk-score-default']);
|
||||
expect(indexTemplate.index_template.composed_of).to.eql(['.risk-score-mappings']);
|
||||
expect(indexTemplate.index_template.template!.mappings?.dynamic).to.eql(false);
|
||||
expect(indexTemplate.index_template.template!.mappings?._meta?.managed).to.eql(true);
|
||||
expect(indexTemplate.index_template.template!.mappings?._meta?.namespace).to.eql('default');
|
||||
expect(indexTemplate.index_template.template!.mappings?._meta?.kibana?.version).to.be.a(
|
||||
'string'
|
||||
);
|
||||
expect(indexTemplate.index_template.template!.settings).to.eql({
|
||||
index: {
|
||||
lifecycle: {
|
||||
name: '.risk-score-ilm-policy',
|
||||
},
|
||||
mapping: {
|
||||
total_fields: {
|
||||
limit: '1000',
|
||||
},
|
||||
},
|
||||
hidden: 'true',
|
||||
auto_expand_replicas: '0-1',
|
||||
},
|
||||
});
|
||||
|
||||
const dsResponse = await es.indices.get({
|
||||
index: indexName,
|
||||
});
|
||||
|
||||
const dataStream = Object.values(dsResponse).find((ds) => ds.data_stream === indexName);
|
||||
|
||||
expect(dataStream?.mappings?._meta?.managed).to.eql(true);
|
||||
expect(dataStream?.mappings?._meta?.namespace).to.eql('default');
|
||||
expect(dataStream?.mappings?._meta?.kibana?.version).to.be.a('string');
|
||||
expect(dataStream?.mappings?.dynamic).to.eql('false');
|
||||
|
||||
expect(dataStream?.settings?.index?.lifecycle).to.eql({
|
||||
name: '.risk-score-ilm-policy',
|
||||
});
|
||||
|
||||
expect(dataStream?.settings?.index?.mapping).to.eql({
|
||||
total_fields: {
|
||||
limit: '1000',
|
||||
},
|
||||
});
|
||||
|
||||
expect(dataStream?.settings?.index?.hidden).to.eql('true');
|
||||
expect(dataStream?.settings?.index?.number_of_shards).to.eql(1);
|
||||
expect(dataStream?.settings?.index?.auto_expand_replicas).to.eql('0-1');
|
||||
});
|
||||
});
|
||||
};
|
|
@ -0,0 +1,389 @@
|
|||
/*
|
||||
* 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 {
|
||||
RISK_ENGINE_INIT_URL,
|
||||
RISK_ENGINE_DISABLE_URL,
|
||||
RISK_ENGINE_ENABLE_URL,
|
||||
RISK_ENGINE_STATUS_URL,
|
||||
} from '@kbn/security-solution-plugin/common/constants';
|
||||
import { riskEngineConfigurationTypeName } from '@kbn/security-solution-plugin/server/lib/risk_engine/saved_object';
|
||||
import { FtrProviderContext } from '../../../common/ftr_provider_context';
|
||||
import {
|
||||
cleanRiskEngineConfig,
|
||||
legacyTransformIds,
|
||||
createTransforms,
|
||||
clearLegacyTransforms,
|
||||
} from './utils';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default ({ getService }: FtrProviderContext) => {
|
||||
const es = getService('es');
|
||||
const supertest = getService('supertest');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
|
||||
describe('Risk Engine', () => {
|
||||
afterEach(async () => {
|
||||
await cleanRiskEngineConfig({
|
||||
kibanaServer,
|
||||
});
|
||||
await clearLegacyTransforms({
|
||||
es,
|
||||
});
|
||||
});
|
||||
|
||||
const initRiskEngine = async () =>
|
||||
await supertest.post(RISK_ENGINE_INIT_URL).set('kbn-xsrf', 'true').send().expect(200);
|
||||
|
||||
const getRiskEngineStatus = async () =>
|
||||
await supertest.get(RISK_ENGINE_STATUS_URL).set('kbn-xsrf', 'true').send().expect(200);
|
||||
|
||||
const enableRiskEngine = async () =>
|
||||
await supertest.post(RISK_ENGINE_ENABLE_URL).set('kbn-xsrf', 'true').send().expect(200);
|
||||
|
||||
const disableRiskEngine = async () =>
|
||||
await supertest.post(RISK_ENGINE_DISABLE_URL).set('kbn-xsrf', 'true').send().expect(200);
|
||||
|
||||
describe('init api', () => {
|
||||
it('should return response with success status', async () => {
|
||||
const response = await initRiskEngine();
|
||||
expect(response.body).to.eql({
|
||||
result: {
|
||||
errors: [],
|
||||
legacy_risk_engine_disabled: true,
|
||||
risk_engine_configuration_created: true,
|
||||
risk_engine_enabled: true,
|
||||
risk_engine_resources_installed: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should install resources on init call', async () => {
|
||||
const ilmPolicyName = '.risk-score-ilm-policy';
|
||||
const componentTemplateName = '.risk-score-mappings';
|
||||
const indexTemplateName = '.risk-score.risk-score-default-index-template';
|
||||
const indexName = 'risk-score.risk-score-default';
|
||||
|
||||
await initRiskEngine();
|
||||
|
||||
const ilmPolicy = await es.ilm.getLifecycle({
|
||||
name: ilmPolicyName,
|
||||
});
|
||||
|
||||
expect(ilmPolicy[ilmPolicyName].policy).to.eql({
|
||||
_meta: {
|
||||
managed: true,
|
||||
},
|
||||
phases: {
|
||||
hot: {
|
||||
min_age: '0ms',
|
||||
actions: {
|
||||
rollover: {
|
||||
max_age: '30d',
|
||||
max_primary_shard_size: '50gb',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { component_templates: componentTemplates1 } = await es.cluster.getComponentTemplate({
|
||||
name: componentTemplateName,
|
||||
});
|
||||
|
||||
expect(componentTemplates1.length).to.eql(1);
|
||||
const componentTemplate = componentTemplates1[0];
|
||||
|
||||
expect(componentTemplate.name).to.eql(componentTemplateName);
|
||||
expect(componentTemplate.component_template.template.mappings).to.eql({
|
||||
dynamic: 'strict',
|
||||
properties: {
|
||||
'@timestamp': {
|
||||
type: 'date',
|
||||
},
|
||||
host: {
|
||||
properties: {
|
||||
name: {
|
||||
type: 'keyword',
|
||||
},
|
||||
risk: {
|
||||
properties: {
|
||||
calculated_level: {
|
||||
type: 'keyword',
|
||||
},
|
||||
calculated_score: {
|
||||
type: 'float',
|
||||
},
|
||||
calculated_score_norm: {
|
||||
type: 'float',
|
||||
},
|
||||
category_1_score: {
|
||||
type: 'float',
|
||||
},
|
||||
id_field: {
|
||||
type: 'keyword',
|
||||
},
|
||||
id_value: {
|
||||
type: 'keyword',
|
||||
},
|
||||
notes: {
|
||||
type: 'keyword',
|
||||
},
|
||||
inputs: {
|
||||
properties: {
|
||||
id: {
|
||||
type: 'keyword',
|
||||
},
|
||||
index: {
|
||||
type: 'keyword',
|
||||
},
|
||||
category: {
|
||||
type: 'keyword',
|
||||
},
|
||||
description: {
|
||||
type: 'keyword',
|
||||
},
|
||||
risk_score: {
|
||||
type: 'float',
|
||||
},
|
||||
timestamp: {
|
||||
type: 'date',
|
||||
},
|
||||
},
|
||||
type: 'object',
|
||||
},
|
||||
},
|
||||
type: 'object',
|
||||
},
|
||||
},
|
||||
},
|
||||
user: {
|
||||
properties: {
|
||||
name: {
|
||||
type: 'keyword',
|
||||
},
|
||||
risk: {
|
||||
properties: {
|
||||
calculated_level: {
|
||||
type: 'keyword',
|
||||
},
|
||||
calculated_score: {
|
||||
type: 'float',
|
||||
},
|
||||
calculated_score_norm: {
|
||||
type: 'float',
|
||||
},
|
||||
category_1_score: {
|
||||
type: 'float',
|
||||
},
|
||||
id_field: {
|
||||
type: 'keyword',
|
||||
},
|
||||
id_value: {
|
||||
type: 'keyword',
|
||||
},
|
||||
notes: {
|
||||
type: 'keyword',
|
||||
},
|
||||
inputs: {
|
||||
properties: {
|
||||
id: {
|
||||
type: 'keyword',
|
||||
},
|
||||
index: {
|
||||
type: 'keyword',
|
||||
},
|
||||
category: {
|
||||
type: 'keyword',
|
||||
},
|
||||
description: {
|
||||
type: 'keyword',
|
||||
},
|
||||
risk_score: {
|
||||
type: 'float',
|
||||
},
|
||||
timestamp: {
|
||||
type: 'date',
|
||||
},
|
||||
},
|
||||
type: 'object',
|
||||
},
|
||||
},
|
||||
type: 'object',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { index_templates: indexTemplates } = await es.indices.getIndexTemplate({
|
||||
name: indexTemplateName,
|
||||
});
|
||||
expect(indexTemplates.length).to.eql(1);
|
||||
const indexTemplate = indexTemplates[0];
|
||||
expect(indexTemplate.name).to.eql(indexTemplateName);
|
||||
expect(indexTemplate.index_template.index_patterns).to.eql([
|
||||
'risk-score.risk-score-default',
|
||||
]);
|
||||
expect(indexTemplate.index_template.composed_of).to.eql(['.risk-score-mappings']);
|
||||
expect(indexTemplate.index_template.template!.mappings?.dynamic).to.eql(false);
|
||||
expect(indexTemplate.index_template.template!.mappings?._meta?.managed).to.eql(true);
|
||||
expect(indexTemplate.index_template.template!.mappings?._meta?.namespace).to.eql('default');
|
||||
expect(indexTemplate.index_template.template!.mappings?._meta?.kibana?.version).to.be.a(
|
||||
'string'
|
||||
);
|
||||
expect(indexTemplate.index_template.template!.settings).to.eql({
|
||||
index: {
|
||||
lifecycle: {
|
||||
name: '.risk-score-ilm-policy',
|
||||
},
|
||||
mapping: {
|
||||
total_fields: {
|
||||
limit: '1000',
|
||||
},
|
||||
},
|
||||
hidden: 'true',
|
||||
auto_expand_replicas: '0-1',
|
||||
},
|
||||
});
|
||||
|
||||
const dsResponse = await es.indices.get({
|
||||
index: indexName,
|
||||
});
|
||||
|
||||
const dataStream = Object.values(dsResponse).find((ds) => ds.data_stream === indexName);
|
||||
|
||||
expect(dataStream?.mappings?._meta?.managed).to.eql(true);
|
||||
expect(dataStream?.mappings?._meta?.namespace).to.eql('default');
|
||||
expect(dataStream?.mappings?._meta?.kibana?.version).to.be.a('string');
|
||||
expect(dataStream?.mappings?.dynamic).to.eql('false');
|
||||
|
||||
expect(dataStream?.settings?.index?.lifecycle).to.eql({
|
||||
name: '.risk-score-ilm-policy',
|
||||
});
|
||||
|
||||
expect(dataStream?.settings?.index?.mapping).to.eql({
|
||||
total_fields: {
|
||||
limit: '1000',
|
||||
},
|
||||
});
|
||||
|
||||
expect(dataStream?.settings?.index?.hidden).to.eql('true');
|
||||
expect(dataStream?.settings?.index?.number_of_shards).to.eql(1);
|
||||
expect(dataStream?.settings?.index?.auto_expand_replicas).to.eql('0-1');
|
||||
});
|
||||
|
||||
it('should create configuration saved object', async () => {
|
||||
await initRiskEngine();
|
||||
const response = await kibanaServer.savedObjects.find({
|
||||
type: riskEngineConfigurationTypeName,
|
||||
});
|
||||
|
||||
expect(response?.saved_objects?.[0]?.attributes).to.eql({
|
||||
enabled: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should create configuration saved object only once', async () => {
|
||||
await initRiskEngine();
|
||||
const firstResponse = await kibanaServer.savedObjects.find({
|
||||
type: riskEngineConfigurationTypeName,
|
||||
});
|
||||
|
||||
await initRiskEngine();
|
||||
const secondResponse = await kibanaServer.savedObjects.find({
|
||||
type: riskEngineConfigurationTypeName,
|
||||
});
|
||||
|
||||
expect(secondResponse?.saved_objects?.length).to.eql(1);
|
||||
expect(secondResponse?.saved_objects?.[0]?.id).to.eql(
|
||||
firstResponse?.saved_objects?.[0]?.id
|
||||
);
|
||||
});
|
||||
|
||||
it('should remove legacy risk score transform if it exists', async () => {
|
||||
await createTransforms({ es });
|
||||
|
||||
for (const transformId of legacyTransformIds) {
|
||||
const tr = await es.transform.getTransform({
|
||||
transform_id: transformId,
|
||||
});
|
||||
|
||||
expect(tr?.transforms?.[0]?.id).to.eql(transformId);
|
||||
}
|
||||
|
||||
await initRiskEngine();
|
||||
|
||||
for (const transformId of legacyTransformIds) {
|
||||
try {
|
||||
await es.transform.getTransform({
|
||||
transform_id: transformId,
|
||||
});
|
||||
} catch (err) {
|
||||
expect(err).to.not.be(undefined);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('status api', () => {
|
||||
it('should disable / enable risk engige', async () => {
|
||||
const status1 = await getRiskEngineStatus();
|
||||
|
||||
expect(status1.body).to.eql({
|
||||
risk_engine_status: 'NOT_INSTALLED',
|
||||
legacy_risk_engine_status: 'NOT_INSTALLED',
|
||||
});
|
||||
|
||||
await initRiskEngine();
|
||||
|
||||
const status2 = await getRiskEngineStatus();
|
||||
|
||||
expect(status2.body).to.eql({
|
||||
risk_engine_status: 'ENABLED',
|
||||
legacy_risk_engine_status: 'NOT_INSTALLED',
|
||||
});
|
||||
|
||||
await disableRiskEngine();
|
||||
const status3 = await getRiskEngineStatus();
|
||||
|
||||
expect(status3.body).to.eql({
|
||||
risk_engine_status: 'DISABLED',
|
||||
legacy_risk_engine_status: 'NOT_INSTALLED',
|
||||
});
|
||||
|
||||
await enableRiskEngine();
|
||||
const status4 = await getRiskEngineStatus();
|
||||
|
||||
expect(status4.body).to.eql({
|
||||
risk_engine_status: 'ENABLED',
|
||||
legacy_risk_engine_status: 'NOT_INSTALLED',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return status of legacy risk engine', async () => {
|
||||
await createTransforms({ es });
|
||||
const status1 = await getRiskEngineStatus();
|
||||
|
||||
expect(status1.body).to.eql({
|
||||
risk_engine_status: 'NOT_INSTALLED',
|
||||
legacy_risk_engine_status: 'ENABLED',
|
||||
});
|
||||
|
||||
await initRiskEngine();
|
||||
|
||||
const status2 = await getRiskEngineStatus();
|
||||
|
||||
expect(status2.body).to.eql({
|
||||
risk_engine_status: 'ENABLED',
|
||||
legacy_risk_engine_status: 'NOT_INSTALLED',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
|
@ -13,7 +13,8 @@ import type {
|
|||
EcsRiskScore,
|
||||
RiskScore,
|
||||
} from '@kbn/security-solution-plugin/server/lib/risk_engine/types';
|
||||
|
||||
import { riskEngineConfigurationTypeName } from '@kbn/security-solution-plugin/server/lib/risk_engine/saved_object';
|
||||
import type { KbnClient } from '@kbn/test';
|
||||
import {
|
||||
createRule,
|
||||
waitForSignalsToBePresent,
|
||||
|
@ -136,3 +137,78 @@ export const waitForRiskScoresToBePresent = async (
|
|||
log
|
||||
);
|
||||
};
|
||||
|
||||
export const getRiskEngineConfigSO = async ({ kibanaServer }: { kibanaServer: KbnClient }) => {
|
||||
const soResponse = await kibanaServer.savedObjects.find({
|
||||
type: riskEngineConfigurationTypeName,
|
||||
});
|
||||
|
||||
return soResponse?.saved_objects?.[0];
|
||||
};
|
||||
|
||||
export const cleanRiskEngineConfig = async ({
|
||||
kibanaServer,
|
||||
}: {
|
||||
kibanaServer: KbnClient;
|
||||
}): Promise<void> => {
|
||||
const so = await getRiskEngineConfigSO({ kibanaServer });
|
||||
if (so) {
|
||||
await kibanaServer.savedObjects.delete({
|
||||
type: riskEngineConfigurationTypeName,
|
||||
id: so.id,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const legacyTransformIds = [
|
||||
'ml_hostriskscore_pivot_transform_default',
|
||||
'ml_hostriskscore_latest_transform_default',
|
||||
'ml_userriskscore_pivot_transform_default',
|
||||
'ml_userriskscore_latest_transform_default',
|
||||
];
|
||||
|
||||
export const clearLegacyTransforms = async ({ es }: { es: Client }): Promise<void> => {
|
||||
const transforms = legacyTransformIds.map((transform) =>
|
||||
es.transform.deleteTransform({
|
||||
transform_id: transform,
|
||||
})
|
||||
);
|
||||
try {
|
||||
await Promise.all(transforms);
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
};
|
||||
|
||||
export const createTransforms = async ({ es }: { es: Client }): Promise<void> => {
|
||||
const transforms = legacyTransformIds.map((transform) =>
|
||||
es.transform.putTransform({
|
||||
transform_id: transform,
|
||||
source: {
|
||||
index: ['.alerts-security.alerts-default'],
|
||||
},
|
||||
dest: {
|
||||
index: 'ml_host_risk_score_default',
|
||||
},
|
||||
pivot: {
|
||||
group_by: {
|
||||
'host.name': {
|
||||
terms: {
|
||||
field: 'host.name',
|
||||
},
|
||||
},
|
||||
},
|
||||
aggregations: {
|
||||
'@timestamp': {
|
||||
max: {
|
||||
field: '@timestamp',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
settings: {},
|
||||
})
|
||||
);
|
||||
|
||||
await Promise.all(transforms);
|
||||
};
|
||||
|
|
|
@ -49,7 +49,6 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
|
|||
`--xpack.securitySolution.enableExperimental=${JSON.stringify([
|
||||
'alertDetailsPageEnabled',
|
||||
'chartEmbeddablesEnabled',
|
||||
'riskScoringRoutesEnabled',
|
||||
])}`,
|
||||
// mock cloud to enable the guided onboarding tour in e2e tests
|
||||
'--xpack.cloud.id=test',
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue