Risk score installation refactory (#142434)

* init

* start transforms

* error handling

* onboarding

* rename

* restart transforms

* cypress

* update unit tests

* update cypress task

* add toastLifeTime

* fix types

* fix i18n

* i18n

* update cypress

* update comment

* Update x-pack/plugins/security_solution/server/lib/risk_score/transform/restart_transform.ts

Co-authored-by: Pablo Machado <pablo.nevesmachado@elastic.co>

* Update x-pack/plugins/security_solution/public/risk_score/containers/onboarding/api/onboarding.ts

Co-authored-by: Pablo Machado <pablo.nevesmachado@elastic.co>

* review

* fix types

* review

* review

* fix types

* [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix'

* fix types

* cypress

* update wording

* update unit tests

* update cypress

* update cypress

* [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix'

Co-authored-by: Pablo Machado <pablo.nevesmachado@elastic.co>
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Angela Chuang 2022-10-05 21:48:19 +01:00 committed by GitHub
parent 35879b9006
commit 0a39572938
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
52 changed files with 3103 additions and 699 deletions

View file

@ -281,6 +281,7 @@ export const DETECTION_ENGINE_RULES_BULK_UPDATE =
`${DETECTION_ENGINE_RULES_URL}/_bulk_update` as const;
export const INTERNAL_RISK_SCORE_URL = '/internal/risk_score' as const;
export const RISK_SCORE_RESTART_TRANSFORMS = `${INTERNAL_RISK_SCORE_URL}/transforms/restart`;
export const DEV_TOOL_PREBUILT_CONTENT =
`${INTERNAL_RISK_SCORE_URL}/prebuilt_content/dev_tool/{console_id}` as const;
export const devToolPrebuiltContentUrl = (spaceId: string, consoleId: string) =>
@ -295,7 +296,6 @@ export const RISK_SCORE_CREATE_INDEX = `${INTERNAL_RISK_SCORE_URL}/indices/creat
export const RISK_SCORE_DELETE_INDICES = `${INTERNAL_RISK_SCORE_URL}/indices/delete`;
export const RISK_SCORE_CREATE_STORED_SCRIPT = `${INTERNAL_RISK_SCORE_URL}/stored_scripts/create`;
export const RISK_SCORE_DELETE_STORED_SCRIPT = `${INTERNAL_RISK_SCORE_URL}/stored_scripts/delete`;
/**
* Internal detection engine routes
*/

View file

@ -0,0 +1,27 @@
/*
* 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 interface ESProcessorConfig {
on_failure?: Processor[];
ignore_failure?: boolean;
if?: string;
tag?: string;
[key: string]: unknown;
}
export interface Processor {
[typeName: string]: ESProcessorConfig;
}
export interface Pipeline {
name: string;
description?: string;
version?: number;
processors: string | Processor[];
on_failure?: Processor[];
isManaged?: boolean;
}

View file

@ -7,6 +7,7 @@
import { DEFAULT_ALERTS_INDEX } from '../constants';
import { RiskScoreEntity, RiskScoreFields } from '../search_strategy';
import type { Pipeline, Processor } from '../types/risk_scores';
/**
* * Since 8.5, all the transforms, scripts,
@ -203,8 +204,8 @@ export const getRiskScoreIngestPipelineOptions = (
riskScoreEntity: RiskScoreEntity,
spaceId = 'default',
stringifyScript?: boolean
) => {
const processors = [
): Pipeline => {
const processors: Processor[] = [
{
set: {
field: 'ingest_timestamp',
@ -301,7 +302,6 @@ export const getCreateRiskScoreIndicesOptions = ({
};
};
/**
/**
* This should be aligned with
* console_templates/enable_user_risk_score.console step 8

View file

@ -0,0 +1,122 @@
/*
* 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 { getNewRule } from '../../objects/rule';
import {
ENABLE_HOST_RISK_SCORE_BUTTON,
ENABLE_USER_RISK_SCORE_BUTTON,
RISK_SCORE_DASHBOARDS_INSTALLATION_SUCCESS_TOAST,
RISK_SCORE_INSTALLATION_SUCCESS_TOAST,
} from '../../screens/entity_analytics';
import {
deleteRiskScore,
intercepInstallRiskScoreModule,
waitForInstallRiskScoreModule,
} from '../../tasks/api_calls/risk_scores';
import { findSavedObjects } from '../../tasks/api_calls/risk_scores/saved_objects';
import { createCustomRuleEnabled } from '../../tasks/api_calls/rules';
import { cleanKibana } from '../../tasks/common';
import { login, visit } from '../../tasks/login';
import { clickEnableRiskScore } from '../../tasks/risk_scores';
import { RiskScoreEntity } from '../../tasks/risk_scores/common';
import {
getRiskScoreLatestTransformId,
getRiskScorePivotTransformId,
getTransformState,
} from '../../tasks/risk_scores/transforms';
import { ENTITY_ANALYTICS_URL } from '../../urls/navigation';
const spaceId = 'default';
describe('Enable risk scores', () => {
before(() => {
cleanKibana();
login();
createCustomRuleEnabled(getNewRule(), 'rule1');
});
beforeEach(() => {
deleteRiskScore({ riskScoreEntity: RiskScoreEntity.host, spaceId, deleteAll: true });
deleteRiskScore({ riskScoreEntity: RiskScoreEntity.user, spaceId, deleteAll: true });
visit(ENTITY_ANALYTICS_URL);
});
afterEach(() => {
deleteRiskScore({ riskScoreEntity: RiskScoreEntity.host, spaceId, deleteAll: true });
deleteRiskScore({ riskScoreEntity: RiskScoreEntity.user, spaceId, deleteAll: true });
});
it('shows enable host risk button', () => {
cy.get(ENABLE_HOST_RISK_SCORE_BUTTON).should('exist');
});
it('should install host risk score successfully', () => {
intercepInstallRiskScoreModule();
clickEnableRiskScore(RiskScoreEntity.host);
waitForInstallRiskScoreModule();
cy.get(ENABLE_HOST_RISK_SCORE_BUTTON).should('be.disabled');
cy.get(RISK_SCORE_INSTALLATION_SUCCESS_TOAST(RiskScoreEntity.host)).should('exist');
cy.get(RISK_SCORE_DASHBOARDS_INSTALLATION_SUCCESS_TOAST(RiskScoreEntity.host)).should('exist');
cy.get(ENABLE_HOST_RISK_SCORE_BUTTON).should('not.exist');
getTransformState(getRiskScorePivotTransformId(RiskScoreEntity.host, spaceId)).then((res) => {
expect(res.status).to.eq(200);
expect(res.body.transforms[0].id).to.eq(
getRiskScorePivotTransformId(RiskScoreEntity.host, spaceId)
);
expect(res.body.transforms[0].state).to.eq('started');
});
getTransformState(getRiskScoreLatestTransformId(RiskScoreEntity.host, spaceId)).then((res) => {
expect(res.status).to.eq(200);
expect(res.body.transforms[0].id).to.eq(
getRiskScoreLatestTransformId(RiskScoreEntity.host, spaceId)
);
expect(res.body.transforms[0].state).to.eq('started');
});
findSavedObjects(RiskScoreEntity.host, spaceId).then((res) => {
expect(res.status).to.eq(200);
expect(res.body.saved_objects.length).to.eq(11);
});
});
it('shows enable user risk button', () => {
cy.get(ENABLE_USER_RISK_SCORE_BUTTON).should('exist');
});
it('should install user risk score successfully', () => {
intercepInstallRiskScoreModule();
clickEnableRiskScore(RiskScoreEntity.user);
waitForInstallRiskScoreModule();
cy.get(ENABLE_USER_RISK_SCORE_BUTTON).should('be.disabled');
cy.get(RISK_SCORE_INSTALLATION_SUCCESS_TOAST(RiskScoreEntity.user)).should('exist');
cy.get(RISK_SCORE_DASHBOARDS_INSTALLATION_SUCCESS_TOAST(RiskScoreEntity.user)).should('exist');
cy.get(ENABLE_USER_RISK_SCORE_BUTTON).should('not.exist');
getTransformState(getRiskScorePivotTransformId(RiskScoreEntity.user, spaceId)).then((res) => {
expect(res.status).to.eq(200);
expect(res.body.transforms[0].id).to.eq(
getRiskScorePivotTransformId(RiskScoreEntity.user, spaceId)
);
expect(res.body.transforms[0].state).to.eq('started');
});
getTransformState(getRiskScoreLatestTransformId(RiskScoreEntity.user, spaceId)).then((res) => {
expect(res.status).to.eq(200);
expect(res.body.transforms[0].id).to.eq(
getRiskScoreLatestTransformId(RiskScoreEntity.user, spaceId)
);
expect(res.body.transforms[0].state).to.eq('started');
});
findSavedObjects(RiskScoreEntity.user, spaceId).then((res) => {
expect(res.status).to.eq(200);
expect(res.body.saved_objects.length).to.eq(11);
});
});
});

View file

@ -0,0 +1,165 @@
/*
* 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 { getNewRule } from '../../objects/rule';
import {
RISK_SCORE_INSTALLATION_SUCCESS_TOAST,
UPGRADE_HOST_RISK_SCORE_BUTTON,
UPGRADE_USER_RISK_SCORE_BUTTON,
UPGRADE_CANCELLATION_BUTTON,
UPGRADE_CONFIRMARION_MODAL,
RISK_SCORE_DASHBOARDS_INSTALLATION_SUCCESS_TOAST,
} from '../../screens/entity_analytics';
import { deleteRiskScore, installLegacyRiskScoreModule } from '../../tasks/api_calls/risk_scores';
import { findSavedObjects } from '../../tasks/api_calls/risk_scores/saved_objects';
import { createCustomRuleEnabled } from '../../tasks/api_calls/rules';
import { cleanKibana } from '../../tasks/common';
import { login, visit } from '../../tasks/login';
import {
clickUpgradeRiskScore,
clickUpgradeRiskScoreConfirmed,
interceptUpgradeRiskScoreModule,
waitForUpgradeRiskScoreModule,
} from '../../tasks/risk_scores';
import { RiskScoreEntity } from '../../tasks/risk_scores/common';
import {
getRiskScoreLatestTransformId,
getRiskScorePivotTransformId,
getTransformState,
} from '../../tasks/risk_scores/transforms';
import { ENTITY_ANALYTICS_URL } from '../../urls/navigation';
const spaceId = 'default';
describe('Upgrade risk scores', () => {
before(() => {
cleanKibana();
login();
createCustomRuleEnabled(getNewRule(), 'rule1');
});
beforeEach(() => {
deleteRiskScore({ riskScoreEntity: RiskScoreEntity.host, spaceId, deleteAll: true });
deleteRiskScore({ riskScoreEntity: RiskScoreEntity.user, spaceId, deleteAll: true });
installLegacyRiskScoreModule(RiskScoreEntity.host, spaceId);
installLegacyRiskScoreModule(RiskScoreEntity.user, spaceId);
visit(ENTITY_ANALYTICS_URL);
});
afterEach(() => {
deleteRiskScore({ riskScoreEntity: RiskScoreEntity.host, spaceId, deleteAll: true });
deleteRiskScore({ riskScoreEntity: RiskScoreEntity.user, spaceId, deleteAll: true });
});
it('shows upgrade host risk button', () => {
cy.get(UPGRADE_HOST_RISK_SCORE_BUTTON).should('be.visible');
});
it('should show a confirmation modal for upgrading host risk score', () => {
clickUpgradeRiskScore(RiskScoreEntity.host);
cy.get(UPGRADE_CONFIRMARION_MODAL(RiskScoreEntity.host)).should('exist');
});
it('display a link to host risk score Elastic doc', () => {
clickUpgradeRiskScore(RiskScoreEntity.host);
cy.get(UPGRADE_CANCELLATION_BUTTON)
.get(`${UPGRADE_CONFIRMARION_MODAL(RiskScoreEntity.host)} a`)
.then((link) => {
expect(link.prop('href')).to.eql(
`https://www.elastic.co/guide/en/security/current/${RiskScoreEntity.host}-risk-score.html`
);
});
});
it('should upgrade host risk score successfully', () => {
clickUpgradeRiskScore(RiskScoreEntity.host);
interceptUpgradeRiskScoreModule(RiskScoreEntity.host);
clickUpgradeRiskScoreConfirmed();
waitForUpgradeRiskScoreModule();
cy.get(UPGRADE_HOST_RISK_SCORE_BUTTON).should('be.disabled');
cy.get(RISK_SCORE_INSTALLATION_SUCCESS_TOAST(RiskScoreEntity.host)).should('exist');
cy.get(RISK_SCORE_DASHBOARDS_INSTALLATION_SUCCESS_TOAST(RiskScoreEntity.host)).should('exist');
cy.get(UPGRADE_HOST_RISK_SCORE_BUTTON).should('not.exist');
getTransformState(getRiskScorePivotTransformId(RiskScoreEntity.host, spaceId)).then((res) => {
expect(res.status).to.eq(200);
expect(res.body.transforms[0].id).to.eq(
getRiskScorePivotTransformId(RiskScoreEntity.host, spaceId)
);
expect(res.body.transforms[0].state).to.eq('started');
});
getTransformState(getRiskScoreLatestTransformId(RiskScoreEntity.host, spaceId)).then((res) => {
expect(res.status).to.eq(200);
expect(res.body.transforms[0].id).to.eq(
getRiskScoreLatestTransformId(RiskScoreEntity.host, spaceId)
);
expect(res.body.transforms[0].state).to.eq('started');
});
findSavedObjects(RiskScoreEntity.host, spaceId).then((res) => {
expect(res.status).to.eq(200);
expect(res.body.saved_objects.length).to.eq(11);
});
});
it('shows upgrade user risk button', () => {
cy.get(UPGRADE_USER_RISK_SCORE_BUTTON).should('be.visible');
});
it('should show a confirmation modal for upgrading user risk score', () => {
clickUpgradeRiskScore(RiskScoreEntity.user);
cy.get(UPGRADE_CONFIRMARION_MODAL(RiskScoreEntity.user)).should('exist');
});
it('display a link to user risk score Elastic doc', () => {
clickUpgradeRiskScore(RiskScoreEntity.user);
cy.get(UPGRADE_CANCELLATION_BUTTON)
.get(`${UPGRADE_CONFIRMARION_MODAL(RiskScoreEntity.user)} a`)
.then((link) => {
expect(link.prop('href')).to.eql(
`https://www.elastic.co/guide/en/security/current/${RiskScoreEntity.user}-risk-score.html`
);
});
});
it('should upgrade user risk score successfully', () => {
clickUpgradeRiskScore(RiskScoreEntity.user);
interceptUpgradeRiskScoreModule(RiskScoreEntity.user);
clickUpgradeRiskScoreConfirmed();
waitForUpgradeRiskScoreModule();
cy.get(UPGRADE_USER_RISK_SCORE_BUTTON).should('be.disabled');
cy.get(RISK_SCORE_INSTALLATION_SUCCESS_TOAST(RiskScoreEntity.user)).should('exist');
cy.get(RISK_SCORE_DASHBOARDS_INSTALLATION_SUCCESS_TOAST(RiskScoreEntity.user)).should('exist');
cy.get(UPGRADE_USER_RISK_SCORE_BUTTON).should('not.exist');
getTransformState(getRiskScorePivotTransformId(RiskScoreEntity.user, spaceId)).then((res) => {
expect(res.status).to.eq(200);
expect(res.body.transforms[0].id).to.eq(
getRiskScorePivotTransformId(RiskScoreEntity.user, spaceId)
);
expect(res.body.transforms[0].state).to.eq('started');
});
getTransformState(getRiskScoreLatestTransformId(RiskScoreEntity.user, spaceId)).then((res) => {
expect(res.status).to.eq(200);
expect(res.body.transforms[0].id).to.eq(
getRiskScoreLatestTransformId(RiskScoreEntity.user, spaceId)
);
expect(res.body.transforms[0].state).to.eq('started');
});
findSavedObjects(RiskScoreEntity.user, spaceId).then((res) => {
expect(res.status).to.eq(200);
expect(res.body.saved_objects.length).to.eq(11);
});
});
});

View file

@ -5,6 +5,8 @@
* 2.0.
*/
import type { RiskScoreEntity } from '../tasks/risk_scores/common';
export const ENABLE_HOST_RISK_SCORE_BUTTON = '[data-test-subj="enable_host_risk_score"]';
export const UPGRADE_HOST_RISK_SCORE_BUTTON = '[data-test-subj="host-risk-score-upgrade"]';
@ -17,6 +19,13 @@ export const HOST_RISK_SCORE_NO_DATA_DETECTED =
export const USER_RISK_SCORE_NO_DATA_DETECTED =
'[data-test-subj="user-risk-score-no-data-detected"]';
export const RISK_SCORE_DASHBOARDS_INSTALLATION_SUCCESS_TOAST = (
riskScoreEntity: RiskScoreEntity
) => `[data-test-subj="${riskScoreEntity}RiskScoreDashboardsSuccessToast"]`;
export const RISK_SCORE_INSTALLATION_SUCCESS_TOAST = (riskScoreEntity: RiskScoreEntity) =>
`[data-test-subj="${riskScoreEntity}EnableSuccessToast"]`;
export const HOSTS_DONUT_CHART =
'[data-test-subj="entity_analytics_hosts"] [data-test-subj="donut-chart"]';
@ -37,3 +46,10 @@ export const ANOMALIES_TABLE =
'[data-test-subj="entity_analytics_anomalies"] #entityAnalyticsDashboardAnomaliesTable';
export const ANOMALIES_TABLE_ROWS = '[data-test-subj="entity_analytics_anomalies"] .euiTableRow';
export const UPGRADE_CONFIRMARION_MODAL = (riskScoreEntity: RiskScoreEntity) =>
`[data-test-subj="${riskScoreEntity}-risk-score-upgrade-confirmation-modal"]`;
export const UPGRADE_CONFIRMATION_BUTTON = '[data-test-subj="confirmModalConfirmButton"]';
export const UPGRADE_CANCELLATION_BUTTON = '[data-test-subj="confirmModalCancelButton"]';

View file

@ -0,0 +1,290 @@
/*
* 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 { ENTITY_ANALYTICS_URL } from '../../../urls/navigation';
import { RISK_SCORE_URL } from '../../../urls/risk_score';
import { visit } from '../../login';
import { RiskScoreEntity } from '../../risk_scores/common';
import {
getCreateLegacyRiskScoreIndicesOptions,
getCreateLegacyRiskScoreLatestIndicesOptions,
} from '../../risk_scores/indices';
import {
getIngestPipelineName,
getLegacyIngestPipelineName,
getLegacyRiskScoreIngestPipelineOptions,
} from '../../risk_scores/ingest_pipelines';
import {
getLegacyRiskHostCreateInitScriptOptions,
getLegacyRiskHostCreateLevelScriptOptions,
getLegacyRiskHostCreateMapScriptOptions,
getLegacyRiskHostCreateReduceScriptOptions,
getLegacyRiskScoreInitScriptId,
getLegacyRiskScoreLevelScriptId,
getLegacyRiskScoreMapScriptId,
getLegacyRiskScoreReduceScriptId,
getLegacyRiskUserCreateLevelScriptOptions,
getLegacyRiskUserCreateMapScriptOptions,
getLegacyRiskUserCreateReduceScriptOptions,
getRiskScoreInitScriptId,
getRiskScoreLevelScriptId,
getRiskScoreMapScriptId,
getRiskScoreReduceScriptId,
} from '../../risk_scores/stored_scripts';
import {
createTransform,
deleteTransforms,
getCreateLegacyLatestTransformOptions,
getCreateLegacyMLHostPivotTransformOptions,
getCreateLegacyMLUserPivotTransformOptions,
getRiskScoreLatestTransformId,
getRiskScorePivotTransformId,
startTransforms,
} from '../../risk_scores/transforms';
import { createIndex, deleteRiskScoreIndicies } from './indices';
import { createIngestPipeline, deleteRiskScoreIngestPipelines } from './ingest_pipelines';
import { deleteSavedObjects } from './saved_objects';
import { createStoredScript, deleteStoredScripts } from './stored_scripts';
/**
* @deleteAll: If set to true, it deletes both old and new version.
* If set to false, it deletes legacy version only.
*/
export const deleteRiskScore = ({
riskScoreEntity,
spaceId,
deleteAll,
}: {
riskScoreEntity: RiskScoreEntity;
spaceId?: string;
deleteAll: boolean;
}) => {
const transformIds = [
getRiskScorePivotTransformId(riskScoreEntity, spaceId),
getRiskScoreLatestTransformId(riskScoreEntity, spaceId),
];
const legacyIngestPipelineNames = [getLegacyIngestPipelineName(riskScoreEntity)];
const ingestPipelinesNames = deleteAll
? [...legacyIngestPipelineNames, getIngestPipelineName(riskScoreEntity, spaceId)]
: legacyIngestPipelineNames;
const legacyScriptIds = [
...(riskScoreEntity === RiskScoreEntity.host
? [getLegacyRiskScoreInitScriptId(riskScoreEntity)]
: []),
getLegacyRiskScoreLevelScriptId(riskScoreEntity),
getLegacyRiskScoreMapScriptId(riskScoreEntity),
getLegacyRiskScoreReduceScriptId(riskScoreEntity),
];
const scripts = deleteAll
? [
...legacyScriptIds,
...(riskScoreEntity === RiskScoreEntity.host
? [getRiskScoreInitScriptId(riskScoreEntity, spaceId)]
: []),
getRiskScoreLevelScriptId(riskScoreEntity, spaceId),
getRiskScoreMapScriptId(riskScoreEntity, spaceId),
getRiskScoreReduceScriptId(riskScoreEntity, spaceId),
]
: legacyScriptIds;
deleteTransforms(transformIds);
deleteRiskScoreIngestPipelines(ingestPipelinesNames);
deleteStoredScripts(scripts);
deleteSavedObjects(`${riskScoreEntity}RiskScoreDashboards`, deleteAll);
deleteRiskScoreIndicies(riskScoreEntity, spaceId);
};
const installLegacyHostRiskScoreModule = (spaceId: string) => {
/**
* Step 1 Upload script: ml_hostriskscore_levels_script
*/
createStoredScript(getLegacyRiskHostCreateLevelScriptOptions())
.then(() => {
/**
* Step 2 Upload script: ml_hostriskscore_init_script
*/
return createStoredScript(getLegacyRiskHostCreateInitScriptOptions());
})
.then(() => {
/**
* Step 3 Upload script: ml_hostriskscore_map_script
*/
return createStoredScript(getLegacyRiskHostCreateMapScriptOptions());
})
.then(() => {
/**
* Step 4 Upload script: ml_hostriskscore_reduce_script
*/
return createStoredScript(getLegacyRiskHostCreateReduceScriptOptions());
})
.then(() => {
/**
* Step 5 Upload the ingest pipeline: ml_hostriskscore_ingest_pipeline
*/
return createIngestPipeline(getLegacyRiskScoreIngestPipelineOptions(RiskScoreEntity.host));
})
.then(() => {
/**
* Step 6 create ml_host_risk_score_{spaceId} index
*/
return createIndex(
getCreateLegacyRiskScoreIndicesOptions({
spaceId,
riskScoreEntity: RiskScoreEntity.host,
})
);
})
.then(() => {
/**
* Step 7 create transform: ml_hostriskscore_pivot_transform_{spaceId}
*/
return createTransform(
getRiskScorePivotTransformId(RiskScoreEntity.host, spaceId),
getCreateLegacyMLHostPivotTransformOptions({ spaceId })
);
})
.then(() => {
/**
* Step 8 create ml_host_risk_score_latest_{spaceId} index
*/
return createIndex(
getCreateLegacyRiskScoreLatestIndicesOptions({
spaceId,
riskScoreEntity: RiskScoreEntity.host,
})
);
})
.then(() => {
/**
* Step 9 create transform: ml_hostriskscore_latest_transform_{spaceId}
*/
return createTransform(
getRiskScoreLatestTransformId(RiskScoreEntity.host, spaceId),
getCreateLegacyLatestTransformOptions({
spaceId,
riskScoreEntity: RiskScoreEntity.host,
})
);
})
.then(() => {
/**
* Step 10 Start the pivot transform
* Step 11 Start the latest transform
*/
const transformIds = [
getRiskScorePivotTransformId(RiskScoreEntity.host, spaceId),
getRiskScoreLatestTransformId(RiskScoreEntity.host, spaceId),
];
return startTransforms(transformIds);
})
.then(() => {
// refresh page
visit(ENTITY_ANALYTICS_URL);
});
};
const installLegacyUserRiskScoreModule = async (spaceId = 'default') => {
/**
* Step 1 Upload script: ml_userriskscore_levels_script
*/
createStoredScript(getLegacyRiskUserCreateLevelScriptOptions())
.then(() => {
/**
* Step 2 Upload script: ml_userriskscore_map_script
*/
return createStoredScript(getLegacyRiskUserCreateMapScriptOptions());
})
.then(() => {
/**
* Step 3 Upload script: ml_userriskscore_reduce_script
*/
return createStoredScript(getLegacyRiskUserCreateReduceScriptOptions());
})
.then(() => {
/**
* Step 4 Upload ingest pipeline: ml_userriskscore_ingest_pipeline
*/
return createIngestPipeline(getLegacyRiskScoreIngestPipelineOptions(RiskScoreEntity.user));
})
.then(() => {
/**
* Step 5 create ml_user_risk_score_{spaceId} index
*/
return createIndex(
getCreateLegacyRiskScoreIndicesOptions({
spaceId,
riskScoreEntity: RiskScoreEntity.user,
})
);
})
.then(() => {
/**
* Step 6 create Transform: ml_userriskscore_pivot_transform_{spaceId}
*/
return createTransform(
getRiskScorePivotTransformId(RiskScoreEntity.user, spaceId),
getCreateLegacyMLUserPivotTransformOptions({ spaceId })
);
})
.then(() => {
/**
* Step 7 create ml_user_risk_score_latest_{spaceId} index
*/
return createIndex(
getCreateLegacyRiskScoreLatestIndicesOptions({
spaceId,
riskScoreEntity: RiskScoreEntity.user,
})
);
})
.then(() => {
/**
* Step 8 create Transform: ml_userriskscore_latest_transform_{spaceId}
*/
return createTransform(
getRiskScoreLatestTransformId(RiskScoreEntity.user, spaceId),
getCreateLegacyLatestTransformOptions({
spaceId,
riskScoreEntity: RiskScoreEntity.user,
})
);
})
.then(() => {
/**
* Step 9 Start the pivot transform
* Step 10 Start the latest transform
*/
const transformIds = [
getRiskScorePivotTransformId(RiskScoreEntity.user, spaceId),
getRiskScoreLatestTransformId(RiskScoreEntity.user, spaceId),
];
return startTransforms(transformIds);
})
.then(() => {
visit(ENTITY_ANALYTICS_URL);
});
};
export const installLegacyRiskScoreModule = (
riskScoreEntity: RiskScoreEntity,
spaceId = 'default'
) => {
if (riskScoreEntity === RiskScoreEntity.user) {
installLegacyUserRiskScoreModule(spaceId);
} else {
installLegacyHostRiskScoreModule(spaceId);
}
};
export const intercepInstallRiskScoreModule = () => {
cy.intercept(`POST`, RISK_SCORE_URL).as('install');
};
export const waitForInstallRiskScoreModule = () => {
cy.wait(['@install'], { requestTimeout: 50000 });
};

View file

@ -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.
*/
import { INDICES_URL } from '../../../urls/risk_score';
import type { RiskScoreEntity } from '../../risk_scores/common';
import { getLatestTransformIndex, getPivotTransformIndex } from '../../risk_scores/indices';
export const createIndex = (options: {
index: string;
mappings: string | Record<string, unknown>;
}) => {
return cy.request({
method: 'put',
url: `${INDICES_URL}/create`,
body: options,
headers: { 'kbn-xsrf': 'cypress-creds-via-config' },
});
};
export const deleteRiskScoreIndicies = (riskScoreEntity: RiskScoreEntity, spaceId = 'default') => {
return cy
.request({
method: 'post',
url: `${INDICES_URL}/delete`,
body: {
indices: [getPivotTransformIndex(riskScoreEntity, spaceId)],
},
headers: { 'kbn-xsrf': 'cypress-creds-via-config' },
failOnStatusCode: false,
})
.then(() => {
return cy.request({
method: 'post',
url: `${INDICES_URL}/delete`,
body: {
indices: [getLatestTransformIndex(riskScoreEntity, spaceId)],
},
headers: { 'kbn-xsrf': 'cypress-creds-via-config' },
failOnStatusCode: false,
});
});
};

View file

@ -0,0 +1,26 @@
/*
* 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 { INGEST_PIPELINES_URL } from '../../../urls/risk_score';
export const createIngestPipeline = (options: { name: string; processors: Array<{}> }) => {
return cy.request({
method: 'post',
url: `${INGEST_PIPELINES_URL}`,
headers: { 'kbn-xsrf': 'cypress-creds-via-config' },
body: options,
});
};
export const deleteRiskScoreIngestPipelines = (names: string[]) => {
return cy.request({
method: 'delete',
url: `${INGEST_PIPELINES_URL}/${names.join(',')}`,
headers: { 'kbn-xsrf': 'cypress-creds-via-config' },
failOnStatusCode: false,
});
};

View file

@ -0,0 +1,56 @@
/*
* 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 { RISK_SCORE_SAVED_OBJECTS_URL, SAVED_OBJECTS_URL } from '../../../urls/risk_score';
import type { RiskScoreEntity } from '../../risk_scores/common';
import { getRiskScoreTagName } from '../../risk_scores/saved_objects';
export const deleteSavedObjects = (
templateName: `${RiskScoreEntity}RiskScoreDashboards`,
deleteAll: boolean
) => {
return cy.request({
method: 'post',
url: `${RISK_SCORE_SAVED_OBJECTS_URL}/_bulk_delete/${templateName}`,
failOnStatusCode: false,
body: {
deleteAll,
},
headers: { 'kbn-xsrf': 'cypress-creds-via-config' },
});
};
export const createSavedObjects = (templateName: `${RiskScoreEntity}RiskScoreDashboards`) => {
return cy.request({
method: 'post',
url: `${RISK_SCORE_SAVED_OBJECTS_URL}/_bulk_create/${templateName}`,
failOnStatusCode: false,
headers: { 'kbn-xsrf': 'cypress-creds-via-config' },
});
};
export const findSavedObjects = (riskScoreEntity: RiskScoreEntity, spaceId = 'default') => {
const search = getRiskScoreTagName(riskScoreEntity, spaceId);
const getReference = (tagId: string) => encodeURIComponent(`[{"type":"tag","id":"${tagId}"}]`);
return cy
.request({
method: 'get',
url: `${SAVED_OBJECTS_URL}/_find?fields=id&type=tag&sort_field=updated_at&search=${search}&search_fields=name`,
headers: { 'kbn-xsrf': 'cypress-creds-via-config' },
})
.then((res) =>
cy.request({
method: 'get',
url: `${SAVED_OBJECTS_URL}/_find?fields=id&type=index-pattern&type=tag&type=visualization&type=dashboard&type=lens&sort_field=updated_at&has_reference=${getReference(
res.body.saved_objects[0].id
)}`,
headers: { 'kbn-xsrf': 'cypress-creds-via-config' },
})
);
};

View file

@ -0,0 +1,31 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { STORED_SCRIPTS_URL } from '../../../urls/risk_score';
export const createStoredScript = (options: { id: string; script: {} }) => {
return cy.request({
method: 'put',
url: `${STORED_SCRIPTS_URL}/create`,
body: options,
headers: { 'kbn-xsrf': 'cypress-creds-via-config' },
});
};
const deleteStoredScript = (id: string) => {
return cy.request({
method: 'delete',
url: `${STORED_SCRIPTS_URL}/delete`,
body: { id },
failOnStatusCode: false,
headers: { 'kbn-xsrf': 'cypress-creds-via-config' },
});
};
export const deleteStoredScripts = async (scriptIds: string[]) => {
await Promise.all(scriptIds.map((scriptId) => deleteStoredScript(scriptId)));
};

View file

@ -0,0 +1,10 @@
/*
* 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 enum RiskScoreEntity {
host = 'host',
user = 'user',
}

View file

@ -0,0 +1,75 @@
/*
* 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 {
ENABLE_HOST_RISK_SCORE_BUTTON,
ENABLE_USER_RISK_SCORE_BUTTON,
UPGRADE_CONFIRMATION_BUTTON,
UPGRADE_HOST_RISK_SCORE_BUTTON,
UPGRADE_USER_RISK_SCORE_BUTTON,
} from '../../screens/entity_analytics';
import {
INGEST_PIPELINES_URL,
RISK_SCORE_SAVED_OBJECTS_URL,
STORED_SCRIPTS_URL,
TRANSFORMS_URL,
} from '../../urls/risk_score';
import { intercepInstallRiskScoreModule } from '../api_calls/risk_scores';
import { RiskScoreEntity } from './common';
import { getLegacyIngestPipelineName } from './ingest_pipelines';
export const interceptUpgradeRiskScoreModule = (riskScoreEntity: RiskScoreEntity) => {
cy.intercept(
`POST`,
`${RISK_SCORE_SAVED_OBJECTS_URL}/_bulk_delete/${riskScoreEntity}RiskScoreDashboards`
).as('deleteDashboards');
cy.intercept(`POST`, `${TRANSFORMS_URL}/stop_transforms`).as('stopTransforms');
cy.intercept(`POST`, `${TRANSFORMS_URL}/delete_transforms`).as('deleteTransforms');
cy.intercept(
`DELETE`,
`${INGEST_PIPELINES_URL}/${getLegacyIngestPipelineName(riskScoreEntity)}`
).as('deleteIngestPipelines');
cy.intercept(`DELETE`, `${STORED_SCRIPTS_URL}/delete`).as('deleteScripts');
intercepInstallRiskScoreModule();
};
export const waitForUpgradeRiskScoreModule = () => {
cy.wait(
[
'@deleteDashboards',
'@stopTransforms',
'@deleteTransforms',
'@deleteIngestPipelines',
'@deleteScripts',
'@install',
],
{ requestTimeout: 50000 }
);
};
export const clickEnableRiskScore = (riskScoreEntity: RiskScoreEntity) => {
const button =
riskScoreEntity === RiskScoreEntity.user
? ENABLE_USER_RISK_SCORE_BUTTON
: ENABLE_HOST_RISK_SCORE_BUTTON;
cy.get(button).click();
};
export const clickUpgradeRiskScore = (riskScoreEntity: RiskScoreEntity) => {
const button =
riskScoreEntity === RiskScoreEntity.user
? UPGRADE_USER_RISK_SCORE_BUTTON
: UPGRADE_HOST_RISK_SCORE_BUTTON;
cy.get(button).click();
};
export const clickUpgradeRiskScoreConfirmed = () => {
cy.get(UPGRADE_CONFIRMATION_BUTTON).click();
};

View file

@ -0,0 +1,96 @@
/*
* 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 { RiskScoreEntity } from './common';
export const getPivotTransformIndex = (riskScoreEntity: RiskScoreEntity, spaceId = 'default') =>
`ml_${riskScoreEntity}_risk_score_${spaceId}`;
export const getLatestTransformIndex = (riskScoreEntity: RiskScoreEntity, spaceId = 'default') =>
`ml_${riskScoreEntity}_risk_score_latest_${spaceId}`;
export const getCreateLegacyRiskScoreIndicesOptions = ({
spaceId = 'default',
riskScoreEntity,
}: {
spaceId?: string;
riskScoreEntity: RiskScoreEntity;
}) => {
const mappings = {
properties: {
[`${riskScoreEntity}.name`]: {
type: 'keyword',
},
'@timestamp': {
type: 'date',
},
ingest_timestamp: {
type: 'date',
},
risk: {
type: 'text',
fields: {
keyword: {
type: 'keyword',
},
},
},
risk_stats: {
properties: {
risk_score: {
type: 'float',
},
},
},
},
};
return {
index: getPivotTransformIndex(riskScoreEntity, spaceId),
mappings,
};
};
export const getCreateLegacyRiskScoreLatestIndicesOptions = ({
spaceId = 'default',
riskScoreEntity,
}: {
spaceId?: string;
riskScoreEntity: RiskScoreEntity;
}) => {
const mappings = {
properties: {
[`${riskScoreEntity}.name`]: {
type: 'keyword',
},
'@timestamp': {
type: 'date',
},
ingest_timestamp: {
type: 'date',
},
risk: {
type: 'text',
fields: {
keyword: {
type: 'keyword',
},
},
},
risk_stats: {
properties: {
risk_score: {
type: 'float',
},
},
},
},
};
return {
index: getLatestTransformIndex(riskScoreEntity, spaceId),
mappings,
};
};

View file

@ -0,0 +1,45 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { RiskScoreEntity } from './common';
import { getLegacyRiskScoreLevelScriptId } from './stored_scripts';
export const getIngestPipelineName = (riskScoreEntity: RiskScoreEntity, spaceId = 'default') =>
`ml_${riskScoreEntity}riskscore_ingest_pipeline_${spaceId}`;
export const getLegacyIngestPipelineName = (riskScoreEntity: RiskScoreEntity) =>
`ml_${riskScoreEntity}riskscore_ingest_pipeline`;
export const getLegacyRiskScoreIngestPipelineOptions = (riskScoreEntity: RiskScoreEntity) => {
const processors = [
{
set: {
field: 'ingest_timestamp',
value: '{{_ingest.timestamp}}',
},
},
{
fingerprint: {
fields: ['@timestamp', '_id'],
method: 'SHA-256',
target_field: '_id',
},
},
{
script: {
id: getLegacyRiskScoreLevelScriptId(riskScoreEntity),
params: {
risk_score: 'risk_stats.risk_score',
},
},
},
];
return {
name: getLegacyIngestPipelineName(riskScoreEntity),
processors,
};
};

View file

@ -0,0 +1,17 @@
/*
* 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 { RiskScoreEntity } from './common';
const HOST_RISK_SCORE = 'Host Risk Score';
const USER_RISK_SCORE = 'User Risk Score';
const getRiskScore = (riskScoreEntity: RiskScoreEntity) =>
riskScoreEntity === RiskScoreEntity.user ? USER_RISK_SCORE : HOST_RISK_SCORE;
export const getRiskScoreTagName = (riskScoreEntity: RiskScoreEntity, spaceId = 'default') =>
`${getRiskScore(riskScoreEntity)} ${spaceId}`;

View file

@ -0,0 +1,110 @@
/*
* 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 { RiskScoreEntity } from './common';
export const getRiskScoreLevelScriptId = (riskScoreEntity: RiskScoreEntity, spaceId = 'default') =>
`ml_${riskScoreEntity}riskscore_levels_script_${spaceId}`;
export const getRiskScoreInitScriptId = (riskScoreEntity: RiskScoreEntity, spaceId = 'default') =>
`ml_${riskScoreEntity}riskscore_init_script_${spaceId}`;
export const getRiskScoreMapScriptId = (riskScoreEntity: RiskScoreEntity, spaceId = 'default') =>
`ml_${riskScoreEntity}riskscore_map_script_${spaceId}`;
export const getRiskScoreReduceScriptId = (riskScoreEntity: RiskScoreEntity, spaceId = 'default') =>
`ml_${riskScoreEntity}riskscore_reduce_script_${spaceId}`;
export const getLegacyRiskScoreLevelScriptId = (riskScoreEntity: RiskScoreEntity) =>
`ml_${riskScoreEntity}riskscore_levels_script`;
export const getLegacyRiskScoreInitScriptId = (riskScoreEntity: RiskScoreEntity) =>
`ml_${riskScoreEntity}riskscore_init_script`;
export const getLegacyRiskScoreMapScriptId = (riskScoreEntity: RiskScoreEntity) =>
`ml_${riskScoreEntity}riskscore_map_script`;
export const getLegacyRiskScoreReduceScriptId = (riskScoreEntity: RiskScoreEntity) =>
`ml_${riskScoreEntity}riskscore_reduce_script`;
export const getLegacyRiskHostCreateLevelScriptOptions = (stringifyScript?: boolean) => {
const source =
"double risk_score = (def)ctx.getByPath(params.risk_score);\nif (risk_score < 20) {\n ctx['risk'] = 'Unknown'\n}\nelse if (risk_score >= 20 && risk_score < 40) {\n ctx['risk'] = 'Low'\n}\nelse if (risk_score >= 40 && risk_score < 70) {\n ctx['risk'] = 'Moderate'\n}\nelse if (risk_score >= 70 && risk_score < 90) {\n ctx['risk'] = 'High'\n}\nelse if (risk_score >= 90) {\n ctx['risk'] = 'Critical'\n}";
return {
id: getLegacyRiskScoreLevelScriptId(RiskScoreEntity.host),
script: {
lang: 'painless',
source: stringifyScript ? JSON.stringify(source) : source,
},
};
};
export const getLegacyRiskHostCreateInitScriptOptions = (stringifyScript?: boolean) => {
const source =
'state.rule_risk_stats = new HashMap();\nstate.host_variant_set = false;\nstate.host_variant = new String();\nstate.tactic_ids = new HashSet();';
return {
id: getLegacyRiskScoreInitScriptId(RiskScoreEntity.host),
script: {
lang: 'painless',
source: stringifyScript ? JSON.stringify(source) : source,
},
};
};
export const getLegacyRiskHostCreateMapScriptOptions = (stringifyScript?: boolean) => {
const source =
'// Get the host variant\nif (state.host_variant_set == false) {\n if (doc.containsKey("host.os.full") && doc["host.os.full"].size() != 0) {\n state.host_variant = doc["host.os.full"].value;\n state.host_variant_set = true;\n }\n}\n// Aggregate all the tactics seen on the host\nif (doc.containsKey("signal.rule.threat.tactic.id") && doc["signal.rule.threat.tactic.id"].size() != 0) {\n state.tactic_ids.add(doc["signal.rule.threat.tactic.id"].value);\n}\n// Get running sum of time-decayed risk score per rule name per shard\nString rule_name = doc["signal.rule.name"].value;\ndef stats = state.rule_risk_stats.getOrDefault(rule_name, [0.0,"",false]);\nint time_diff = (int)((System.currentTimeMillis() - doc["@timestamp"].value.toInstant().toEpochMilli()) / (1000.0 * 60.0 * 60.0));\ndouble risk_derate = Math.min(1, Math.exp((params.lookback_time - time_diff) / params.time_decay_constant));\nstats[0] = Math.max(stats[0], doc["signal.rule.risk_score"].value * risk_derate);\nif (stats[2] == false) {\n stats[1] = doc["kibana.alert.rule.uuid"].value;\n stats[2] = true;\n}\nstate.rule_risk_stats.put(rule_name, stats);';
return {
id: getLegacyRiskScoreMapScriptId(RiskScoreEntity.host),
script: {
lang: 'painless',
source: stringifyScript ? JSON.stringify(source) : source,
},
};
};
export const getLegacyRiskHostCreateReduceScriptOptions = (stringifyScript?: boolean) => {
const source =
'// Consolidating time decayed risks and tactics from across all shards\nMap total_risk_stats = new HashMap();\nString host_variant = new String();\ndef tactic_ids = new HashSet();\nfor (state in states) {\n for (key in state.rule_risk_stats.keySet()) {\n def rule_stats = state.rule_risk_stats.get(key);\n def stats = total_risk_stats.getOrDefault(key, [0.0,"",false]);\n stats[0] = Math.max(stats[0], rule_stats[0]);\n if (stats[2] == false) {\n stats[1] = rule_stats[1];\n stats[2] = true;\n } \n total_risk_stats.put(key, stats);\n }\n if (host_variant.length() == 0) {\n host_variant = state.host_variant;\n }\n tactic_ids.addAll(state.tactic_ids);\n}\n// Consolidating individual rule risks and arranging them in decreasing order\nList risks = new ArrayList();\nfor (key in total_risk_stats.keySet()) {\n risks.add(total_risk_stats[key][0])\n}\nCollections.sort(risks, Collections.reverseOrder());\n// Calculating total host risk score\ndouble total_risk = 0.0;\ndouble risk_cap = params.max_risk * params.zeta_constant;\nfor (int i=0;i<risks.length;i++) {\n total_risk += risks[i] / Math.pow((1+i), params.p);\n}\n// Normalizing the host risk score\ndouble total_norm_risk = 100 * total_risk / risk_cap;\nif (total_norm_risk < 40) {\n total_norm_risk = 2.125 * total_norm_risk;\n}\nelse if (total_norm_risk >= 40 && total_norm_risk < 50) {\n total_norm_risk = 85 + (total_norm_risk - 40);\n}\nelse {\n total_norm_risk = 95 + (total_norm_risk - 50) / 10;\n}\n// Calculating multipliers to the host risk score\ndouble risk_multiplier = 1.0;\nList multipliers = new ArrayList();\n// Add a multiplier if host is a server\nif (host_variant.toLowerCase().contains("server")) {\n risk_multiplier *= params.server_multiplier;\n multipliers.add("Host is a server");\n}\n// Add multipliers based on number and diversity of tactics seen on the host\nfor (String tactic : tactic_ids) {\n multipliers.add("Tactic "+tactic);\n risk_multiplier *= 1 + params.tactic_base_multiplier * params.tactic_weights.getOrDefault(tactic, 0);\n}\n// Calculating final risk\ndouble final_risk = total_norm_risk;\nif (risk_multiplier > 1.0) {\n double prior_odds = (total_norm_risk) / (100 - total_norm_risk);\n double updated_odds = prior_odds * risk_multiplier; \n final_risk = 100 * updated_odds / (1 + updated_odds);\n}\n// Adding additional metadata\nList rule_stats = new ArrayList();\nfor (key in total_risk_stats.keySet()) {\n Map temp = new HashMap();\n temp["rule_name"] = key;\n temp["rule_risk"] = total_risk_stats[key][0];\n temp["rule_id"] = total_risk_stats[key][1];\n rule_stats.add(temp);\n}\n\nreturn ["calculated_score_norm": final_risk, "rule_risks": rule_stats, "multipliers": multipliers];';
return {
id: getLegacyRiskScoreReduceScriptId(RiskScoreEntity.host),
script: {
lang: 'painless',
source: stringifyScript ? JSON.stringify(source) : source,
},
};
};
export const getLegacyRiskUserCreateLevelScriptOptions = () => {
const source =
"double risk_score = (def)ctx.getByPath(params.risk_score);\nif (risk_score < 20) {\n ctx['risk'] = 'Unknown'\n}\nelse if (risk_score >= 20 && risk_score < 40) {\n ctx['risk'] = 'Low'\n}\nelse if (risk_score >= 40 && risk_score < 70) {\n ctx['risk'] = 'Moderate'\n}\nelse if (risk_score >= 70 && risk_score < 90) {\n ctx['risk'] = 'High'\n}\nelse if (risk_score >= 90) {\n ctx['risk'] = 'Critical'\n}";
return {
id: getLegacyRiskScoreLevelScriptId(RiskScoreEntity.user),
script: {
lang: 'painless',
source,
},
};
};
export const getLegacyRiskUserCreateMapScriptOptions = () => {
const source =
'// Get running sum of risk score per rule name per shard\\\\\nString rule_name = doc["signal.rule.name"].value;\ndef stats = state.rule_risk_stats.getOrDefault(rule_name, 0.0);\nstats = doc["signal.rule.risk_score"].value;\nstate.rule_risk_stats.put(rule_name, stats);';
return {
id: getLegacyRiskScoreMapScriptId(RiskScoreEntity.user),
script: {
lang: 'painless',
source,
},
};
};
export const getLegacyRiskUserCreateReduceScriptOptions = () => {
const source =
'// Consolidating time decayed risks from across all shards\nMap total_risk_stats = new HashMap();\nfor (state in states) {\n for (key in state.rule_risk_stats.keySet()) {\n def rule_stats = state.rule_risk_stats.get(key);\n def stats = total_risk_stats.getOrDefault(key, 0.0);\n stats = rule_stats;\n total_risk_stats.put(key, stats);\n }\n}\n// Consolidating individual rule risks and arranging them in decreasing order\nList risks = new ArrayList();\nfor (key in total_risk_stats.keySet()) {\n risks.add(total_risk_stats[key])\n}\nCollections.sort(risks, Collections.reverseOrder());\n// Calculating total risk and normalizing it to a range\ndouble total_risk = 0.0;\ndouble risk_cap = params.max_risk * params.zeta_constant;\nfor (int i=0;i<risks.length;i++) {\n total_risk += risks[i] / Math.pow((1+i), params.p);\n}\ndouble total_norm_risk = 100 * total_risk / risk_cap;\nif (total_norm_risk < 40) {\n total_norm_risk = 2.125 * total_norm_risk;\n}\nelse if (total_norm_risk >= 40 && total_norm_risk < 50) {\n total_norm_risk = 85 + (total_norm_risk - 40);\n}\nelse {\n total_norm_risk = 95 + (total_norm_risk - 50) / 10;\n}\n\nList rule_stats = new ArrayList();\nfor (key in total_risk_stats.keySet()) {\n Map temp = new HashMap();\n temp["rule_name"] = key;\n temp["rule_risk"] = total_risk_stats[key];\n rule_stats.add(temp);\n}\n\nreturn ["risk_score": total_norm_risk, "rule_risks": rule_stats];';
return {
id: getLegacyRiskScoreReduceScriptId(RiskScoreEntity.user),
script: {
lang: 'painless',
source,
},
};
};

View file

@ -0,0 +1,307 @@
/*
* 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 { TRANSFORMS_URL } from '../../urls/risk_score';
import { RiskScoreEntity } from './common';
import { getLatestTransformIndex, getPivotTransformIndex } from './indices';
import { getLegacyIngestPipelineName } from './ingest_pipelines';
import {
getLegacyRiskScoreInitScriptId,
getLegacyRiskScoreMapScriptId,
getLegacyRiskScoreReduceScriptId,
} from './stored_scripts';
const DEFAULT_ALERTS_INDEX = '.alerts-security.alerts' as const;
export const getAlertsIndex = (spaceId = 'default') => `${DEFAULT_ALERTS_INDEX}-${spaceId}`;
export const getRiskScorePivotTransformId = (
riskScoreEntity: RiskScoreEntity,
spaceId = 'default'
) => `ml_${riskScoreEntity}riskscore_pivot_transform_${spaceId}`;
export const getRiskScoreLatestTransformId = (
riskScoreEntity: RiskScoreEntity,
spaceId = 'default'
) => `ml_${riskScoreEntity}riskscore_latest_transform_${spaceId}`;
export const getTransformState = (transformId: string) => {
return cy.request<{ transforms: Array<{ id: string; state: string }>; count: number }>({
method: 'get',
url: `${TRANSFORMS_URL}/transforms/${transformId}/_stats`,
headers: { 'kbn-xsrf': 'cypress-creds-via-config' },
});
};
export const startTransforms = (transformIds: string[]) => {
return cy.request({
method: 'post',
url: `${TRANSFORMS_URL}/start_transforms`,
headers: { 'kbn-xsrf': 'cypress-creds-via-config' },
body: transformIds.map((id) => ({
id,
})),
});
};
const stopTransform = (state: {
transforms: Array<{ id: string; state: string }>;
count: number;
}) => {
return cy.request({
method: 'post',
url: `${TRANSFORMS_URL}/stop_transforms`,
headers: { 'kbn-xsrf': 'cypress-creds-via-config' },
body:
state != null && state.transforms.length > 0
? [
{
id: state.transforms[0].id,
state: state.transforms[0].state,
},
]
: ([] as Array<{ id: string; state: string }>),
});
};
export const createTransform = (transformId: string, options: string | Record<string, unknown>) => {
return cy.request({
method: 'put',
url: `${TRANSFORMS_URL}/transforms/${transformId}`,
headers: { 'kbn-xsrf': 'cypress-creds-via-config' },
body: options,
});
};
export const deleteTransform = (transformId: string) => {
return cy.request({
method: 'post',
url: `${TRANSFORMS_URL}/delete_transforms`,
headers: { 'kbn-xsrf': 'cypress-creds-via-config' },
failOnStatusCode: false,
body: {
transformsInfo: [
{
id: transformId,
state: 'stopped',
},
],
deleteDestIndex: true,
deleteDestDataView: true,
forceDelete: false,
},
});
};
export const deleteTransforms = (transformIds: string[]) => {
const deleteSingleTransform = (transformId: string) =>
getTransformState(transformId)
.then(({ body: result }) => {
return stopTransform(result);
})
.then(() => {
deleteTransform(transformId);
});
transformIds.map((transformId) => deleteSingleTransform(transformId));
};
export const getCreateLegacyMLHostPivotTransformOptions = ({
spaceId = 'default',
}: {
spaceId?: string;
}) => {
const options = {
dest: {
index: getPivotTransformIndex(RiskScoreEntity.host, spaceId),
pipeline: getLegacyIngestPipelineName(RiskScoreEntity.host),
},
frequency: '1h',
pivot: {
aggregations: {
'@timestamp': {
max: {
field: '@timestamp',
},
},
risk_stats: {
scripted_metric: {
combine_script: 'return state',
init_script: {
id: getLegacyRiskScoreInitScriptId(RiskScoreEntity.host),
},
map_script: {
id: getLegacyRiskScoreMapScriptId(RiskScoreEntity.host),
},
params: {
lookback_time: 72,
max_risk: 100,
p: 1.5,
server_multiplier: 1.5,
tactic_base_multiplier: 0.25,
tactic_weights: {
TA0001: 1,
TA0002: 2,
TA0003: 3,
TA0004: 4,
TA0005: 4,
TA0006: 4,
TA0007: 4,
TA0008: 5,
TA0009: 6,
TA0010: 7,
TA0011: 6,
TA0040: 8,
TA0042: 1,
TA0043: 1,
},
time_decay_constant: 6,
zeta_constant: 2.612,
},
reduce_script: {
id: getLegacyRiskScoreReduceScriptId(RiskScoreEntity.host),
},
},
},
},
group_by: {
[`${RiskScoreEntity.host}.name`]: {
terms: {
field: `${RiskScoreEntity.host}.name`,
},
},
},
},
source: {
index: [getAlertsIndex(spaceId)],
query: {
bool: {
filter: [
{
range: {
'@timestamp': {
gte: 'now-5d',
},
},
},
],
},
},
},
sync: {
time: {
delay: '120s',
field: '@timestamp',
},
},
};
return options;
};
export const getCreateLegacyMLUserPivotTransformOptions = ({
spaceId = 'default',
}: {
spaceId?: string;
}) => {
const options = {
dest: {
index: getPivotTransformIndex(RiskScoreEntity.user, spaceId),
pipeline: getLegacyIngestPipelineName(RiskScoreEntity.user),
},
frequency: '1h',
pivot: {
aggregations: {
'@timestamp': {
max: {
field: '@timestamp',
},
},
risk_stats: {
scripted_metric: {
combine_script: 'return state',
init_script: 'state.rule_risk_stats = new HashMap();',
map_script: {
id: getLegacyRiskScoreMapScriptId(RiskScoreEntity.user),
},
params: {
max_risk: 100,
p: 1.5,
zeta_constant: 2.612,
},
reduce_script: {
id: getLegacyRiskScoreReduceScriptId(RiskScoreEntity.user),
},
},
},
},
group_by: {
'user.name': {
terms: {
field: 'user.name',
},
},
},
},
source: {
index: [getAlertsIndex(spaceId)],
query: {
bool: {
filter: [
{
range: {
'@timestamp': {
gte: 'now-90d',
},
},
},
{
match: {
'signal.status': 'open',
},
},
],
},
},
},
sync: {
time: {
delay: '120s',
field: '@timestamp',
},
},
};
return options;
};
export const getCreateLegacyLatestTransformOptions = ({
spaceId = 'default',
riskScoreEntity,
}: {
spaceId?: string;
riskScoreEntity: RiskScoreEntity;
}) => {
const options = {
dest: {
index: getLatestTransformIndex(riskScoreEntity, spaceId),
},
frequency: '1h',
latest: {
sort: '@timestamp',
unique_key: [`${riskScoreEntity}.name`],
},
source: {
index: [getPivotTransformIndex(riskScoreEntity, spaceId)],
},
sync: {
time: {
delay: '2s',
field: 'ingest_timestamp',
},
},
};
return options;
};

View file

@ -0,0 +1,15 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export const RISK_SCORE_URL = `/internal/risk_score` as const;
export const INDICES_URL = `/internal/risk_score/indices` as const;
export const INGEST_PIPELINES_URL = `/api/ingest_pipelines` as const;
export const TRANSFORMS_URL = `/api/transform` as const;
export const STORED_SCRIPTS_URL = `/internal/risk_score/stored_scripts` as const;
export const RISK_SCORE_SAVED_OBJECTS_URL =
`/internal/risk_score/prebuilt_content/saved_objects` as const;
export const SAVED_OBJECTS_URL = `/api/saved_objects` as const;

View file

@ -42,7 +42,7 @@ export const ENABLE_RISK_SCORE = (riskEntity: RiskScoreEntity) =>
export const ENABLE_RISK_SCORE_DESCRIPTION = (riskEntity: RiskScoreEntity) =>
i18n.translate('xpack.securitySolution.enableRiskScore.enableRiskScoreDescription', {
defaultMessage:
'Once you have enabled this feature you can get quick access to the {riskEntity} risk scores in this section.',
'Once you have enabled this feature you can get quick access to the {riskEntity} risk scores in this section. The data might need an hour to be generated after enabling the module.',
values: {
riskEntity: getRiskEntityTranslation(riskEntity, true),
},

View file

@ -28,10 +28,10 @@ const RiskScoreRestartButtonComponent = ({
REQUEST_NAMES.REFRESH_RISK_SCORE,
restartRiskScoreTransforms
);
const spaceId = useSpaceId();
const { renderDocLink } = useRiskScoreToastContent(riskScoreEntity);
const { http, notifications, theme } = useKibana().services;
const { http, notifications } = useKibana().services;
const onClick = useCallback(async () => {
fetch({
@ -41,9 +41,8 @@ const RiskScoreRestartButtonComponent = ({
renderDocLink,
riskScoreEntity,
spaceId,
theme,
});
}, [fetch, http, notifications, refetch, renderDocLink, riskScoreEntity, spaceId, theme]);
}, [fetch, http, notifications, refetch, renderDocLink, riskScoreEntity, spaceId]);
return (
<EuiButton

View file

@ -30,14 +30,14 @@ export const USER_WARNING_TITLE = i18n.translate(
export const HOST_WARNING_BODY = i18n.translate(
'xpack.securitySolution.riskScore.hostsDashboardWarningPanelBody',
{
defaultMessage: `We haven't detected any host risk score data from the hosts in your environment.`,
defaultMessage: `We haven't detected any host risk score data from the hosts in your environment. The data might need an hour to be generated after enabling the module.`,
}
);
export const USER_WARNING_BODY = i18n.translate(
'xpack.securitySolution.riskScore.usersDashboardWarningPanelBody',
{
defaultMessage: `We haven't detected any user risk score data from the users in your environment.`,
defaultMessage: `We haven't detected any user risk score data from the users in your environment. The data might need an hour to be generated after enabling the module.`,
}
);

View file

@ -34,133 +34,40 @@ const mockTimerange = {
to: 'endDate',
};
const mockRefetch = jest.fn();
describe(`installRiskScoreModule - ${RiskScoreEntity.host}`, () => {
beforeAll(async () => {
await installRiskScoreModule({
http: mockHttp,
refetch: mockRefetch,
spaceId: mockSpaceId,
timerange: mockTimerange,
riskScoreEntity: RiskScoreEntity.host,
describe.each([RiskScoreEntity.host, RiskScoreEntity.user])(
`installRiskScoreModule - %s`,
(riskScoreEntity) => {
beforeAll(async () => {
await installRiskScoreModule({
http: mockHttp,
refetch: mockRefetch,
spaceId: mockSpaceId,
timerange: mockTimerange,
riskScoreEntity,
});
});
});
afterAll(() => {
jest.clearAllMocks();
});
it(`Create script: ml_${RiskScoreEntity.host}riskscore_levels_script_${mockSpaceId}`, async () => {
expect((api.createStoredScript as jest.Mock).mock.calls[0][0].options).toMatchSnapshot();
});
it(`Create script: ml_${RiskScoreEntity.host}riskscore_init_script_${mockSpaceId}`, async () => {
expect((api.createStoredScript as jest.Mock).mock.calls[1][0].options).toMatchSnapshot();
});
it(`Create script: ml_${RiskScoreEntity.host}riskscore_map_script_${mockSpaceId}`, async () => {
expect((api.createStoredScript as jest.Mock).mock.calls[2][0].options).toMatchSnapshot();
});
it(`Create script: ml_${RiskScoreEntity.host}riskscore_reduce_script_${mockSpaceId}`, async () => {
expect((api.createStoredScript as jest.Mock).mock.calls[3][0].options).toMatchSnapshot();
});
it(`Create IngestPipeline: ml_${RiskScoreEntity.host}riskscore_ingest_pipeline_${mockSpaceId}`, async () => {
expect((api.createIngestPipeline as jest.Mock).mock.calls[0][0].options).toMatchSnapshot();
});
it(`Create Index: ml_${RiskScoreEntity.host}_risk_score_${mockSpaceId}`, async () => {
expect((api.createIndices as jest.Mock).mock.calls[0][0].options).toMatchSnapshot();
});
it(`Create Index: ml_${RiskScoreEntity.host}_risk_score_latest_${mockSpaceId}`, async () => {
expect((api.createIndices as jest.Mock).mock.calls[1][0].options).toMatchSnapshot();
});
it(`Create Transform: ml_${RiskScoreEntity.host}riskscore_pivot_transform_${mockSpaceId}`, async () => {
expect((api.createTransform as jest.Mock).mock.calls[0][0].options).toMatchSnapshot();
});
it(`Create Transform: ml_${RiskScoreEntity.host}riskscore_latest_transform_${mockSpaceId}`, async () => {
expect((api.createTransform as jest.Mock).mock.calls[1][0].options).toMatchSnapshot();
});
it(`Start Transforms`, () => {
expect((api.startTransforms as jest.Mock).mock.calls[0][0].transformIds).toMatchSnapshot();
});
it(`Create ${RiskScoreEntity.host} dashboards`, () => {
expect(
(bulkCreatePrebuiltSavedObjects as jest.Mock).mock.calls[0][0].options.templateName
).toEqual(`${RiskScoreEntity.host}RiskScoreDashboards`);
});
it('Refresh module', () => {
expect(mockRefetch).toBeCalled();
});
});
describe(`installRiskScoreModule - ${RiskScoreEntity.user}`, () => {
beforeAll(async () => {
await installRiskScoreModule({
http: mockHttp,
refetch: mockRefetch,
spaceId: mockSpaceId,
timerange: mockTimerange,
riskScoreEntity: RiskScoreEntity.user,
afterAll(() => {
jest.clearAllMocks();
});
});
afterAll(() => {
jest.clearAllMocks();
});
it(`installRiskScore`, () => {
expect((api.installRiskScore as jest.Mock).mock.calls[0][0].options.riskScoreEntity).toEqual(
riskScoreEntity
);
});
it(`Create script: ml_${RiskScoreEntity.user}riskscore_levels_script_${mockSpaceId}`, async () => {
expect((api.createStoredScript as jest.Mock).mock.calls[0][0].options).toMatchSnapshot();
});
it(`Create ${riskScoreEntity} dashboards`, () => {
expect(
(bulkCreatePrebuiltSavedObjects as jest.Mock).mock.calls[0][0].options.templateName
).toEqual(`${riskScoreEntity}RiskScoreDashboards`);
});
it(`Create script: ml_${RiskScoreEntity.user}riskscore_map_script_${mockSpaceId}`, async () => {
expect((api.createStoredScript as jest.Mock).mock.calls[1][0].options).toMatchSnapshot();
});
it(`Create script: ml_${RiskScoreEntity.user}riskscore_reduce_script_${mockSpaceId}`, async () => {
expect((api.createStoredScript as jest.Mock).mock.calls[2][0].options).toMatchSnapshot();
});
it(`Create IngestPipeline: ml_${RiskScoreEntity.user}riskscore_ingest_pipeline_${mockSpaceId}`, async () => {
expect((api.createIngestPipeline as jest.Mock).mock.calls[0][0].options).toMatchSnapshot();
});
it(`Create Index: ml_${RiskScoreEntity.user}_risk_score_${mockSpaceId}`, async () => {
expect((api.createIndices as jest.Mock).mock.calls[0][0].options).toMatchSnapshot();
});
it(`Create Index: ml_${RiskScoreEntity.user}_risk_score_latest_${mockSpaceId}`, async () => {
expect((api.createIndices as jest.Mock).mock.calls[1][0].options).toMatchSnapshot();
});
it(`Create Transform: ml_${RiskScoreEntity.user}riskscore_pivot_transform_${mockSpaceId}`, async () => {
expect((api.createTransform as jest.Mock).mock.calls[0][0].options).toMatchSnapshot();
});
it(`Create Transform: ml_${RiskScoreEntity.user}riskscore_latest_transform_${mockSpaceId}`, async () => {
expect((api.createTransform as jest.Mock).mock.calls[1][0].options).toMatchSnapshot();
});
it(`Start Transforms`, () => {
expect((api.startTransforms as jest.Mock).mock.calls[0][0].transformIds).toMatchSnapshot();
});
it(`Create Users dashboards`, () => {
expect(
(bulkCreatePrebuiltSavedObjects as jest.Mock).mock.calls[0][0].options.templateName
).toEqual(`${RiskScoreEntity.user}RiskScoreDashboards`);
});
it('Refresh module', () => {
expect(mockRefetch).toBeCalled();
});
});
it('Refresh module', () => {
expect(mockRefetch).toBeCalled();
});
}
);
describe.each([[RiskScoreEntity.host], [RiskScoreEntity.user]])(
'uninstallLegacyRiskScoreModule - %s',
@ -225,9 +132,9 @@ describe.each([[RiskScoreEntity.host], [RiskScoreEntity.user]])(
beforeAll(async () => {
await restartRiskScoreTransforms({
http: mockHttp,
spaceId: 'customSpace',
refetch: mockRefetch,
riskScoreEntity,
spaceId: mockSpaceId,
});
});

View file

@ -11,28 +11,22 @@ import * as utils from '../../../../common/utils/risk_score_modules';
import type { inputsModel } from '../../../common/store';
import {
createIngestPipeline,
createIndices,
createStoredScript,
createTransform,
startTransforms,
deleteStoredScripts,
deleteTransforms,
deleteIngestPipelines,
stopTransforms,
bulkCreatePrebuiltSavedObjects,
bulkDeletePrebuiltSavedObjects,
installRiskScore,
bulkCreatePrebuiltSavedObjects,
stopTransforms,
startTransforms,
} from '../../containers/onboarding/api';
import {
INGEST_PIPELINE_DELETION_ERROR_MESSAGE,
INSTALLATION_ERROR,
START_TRANSFORMS_ERROR_MESSAGE,
TRANSFORM_CREATION_ERROR_MESSAGE,
TRANSFORM_DELETION_ERROR_MESSAGE,
UNINSTALLATION_ERROR,
} from '../../containers/onboarding/api/translations';
interface InstallRiskyScoreModule {
interface InstallRiskScoreModule {
dashboard?: DashboardStart;
http: HttpSetup;
notifications?: NotificationsStart;
@ -48,7 +42,7 @@ interface InstallRiskyScoreModule {
};
}
type UpgradeRiskyScoreModule = InstallRiskyScoreModule;
type UpgradeRiskScoreModule = InstallRiskScoreModule;
const installHostRiskScoreModule = async ({
dashboard,
@ -57,157 +51,16 @@ const installHostRiskScoreModule = async ({
refetch,
renderDashboardLink,
renderDocLink,
spaceId = 'default',
theme,
timerange,
}: InstallRiskyScoreModule) => {
/**
* console_templates/enable_host_risk_score.console
* Step 1 Upload script: ml_hostriskscore_levels_script_{spaceId}
*/
await createStoredScript({
}: InstallRiskScoreModule) => {
await installRiskScore({
http,
theme,
renderDocLink,
notifications,
options: utils.getRiskHostCreateLevelScriptOptions(spaceId),
});
/**
* console_templates/enable_host_risk_score.console
* Step 2 Upload script: ml_hostriskscore_init_script_{spaceId}
*/
await createStoredScript({
http,
theme,
renderDocLink,
notifications,
options: utils.getRiskHostCreateInitScriptOptions(spaceId),
});
/**
* console_templates/enable_host_risk_score.console
* Step 3 Upload script: ml_hostriskscore_map_script_{spaceId}
*/
await createStoredScript({
http,
theme,
renderDocLink,
notifications,
options: utils.getRiskHostCreateMapScriptOptions(spaceId),
});
/**
* console_templates/enable_host_risk_score.console
* Step 4 Upload script: ml_hostriskscore_reduce_script_{spaceId}
*/
await createStoredScript({
http,
theme,
renderDocLink,
notifications,
options: utils.getRiskHostCreateReduceScriptOptions(spaceId),
});
/**
* console_templates/enable_host_risk_score.console
* Step 5 Upload the ingest pipeline: ml_hostriskscore_ingest_pipeline_{spaceId}
*/
await createIngestPipeline({
http,
theme,
renderDocLink,
notifications,
options: utils.getRiskScoreIngestPipelineOptions(RiskScoreEntity.host, spaceId),
});
/**
* console_templates/enable_host_risk_score.console
* Step 6 create ml_host_risk_score_{spaceId} index
*/
await createIndices({
http,
theme,
renderDocLink,
notifications,
options: utils.getCreateRiskScoreIndicesOptions({
spaceId,
options: {
riskScoreEntity: RiskScoreEntity.host,
}),
});
/**
* console_templates/enable_host_risk_score.console
* Step 7 create transform: ml_hostriskscore_pivot_transform_{spaceId}
*/
await createTransform({
http,
theme,
renderDocLink,
notifications,
errorMessage: `${INSTALLATION_ERROR} - ${TRANSFORM_CREATION_ERROR_MESSAGE}`,
transformId: utils.getRiskScorePivotTransformId(RiskScoreEntity.host, spaceId),
options: utils.getCreateMLHostPivotTransformOptions({ spaceId }),
});
/**
* console_templates/enable_host_risk_score.console
* Step 9 create ml_host_risk_score_latest_{spaceId} index
*/
await createIndices({
http,
theme,
renderDocLink,
notifications,
options: utils.getCreateRiskScoreLatestIndicesOptions({
spaceId,
riskScoreEntity: RiskScoreEntity.host,
}),
});
/**
* console_templates/enable_host_risk_score.console
* Step 10 create transform: ml_hostriskscore_latest_transform_{spaceId}
*/
await createTransform({
http,
theme,
renderDocLink,
notifications,
errorMessage: `${INSTALLATION_ERROR} - ${TRANSFORM_CREATION_ERROR_MESSAGE}`,
transformId: utils.getRiskScoreLatestTransformId(RiskScoreEntity.host, spaceId),
options: utils.getCreateLatestTransformOptions({
spaceId,
riskScoreEntity: RiskScoreEntity.host,
}),
});
/**
* console_templates/enable_host_risk_score.console
* Step 8 Start the pivot transform
* Step 11 Start the latest transform
*/
const transformIds = [
utils.getRiskScorePivotTransformId(RiskScoreEntity.host, spaceId),
utils.getRiskScoreLatestTransformId(RiskScoreEntity.host, spaceId),
];
await startTransforms({
http,
theme,
renderDocLink,
notifications,
errorMessage: `${INSTALLATION_ERROR} - ${START_TRANSFORMS_ERROR_MESSAGE(transformIds.length)}`,
transformIds,
});
await restartRiskScoreTransforms({
http,
notifications,
refetch,
renderDocLink,
riskScoreEntity: RiskScoreEntity.host,
spaceId,
theme,
},
});
// Install dashboards and relevant saved objects
@ -239,144 +92,14 @@ const installUserRiskScoreModule = async ({
spaceId = 'default',
theme,
timerange,
}: InstallRiskyScoreModule) => {
/**
* console_templates/enable_user_risk_score.console
* Step 1 Upload script: ml_userriskscore_levels_script_{spaceId}
*/
await createStoredScript({
}: InstallRiskScoreModule) => {
await installRiskScore({
http,
theme,
renderDocLink,
notifications,
options: utils.getRiskUserCreateLevelScriptOptions(spaceId),
});
/**
* console_templates/enable_user_risk_score.console
* Step 2 Upload script: ml_userriskscore_map_script_{spaceId}
*/
await createStoredScript({
http,
theme,
renderDocLink,
notifications,
options: utils.getRiskUserCreateMapScriptOptions(spaceId),
});
/**
* console_templates/enable_user_risk_score.console
* Step 3 Upload script: ml_userriskscore_reduce_script_{spaceId}
*/
await createStoredScript({
http,
theme,
renderDocLink,
notifications,
options: utils.getRiskUserCreateReduceScriptOptions(spaceId),
});
/**
* console_templates/enable_user_risk_score.console
* Step 4 Upload ingest pipeline: ml_userriskscore_ingest_pipeline_{spaceId}
*/
await createIngestPipeline({
http,
theme,
renderDocLink,
notifications,
options: utils.getRiskScoreIngestPipelineOptions(RiskScoreEntity.user, spaceId),
});
/**
* console_templates/enable_user_risk_score.console
* Step 5 create ml_user_risk_score_{spaceId} index
*/
await createIndices({
http,
theme,
renderDocLink,
notifications,
options: utils.getCreateRiskScoreIndicesOptions({
spaceId,
options: {
riskScoreEntity: RiskScoreEntity.user,
}),
});
/**
* console_templates/enable_user_risk_score.console
* Step 6 create Transform: ml_userriskscore_pivot_transform_{spaceId}
*/
await createTransform({
http,
theme,
renderDocLink,
notifications,
errorMessage: `${INSTALLATION_ERROR} - ${TRANSFORM_CREATION_ERROR_MESSAGE}`,
transformId: utils.getRiskScorePivotTransformId(RiskScoreEntity.user, spaceId),
options: utils.getCreateMLUserPivotTransformOptions({ spaceId }),
});
/**
* console_templates/enable_user_risk_score.console
* Step 8 create ml_user_risk_score_latest_{spaceId} index
*/
await createIndices({
http,
theme,
renderDocLink,
notifications,
options: utils.getCreateRiskScoreLatestIndicesOptions({
spaceId,
riskScoreEntity: RiskScoreEntity.user,
}),
});
/**
* console_templates/enable_user_risk_score.console
* Step 9 create Transform: ml_userriskscore_latest_transform_{spaceId}
*/
await createTransform({
http,
theme,
renderDocLink,
notifications,
errorMessage: `${INSTALLATION_ERROR} - ${TRANSFORM_CREATION_ERROR_MESSAGE}`,
transformId: utils.getRiskScoreLatestTransformId(RiskScoreEntity.user, spaceId),
options: utils.getCreateLatestTransformOptions({
spaceId,
riskScoreEntity: RiskScoreEntity.user,
}),
});
/**
* console_templates/enable_user_risk_score.console
* Step 7 Start the pivot transform
* Step 10 Start the latest transform
*/
const transformIds = [
utils.getRiskScorePivotTransformId(RiskScoreEntity.user, spaceId),
utils.getRiskScoreLatestTransformId(RiskScoreEntity.user, spaceId),
];
await startTransforms({
errorMessage: `${INSTALLATION_ERROR} - ${START_TRANSFORMS_ERROR_MESSAGE(transformIds.length)}`,
http,
notifications,
renderDocLink,
theme,
transformIds,
});
/**
* Restart transform immediately to force it pick up the alerts data.
* This can effectively reduce the chance of no data appears once installation complete.
* */
await restartRiskScoreTransforms({
http,
notifications,
refetch,
renderDocLink,
riskScoreEntity: RiskScoreEntity.user,
spaceId,
theme,
},
});
// Install dashboards and relevant saved objects
@ -398,7 +121,7 @@ const installUserRiskScoreModule = async ({
}
};
export const installRiskScoreModule = async (settings: InstallRiskyScoreModule) => {
export const installRiskScoreModule = async (settings: InstallRiskScoreModule) => {
if (settings.riskScoreEntity === RiskScoreEntity.user) {
await installUserRiskScoreModule(settings);
} else {
@ -442,68 +165,67 @@ export const uninstallLegacyRiskScoreModule = async ({
const legacyIngestPipelineNames = [utils.getLegacyIngestPipelineName(riskScoreEntity)];
/**
* Intended not to pass notification to bulkDeletePrebuiltSavedObjects.
* As the only error it can happen is saved object not found, and
* that is what bulkDeletePrebuiltSavedObjects wants.
* (Before 8.5 once an saved object was created, it was shared across different spaces.
* If it has been upgrade in one space, "saved object not found" will happen when upgrading other spaces.
* Or it could be users manually deleted the saved object.)
*/
await bulkDeletePrebuiltSavedObjects({
http,
options: {
templateName: `${riskScoreEntity}RiskScoreDashboards`,
},
});
await deleteTransforms({
http,
theme,
renderDocLink,
notifications,
errorMessage: `${UNINSTALLATION_ERROR} - ${TRANSFORM_DELETION_ERROR_MESSAGE(
legacyTransformIds.length
)}`,
transformIds: legacyTransformIds,
options: {
deleteDestIndex: true,
deleteDestDataView: true,
forceDelete: false,
},
});
/**
* Intended not to pass notification to deleteIngestPipelines.
* As the only error it can happen is ingest pipeline not found, and
* that is what deleteIngestPipelines wants.
* (Before 8.5 once an ingest pipeline was created, it was shared across different spaces.
* If it has been upgrade in one space, "ingest pipeline not found" will happen when upgrading other spaces.
* Or it could be users manually deleted the ingest pipeline.)
*/
await deleteIngestPipelines({
http,
errorMessage: `${UNINSTALLATION_ERROR} - ${INGEST_PIPELINE_DELETION_ERROR_MESSAGE(
legacyIngestPipelineNames.length
)}`,
names: legacyIngestPipelineNames.join(','),
});
/**
* Intended not to pass notification to deleteStoredScripts.
* As the only error it can happen is script not found, and
* that is what deleteStoredScripts wants.
* (Before 8.5 once a script was created, it was shared across different spaces.
* If it has been upgrade in one space, "script not found" will happen when upgrading other spaces.
* Or it could be users manually deleted the script.)
*/
await deleteStoredScripts({
http,
ids:
riskScoreEntity === RiskScoreEntity.user
? legacyRiskScoreUsersScriptIds
: legacyRiskScoreHostsScriptIds,
});
await Promise.all([
/**
* Intended not to pass notification to bulkDeletePrebuiltSavedObjects.
* As the only error it can happen is saved object not found, and
* that is what bulkDeletePrebuiltSavedObjects wants.
* (Before 8.5 once an saved object was created, it was shared across different spaces.
* If it has been upgrade in one space, "saved object not found" will happen when upgrading other spaces.
* Or it could be users manually deleted the saved object.)
*/
bulkDeletePrebuiltSavedObjects({
http,
options: {
templateName: `${riskScoreEntity}RiskScoreDashboards`,
},
}),
deleteTransforms({
http,
theme,
renderDocLink,
notifications,
errorMessage: `${UNINSTALLATION_ERROR} - ${TRANSFORM_DELETION_ERROR_MESSAGE(
legacyTransformIds.length
)}`,
transformIds: legacyTransformIds,
options: {
deleteDestIndex: true,
deleteDestDataView: true,
forceDelete: false,
},
}),
/**
* Intended not to pass notification to deleteIngestPipelines.
* As the only error it can happen is ingest pipeline not found, and
* that is what deleteIngestPipelines wants.
* (Before 8.5 once an ingest pipeline was created, it was shared across different spaces.
* If it has been upgrade in one space, "ingest pipeline not found" will happen when upgrading other spaces.
* Or it could be users manually deleted the ingest pipeline.)
*/
deleteIngestPipelines({
http,
errorMessage: `${UNINSTALLATION_ERROR} - ${INGEST_PIPELINE_DELETION_ERROR_MESSAGE(
legacyIngestPipelineNames.length
)}`,
names: legacyIngestPipelineNames.join(','),
}),
/**
* Intended not to pass notification to deleteStoredScripts.
* As the only error it can happen is script not found, and
* that is what deleteStoredScripts wants.
* (Before 8.5 once a script was created, it was shared across different spaces.
* If it has been upgrade in one space, "script not found" will happen when upgrading other spaces.
* Or it could be users manually deleted the script.)
*/
deleteStoredScripts({
http,
ids:
riskScoreEntity === RiskScoreEntity.user
? legacyRiskScoreUsersScriptIds
: legacyRiskScoreHostsScriptIds,
}),
]);
if (refetch) {
refetch();
@ -520,7 +242,7 @@ export const upgradeHostRiskScoreModule = async ({
spaceId = 'default',
theme,
timerange,
}: UpgradeRiskyScoreModule) => {
}: UpgradeRiskScoreModule) => {
await uninstallLegacyRiskScoreModule({
http,
notifications,
@ -553,7 +275,7 @@ export const upgradeUserRiskScoreModule = async ({
spaceId = 'default',
theme,
timerange,
}: UpgradeRiskyScoreModule) => {
}: UpgradeRiskScoreModule) => {
await uninstallLegacyRiskScoreModule({
http,
notifications,
@ -583,7 +305,6 @@ export const restartRiskScoreTransforms = async ({
renderDocLink,
riskScoreEntity,
spaceId,
theme,
}: {
http: HttpSetup;
notifications?: NotificationsStart;
@ -591,7 +312,6 @@ export const restartRiskScoreTransforms = async ({
renderDocLink?: (message: string) => React.ReactNode;
riskScoreEntity: RiskScoreEntity;
spaceId?: string;
theme?: ThemeServiceStart;
}) => {
const transformIds = [
utils.getRiskScorePivotTransformId(riskScoreEntity, spaceId),
@ -602,7 +322,6 @@ export const restartRiskScoreTransforms = async ({
http,
notifications,
renderDocLink,
theme,
transformIds,
});
@ -610,7 +329,6 @@ export const restartRiskScoreTransforms = async ({
http,
notifications,
renderDocLink,
theme,
transformIds,
});

View file

@ -9,3 +9,4 @@ export * from './ingest_pipelines';
export * from './transforms';
export * from './stored_scripts';
export * from './saved_objects';
export * from './onboarding';

View file

@ -0,0 +1,88 @@
/*
* 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 { HttpSetup, NotificationsStart } from '@kbn/core/public';
import { INTERNAL_RISK_SCORE_URL } from '../../../../../common/constants';
import { RiskScoreEntity } from '../../../../../common/search_strategy';
import {
HOST_RISK_SCORES_ENABLED_TITLE,
INSTALLATION_ERROR,
RISK_SCORES_ENABLED_TEXT,
USER_RISK_SCORES_ENABLED_TITLE,
} from './translations';
interface Options {
riskScoreEntity: RiskScoreEntity;
}
type Response = Record<string, { success?: boolean; error?: Error }>;
const toastLifeTimeMs = 600000;
export const installRiskScore = ({
errorMessage,
http,
notifications,
options,
renderDocLink,
signal,
}: {
errorMessage?: string;
http: HttpSetup;
notifications?: NotificationsStart;
options: Options;
renderDocLink?: (message: string) => React.ReactNode;
signal?: AbortSignal;
}) => {
return http
.post<Response[]>(INTERNAL_RISK_SCORE_URL, {
body: JSON.stringify(options),
signal,
})
.then((result) => {
const resp = result.reduce(
(acc, curr) => {
const [[key, res]] = Object.entries(curr);
if (res.success) {
return res.success != null ? { ...acc, success: [...acc.success, `${key}`] } : acc;
} else {
return res.error != null
? { ...acc, error: [...acc.error, `${key}: ${res?.error?.message}`] }
: acc;
}
},
{ success: [] as string[], error: [] as string[] }
);
if (resp.error.length > 0) {
notifications?.toasts?.addError(new Error(errorMessage ?? INSTALLATION_ERROR), {
title: errorMessage ?? INSTALLATION_ERROR,
toastMessage: renderDocLink
? (renderDocLink(resp.error.join(', ')) as unknown as string)
: resp.error.join(', '),
toastLifeTimeMs,
});
} else {
notifications?.toasts?.addSuccess({
'data-test-subj': `${options.riskScoreEntity}EnableSuccessToast`,
title:
options.riskScoreEntity === RiskScoreEntity.user
? USER_RISK_SCORES_ENABLED_TITLE
: HOST_RISK_SCORES_ENABLED_TITLE,
text: RISK_SCORES_ENABLED_TEXT(resp.success.join(', ')),
});
}
})
.catch((e) => {
notifications?.toasts?.addError(new Error(errorMessage ?? INSTALLATION_ERROR), {
title: errorMessage ?? INSTALLATION_ERROR,
toastMessage: renderDocLink ? renderDocLink(e?.body?.message) : e?.body?.message,
toastLifeTimeMs,
});
});
};

View file

@ -5,13 +5,7 @@
* 2.0.
*/
import type {
HttpSetup,
NotificationsStart,
SavedObject,
SavedObjectAttributes,
ThemeServiceStart,
} from '@kbn/core/public';
import type { HttpSetup, NotificationsStart, ThemeServiceStart } from '@kbn/core/public';
import { toMountPoint } from '@kbn/kibana-react-plugin/public';
import type { DashboardStart } from '@kbn/dashboard-plugin/public';
import { RISKY_HOSTS_DASHBOARD_TITLE, RISKY_USERS_DASHBOARD_TITLE } from '../../../constants';
@ -30,8 +24,10 @@ import {
const toastLifeTimeMs = 600000;
type DashboardsSavedObjectTemplate = `${RiskScoreEntity}RiskScoreDashboards`;
interface Options {
templateName: string;
templateName: DashboardsSavedObjectTemplate;
}
export const bulkCreatePrebuiltSavedObjects = async ({
@ -58,20 +54,24 @@ export const bulkCreatePrebuiltSavedObjects = async ({
theme?: ThemeServiceStart;
}) => {
const res = await http
.post<{ saved_objects: Array<SavedObject<SavedObjectAttributes>> }>(
prebuiltSavedObjectsBulkCreateUrl(options.templateName)
)
.post<
Record<
DashboardsSavedObjectTemplate,
{
success?: boolean;
error: Error;
body?: Array<{ type: string; title: string; id: string; name: string }>;
}
>
>(prebuiltSavedObjectsBulkCreateUrl(options.templateName))
.then((result) => {
const errors = result.saved_objects.reduce<string[]>((acc, o) => {
return o.error != null ? [...acc, `${o.id}: ${o.error.message}`] : acc;
}, []);
const response = result[options.templateName];
const error = response?.error?.message;
if (errors.length > 0) {
if (error) {
notifications?.toasts?.addError(new Error(errorMessage ?? IMPORT_SAVED_OBJECTS_FAILURE), {
title: errorMessage ?? IMPORT_SAVED_OBJECTS_FAILURE,
toastMessage: renderDocLink
? (renderDocLink(errors.join(', ')) as unknown as string)
: errors.join(', '),
toastMessage: renderDocLink ? (renderDocLink(error) as unknown as string) : error,
toastLifeTimeMs,
});
} else {
@ -80,8 +80,8 @@ export const bulkCreatePrebuiltSavedObjects = async ({
? RISKY_USERS_DASHBOARD_TITLE
: RISKY_HOSTS_DASHBOARD_TITLE;
const targetDashboard = result.saved_objects.find(
(obj) => obj.type === 'dashboard' && obj?.attributes?.title === dashboardTitle
const targetDashboard = response?.body?.find(
(obj) => obj.type === 'dashboard' && obj?.title === dashboardTitle
);
let targetUrl;
@ -95,12 +95,15 @@ export const bulkCreatePrebuiltSavedObjects = async ({
});
}
const successMessage = result.saved_objects
.map((o) => o?.attributes?.title ?? o?.attributes?.name)
.join(', ');
const successMessage = response?.body?.map((o) => o?.title ?? o?.name).join(', ');
if (successMessage == null || response?.body?.length == null) {
return;
}
notifications?.toasts?.addSuccess({
title: IMPORT_SAVED_OBJECTS_SUCCESS(result.saved_objects.length),
'data-test-subj': `${options.templateName}SuccessToast`,
title: IMPORT_SAVED_OBJECTS_SUCCESS(response?.body?.length),
text: toMountPoint(
renderDashboardLink && targetUrl
? renderDashboardLink(successMessage, targetUrl)
@ -109,6 +112,7 @@ export const bulkCreatePrebuiltSavedObjects = async ({
theme$: theme?.theme$,
}
),
toastLifeTimeMs,
});
}
})

View file

@ -5,9 +5,11 @@
* 2.0.
*/
import { RISK_SCORE_RESTART_TRANSFORMS } from '../../../../../common/constants';
import {
GET_TRANSFORM_STATE_ERROR_MESSAGE,
GET_TRANSFORM_STATE_NOT_FOUND_MESSAGE,
RESTART_TRANSFORMS_ERROR_MESSAGE,
START_TRANSFORMS_ERROR_MESSAGE,
STOP_TRANSFORMS_ERROR_MESSAGE,
TRANSFORM_CREATION_ERROR_MESSAGE,
@ -20,6 +22,8 @@ import type {
DeleteTransformsResult,
GetTransformsState,
GetTransformState,
RestartTransforms,
RestartTransformResult,
StartTransforms,
StartTransformsResult,
StopTransforms,
@ -318,3 +322,49 @@ export async function deleteTransforms({
return res;
}
export async function restartTransforms({
http,
notifications,
renderDocLink,
signal,
errorMessage,
riskScoreEntity,
}: RestartTransforms) {
const res = await http
.post<RestartTransformResult[]>(`${RISK_SCORE_RESTART_TRANSFORMS}`, {
body: JSON.stringify({ riskScoreEntity }),
signal,
})
.then((result) => {
const failedIds = result.reduce<string[]>((acc, curr) => {
const [[key, val]] = Object.entries(curr);
return !val.success
? [...acc, val?.error?.message ? `${key}: ${val?.error?.message}` : key]
: acc;
}, []);
const errorMessageTitle = errorMessage ?? RESTART_TRANSFORMS_ERROR_MESSAGE(failedIds.length);
if (failedIds.length > 0) {
notifications?.toasts?.addError(new Error(errorMessageTitle), {
title: errorMessageTitle,
toastMessage: getErrorToastMessage({
messageBody: failedIds.join(', '),
renderDocLink,
}),
toastLifeTimeMs,
});
}
return result;
})
.catch((e) => {
notifications?.toasts?.addError(e, {
title: errorMessage ?? RESTART_TRANSFORMS_ERROR_MESSAGE(),
toastMessage: getErrorToastMessage({ messageBody: e?.body?.message, renderDocLink }),
toastLifeTimeMs,
});
});
return res;
}

View file

@ -80,6 +80,12 @@ export const START_TRANSFORMS_ERROR_MESSAGE = (totalCount: number) =>
defaultMessage: `Failed to start {totalCount, plural, =1 {Transform} other {Transforms}}`,
});
export const RESTART_TRANSFORMS_ERROR_MESSAGE = (totalCount?: number) =>
i18n.translate('xpack.securitySolution.riskScore.api.transforms.start.errorMessageTitle', {
values: { totalCount },
defaultMessage: `Failed to start {totalCount, plural, =1 {Transform} other {Transforms}}`,
});
export const STOP_TRANSFORMS_ERROR_MESSAGE = (totalCount: number) =>
i18n.translate('xpack.securitySolution.riskScore.api.transforms.stop.errorMessageTitle', {
values: { totalCount },
@ -119,3 +125,23 @@ export const DELETE_SAVED_OBJECTS_FAILURE = i18n.translate(
defaultMessage: `Failed to delete saved objects`,
}
);
export const HOST_RISK_SCORES_ENABLED_TITLE = i18n.translate(
'xpack.securitySolution.riskScore.hostRiskScoresEnabledTitle',
{
defaultMessage: `Host Risk Scores enabled`,
}
);
export const USER_RISK_SCORES_ENABLED_TITLE = i18n.translate(
'xpack.securitySolution.riskScore.userRiskScoresEnabledTitle',
{
defaultMessage: `User Risk Scores enabled`,
}
);
export const RISK_SCORES_ENABLED_TEXT = (items: string) =>
i18n.translate('xpack.securitySolution.riskScore.savedObjects.enableRiskScoreSuccessTitle', {
values: { items },
defaultMessage: `{items} imported successfully`,
});

View file

@ -5,6 +5,8 @@
* 2.0.
*/
import type { HttpSetup, NotificationsStart, ThemeServiceStart } from '@kbn/core/public';
import type { OutputError } from '@kbn/securitysolution-es-utils';
import type { RiskScoreEntity } from '../../../../../common/search_strategy/security_solution/risk_score/common';
interface RiskyScoreApiBase {
errorMessage?: string;
@ -71,6 +73,10 @@ export interface StartTransforms extends RiskyScoreApiBase {
transformIds: string[];
}
export interface RestartTransforms extends RiskyScoreApiBase {
riskScoreEntity: RiskScoreEntity;
}
interface TransformResult {
success: boolean;
error?: { root_cause?: unknown; type?: string; reason?: string };
@ -78,6 +84,11 @@ interface TransformResult {
export type StartTransformsResult = Record<string, TransformResult>;
export type RestartTransformResult = Record<
string,
{ success: boolean; error: OutputError | null }
>;
export interface StopTransforms extends RiskyScoreApiBase {
transformIds: string[];
}

View file

@ -10,7 +10,7 @@
* 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 {
serverMock,
requestContextMock,
@ -19,35 +19,37 @@ import {
import { createEsIndexRoute } from './create_index_route';
import { RISK_SCORE_CREATE_INDEX } from '../../../../common/constants';
import { createIndex } from './lib/create_index';
import { transformError } from '@kbn/securitysolution-es-utils';
const testIndex = 'test-index';
jest.mock('./lib/create_index', () => {
const actualModule = jest.requireActual('./lib/create_index');
return {
...actualModule,
createIndex: jest.fn(),
createIndex: jest.fn().mockResolvedValue({ [testIndex]: { success: true, error: null } }),
};
});
describe('createEsIndexRoute', () => {
let server: ReturnType<typeof serverMock.create>;
let { context } = requestContextMock.createTools();
const logger = { error: jest.fn() } as unknown as Logger;
beforeEach(() => {
jest.resetModules();
jest.resetAllMocks();
jest.clearAllMocks();
server = serverMock.create();
({ context } = requestContextMock.createTools());
createEsIndexRoute(server.router);
createEsIndexRoute(server.router, logger);
});
it('create index', async () => {
const request = requestMock.create({
method: 'put',
path: RISK_SCORE_CREATE_INDEX,
body: { index: 'test-index', mappings: {} },
body: { index: testIndex, mappings: {} },
});
const response = await server.inject(request, requestContextMock.convertContext(context));
expect(createIndex).toHaveBeenCalled();
expect(response.status).toEqual(200);
@ -63,4 +65,20 @@ describe('createEsIndexRoute', () => {
expect(result.ok).not.toHaveBeenCalled();
});
it('return error if failed to create index', async () => {
(createIndex as jest.Mock).mockResolvedValue({
[testIndex]: { success: false, error: transformError(new Error('unknown error')) },
});
const request = requestMock.create({
method: 'put',
path: RISK_SCORE_CREATE_INDEX,
body: { index: testIndex, mappings: {} },
});
const response = await server.inject(request, requestContextMock.convertContext(context));
expect(createIndex).toHaveBeenCalled();
expect(response.status).toEqual(500);
expect(response.body).toEqual({ message: 'unknown error', status_code: 500 });
});
});

View file

@ -5,13 +5,15 @@
* 2.0.
*/
import { transformError } from '@kbn/securitysolution-es-utils';
import { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils';
import type { Logger } from '@kbn/core/server';
import { transformError } from '@kbn/securitysolution-es-utils';
import { RISK_SCORE_CREATE_INDEX } from '../../../../common/constants';
import type { SecuritySolutionPluginRouter } from '../../../types';
import { createEsIndexBodySchema, createIndex } from './lib/create_index';
export const createEsIndexRoute = (router: SecuritySolutionPluginRouter) => {
export const createEsIndexRoute = (router: SecuritySolutionPluginRouter, logger: Logger) => {
router.put(
{
path: RISK_SCORE_CREATE_INDEX,
@ -23,15 +25,27 @@ export const createEsIndexRoute = (router: SecuritySolutionPluginRouter) => {
async (context, request, response) => {
const siemResponse = buildSiemResponse(response);
const { client } = (await context.core).elasticsearch;
const esClient = client.asCurrentUser;
const options = request.body;
try {
await createIndex({
client,
const result = await createIndex({
esClient,
logger,
options,
});
return response.ok({ body: options });
} catch (err) {
const error = transformError(err);
const error = result[options.index].error;
if (error != null) {
return siemResponse.error({
body: error.message,
statusCode: error.statusCode,
});
} else {
return response.ok({ body: options });
}
} catch (e) {
const error = transformError(e);
return siemResponse.error({
body: error.message,
statusCode: error.statusCode,

View file

@ -6,21 +6,38 @@
*/
import type { TypeOf } from '@kbn/config-schema';
import { schema } from '@kbn/config-schema';
import type { IScopedClusterClient } from '@kbn/core-elasticsearch-server';
import type { ElasticsearchClient, Logger } from '@kbn/core/server';
import { transformError } from '@kbn/securitysolution-es-utils';
export const createEsIndexBodySchema = schema.object({
index: schema.string({ minLength: 1 }),
mappings: schema.maybe(schema.recordOf(schema.string({ minLength: 1 }), schema.any())),
mappings: schema.maybe(
schema.oneOf([schema.string(), schema.recordOf(schema.string({ minLength: 1 }), schema.any())])
),
});
type CreateEsIndexBodySchema = TypeOf<typeof createEsIndexBodySchema>;
export const createIndex = async ({
client,
esClient,
logger,
options,
}: {
client: IScopedClusterClient;
esClient: ElasticsearchClient;
logger: Logger;
options: CreateEsIndexBodySchema;
}) => {
await client.asCurrentUser.indices.create(options);
try {
await esClient.indices.create({
index: options.index,
mappings:
typeof options.mappings === 'string' ? JSON.parse(options.mappings) : options.mappings,
});
return { [options.index]: { success: true, error: null } };
} catch (err) {
const error = transformError(err);
logger.error(`Failed to create index: ${options.index}: ${error.message}`);
return { [options.index]: { success: false, error } };
}
};

View file

@ -0,0 +1,44 @@
/*
* 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 { transformError } from '@kbn/securitysolution-es-utils';
import type { ElasticsearchClient, Logger } from '@kbn/core/server';
import type { Pipeline } from '../../../../../common/types/risk_scores';
export const createIngestPipeline = async ({
esClient,
logger,
options,
}: {
esClient: ElasticsearchClient;
logger: Logger;
options: Pipeline;
}) => {
const processors =
typeof options.processors === 'string' ? JSON.parse(options.processors) : options.processors;
// eslint-disable-next-line @typescript-eslint/naming-convention
const { name, description, version, on_failure } = options;
try {
await esClient.ingest.putPipeline({
id: name,
body: {
description,
processors,
version,
on_failure,
},
});
return { [name]: { success: true, error: null } };
} catch (err) {
const error = transformError(err);
logger.error(`Failed to create ingest pipeline: ${name}: ${error.message}`);
return { [name]: { success: false, error } };
}
};

View file

@ -0,0 +1,313 @@
/*
* 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, Logger } from '@kbn/core/server';
import { RiskScoreEntity } from '../../../../../common/search_strategy';
import {
getCreateLatestTransformOptions,
getCreateMLHostPivotTransformOptions,
getCreateMLUserPivotTransformOptions,
getCreateRiskScoreIndicesOptions,
getCreateRiskScoreLatestIndicesOptions,
getRiskHostCreateInitScriptOptions,
getRiskHostCreateLevelScriptOptions,
getRiskHostCreateMapScriptOptions,
getRiskHostCreateReduceScriptOptions,
getRiskScoreIngestPipelineOptions,
getRiskScoreLatestTransformId,
getRiskScorePivotTransformId,
getRiskUserCreateLevelScriptOptions,
getRiskUserCreateMapScriptOptions,
getRiskUserCreateReduceScriptOptions,
} from '../../../../../common/utils/risk_score_modules';
import { createIndex } from '../../indices/lib/create_index';
import { createStoredScript } from '../../stored_scripts/lib/create_script';
import { createAndStartTransform } from '../../transform/helpers/transforms';
import { createIngestPipeline } from './ingest_pipeline';
interface InstallRiskScoreModule {
esClient: ElasticsearchClient;
logger: Logger;
riskScoreEntity: RiskScoreEntity;
spaceId: string;
}
const createHostRiskScoreIngestPipelineGrouping = ({
esClient,
logger,
riskScoreEntity,
spaceId,
}: InstallRiskScoreModule) => {
/**
* console_templates/enable_host_risk_score.console
* Step 1 Upload script: ml_hostriskscore_levels_script_{spaceId}
*/
const createLevelScriptOptions = getRiskHostCreateLevelScriptOptions(spaceId);
return createStoredScript({
esClient,
logger,
options: createLevelScriptOptions,
}).then((createStoredScriptResult) => {
if (createStoredScriptResult[createLevelScriptOptions.id].success) {
/**
* console_templates/enable_host_risk_score.console
* Step 5 Upload the ingest pipeline: ml_hostriskscore_ingest_pipeline_{spaceId}
*/
const createIngestPipelineOptions = getRiskScoreIngestPipelineOptions(
riskScoreEntity,
spaceId
);
return createIngestPipeline({
esClient,
logger,
options: createIngestPipelineOptions,
}).then((createIngestPipelineResult) => {
return [createStoredScriptResult, createIngestPipelineResult];
});
} else {
return [createStoredScriptResult];
}
});
};
const installHostRiskScoreModule = async ({
esClient,
riskScoreEntity,
logger,
spaceId,
}: InstallRiskScoreModule) => {
const result = await Promise.all([
/**
* console_templates/enable_host_risk_score.console
* Step 1 Upload script: ml_hostriskscore_levels_script_{spaceId}
* Step 5 Upload the ingest pipeline: ml_hostriskscore_ingest_pipeline_{spaceId}
*/
createHostRiskScoreIngestPipelineGrouping({
esClient,
logger,
riskScoreEntity,
spaceId,
}),
/**
* console_templates/enable_host_risk_score.console
* Step 2 Upload script: ml_hostriskscore_init_script_{spaceId}
*/
createStoredScript({
esClient,
logger,
options: getRiskHostCreateInitScriptOptions(spaceId),
}),
/**
* console_templates/enable_host_risk_score.console
* Step 3 Upload script: ml_hostriskscore_map_script_{spaceId}
*/
createStoredScript({
esClient,
logger,
options: getRiskHostCreateMapScriptOptions(spaceId),
}),
/**
* console_templates/enable_host_risk_score.console
* Step 4 Upload script: ml_hostriskscore_reduce_script_{spaceId}
*/
createStoredScript({
esClient,
logger,
options: getRiskHostCreateReduceScriptOptions(spaceId),
}),
/**
* console_templates/enable_host_risk_score.console
* Step 6 create ml_host_risk_score_{spaceId} index
*/
createIndex({
esClient,
logger,
options: getCreateRiskScoreIndicesOptions({
spaceId,
riskScoreEntity,
}),
}),
/**
* console_templates/enable_host_risk_score.console
* Step 9 create ml_host_risk_score_latest_{spaceId} index
*/
createIndex({
esClient,
logger,
options: getCreateRiskScoreLatestIndicesOptions({
spaceId,
riskScoreEntity,
}),
}),
]);
/**
* console_templates/enable_host_risk_score.console
* Step 7 create transform: ml_hostriskscore_pivot_transform_{spaceId}
* Step 8 Start the pivot transform
*/
const createAndStartPivotTransformResult = await createAndStartTransform({
esClient,
logger,
transformId: getRiskScorePivotTransformId(riskScoreEntity, spaceId),
options: getCreateMLHostPivotTransformOptions({ spaceId }),
});
/**
* console_templates/enable_host_risk_score.console
* Step 10 create transform: ml_hostriskscore_latest_transform_{spaceId}
* Step 11 Start the latest transform
*/
const createAndStartLatestTransformResult = await createAndStartTransform({
esClient,
logger,
transformId: getRiskScoreLatestTransformId(riskScoreEntity, spaceId),
options: getCreateLatestTransformOptions({ riskScoreEntity, spaceId }),
});
return [
...result,
createAndStartPivotTransformResult,
createAndStartLatestTransformResult,
].flat();
};
const createUserRiskScoreIngestPipelineGrouping = async ({
esClient,
logger,
riskScoreEntity,
spaceId,
}: InstallRiskScoreModule) => {
/**
* console_templates/enable_user_risk_score.console
* Step 1 Upload script: ml_userriskscore_levels_script_{spaceId}
*/
const createLevelScriptOptions = getRiskUserCreateLevelScriptOptions(spaceId);
const createStoredScriptResult = await createStoredScript({
esClient,
logger,
options: createLevelScriptOptions,
});
/**
* console_templates/enable_user_risk_score.console
* Step 4 Upload ingest pipeline: ml_userriskscore_ingest_pipeline_{spaceId}
*/
const createIngestPipelineOptions = getRiskScoreIngestPipelineOptions(riskScoreEntity, spaceId);
const createIngestPipelineResult = await createIngestPipeline({
esClient,
logger,
options: createIngestPipelineOptions,
});
return [createStoredScriptResult, createIngestPipelineResult];
};
const installUserRiskScoreModule = async ({
esClient,
logger,
riskScoreEntity,
spaceId,
}: InstallRiskScoreModule) => {
const result = await Promise.all([
/**
* console_templates/enable_user_risk_score.console
* Step 1 Upload script: ml_userriskscore_levels_script_{spaceId}
* Step 4 Upload ingest pipeline: ml_userriskscore_ingest_pipeline_{spaceId}
*/
createUserRiskScoreIngestPipelineGrouping({ esClient, logger, riskScoreEntity, spaceId }),
/**
* console_templates/enable_user_risk_score.console
* Step 2 Upload script: ml_userriskscore_map_script_{spaceId}
*/
createStoredScript({
esClient,
logger,
options: getRiskUserCreateMapScriptOptions(spaceId),
}),
/**
* console_templates/enable_user_risk_score.console
* Step 3 Upload script: ml_userriskscore_reduce_script_{spaceId}
*/
createStoredScript({
esClient,
logger,
options: getRiskUserCreateReduceScriptOptions(spaceId),
}),
/**
* console_templates/enable_user_risk_score.console
* Step 5 create ml_user_risk_score_{spaceId} index
*/
createIndex({
esClient,
logger,
options: getCreateRiskScoreIndicesOptions({
spaceId,
riskScoreEntity,
}),
}),
/**
* console_templates/enable_user_risk_score.console
* Step 8 create ml_user_risk_score_latest_{spaceId} index
*/
createIndex({
esClient,
logger,
options: getCreateRiskScoreLatestIndicesOptions({
spaceId,
riskScoreEntity,
}),
}),
]);
/**
* console_templates/enable_user_risk_score.console
* Step 6 create Transform: ml_userriskscore_pivot_transform_{spaceId}
* Step 7 Start the pivot transform
*/
const createAndStartPivotTransformResult = await createAndStartTransform({
esClient,
logger,
transformId: getRiskScorePivotTransformId(riskScoreEntity, spaceId),
options: getCreateMLUserPivotTransformOptions({ spaceId }),
});
/**
* console_templates/enable_user_risk_score.console
* Step 9 create Transform: ml_userriskscore_latest_transform_{spaceId}
* Step 10 Start the latest transform
*/
const createAndStartLatestTransformResult = await createAndStartTransform({
esClient,
logger,
transformId: getRiskScoreLatestTransformId(riskScoreEntity, spaceId),
options: getCreateLatestTransformOptions({ riskScoreEntity, spaceId }),
});
return [
...result,
createAndStartPivotTransformResult,
createAndStartLatestTransformResult,
].flat();
};
export const installRiskScoreModule = async (settings: InstallRiskScoreModule) => {
if (settings.riskScoreEntity === RiskScoreEntity.user) {
const result = await installUserRiskScoreModule(settings);
return result;
} else {
const result = await installHostRiskScoreModule(settings);
return result;
}
};

View file

@ -1,8 +1,8 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`installRiskScoreModule - host Create Index: ml_host_risk_score_customSpace 1`] = `
exports[`installRiskScoresRoute - host Create Index: ml_host_risk_score_latest_mockSpaceId 1`] = `
Object {
"index": "ml_host_risk_score_customSpace",
"index": "ml_host_risk_score_latest_default",
"mappings": Object {
"properties": Object {
"@timestamp": Object {
@ -54,9 +54,9 @@ Object {
}
`;
exports[`installRiskScoreModule - host Create Index: ml_host_risk_score_latest_customSpace 1`] = `
exports[`installRiskScoresRoute - host Create Index: ml_host_risk_score_mockSpaceId 1`] = `
Object {
"index": "ml_host_risk_score_latest_customSpace",
"index": "ml_host_risk_score_default",
"mappings": Object {
"properties": Object {
"@timestamp": Object {
@ -108,9 +108,9 @@ Object {
}
`;
exports[`installRiskScoreModule - host Create IngestPipeline: ml_hostriskscore_ingest_pipeline_customSpace 1`] = `
exports[`installRiskScoresRoute - host Create IngestPipeline: ml_hostriskscore_ingest_pipeline_mockSpaceId 1`] = `
Object {
"name": "ml_hostriskscore_ingest_pipeline_customSpace",
"name": "ml_hostriskscore_ingest_pipeline_default",
"processors": Array [
Object {
"set": Object {
@ -130,7 +130,7 @@ Object {
},
Object {
"script": Object {
"id": "ml_hostriskscore_levels_script_customSpace",
"id": "ml_hostriskscore_levels_script_default",
"params": Object {
"risk_score": "host.risk.calculated_score_norm",
},
@ -140,10 +140,10 @@ Object {
}
`;
exports[`installRiskScoreModule - host Create Transform: ml_hostriskscore_latest_transform_customSpace 1`] = `
exports[`installRiskScoresRoute - host Create and start Transform: ml_hostriskscore_latest_transform_mockSpaceId 1`] = `
Object {
"dest": Object {
"index": "ml_host_risk_score_latest_customSpace",
"index": "ml_host_risk_score_latest_default",
},
"frequency": "1h",
"latest": Object {
@ -154,7 +154,7 @@ Object {
},
"source": Object {
"index": Array [
"ml_host_risk_score_customSpace",
"ml_host_risk_score_default",
],
},
"sync": Object {
@ -166,11 +166,11 @@ Object {
}
`;
exports[`installRiskScoreModule - host Create Transform: ml_hostriskscore_pivot_transform_customSpace 1`] = `
exports[`installRiskScoresRoute - host Create and start Transform: ml_hostriskscore_pivot_transform_mockSpaceId 1`] = `
Object {
"dest": Object {
"index": "ml_host_risk_score_customSpace",
"pipeline": "ml_hostriskscore_ingest_pipeline_customSpace",
"index": "ml_host_risk_score_default",
"pipeline": "ml_hostriskscore_ingest_pipeline_default",
},
"frequency": "1h",
"pivot": Object {
@ -184,10 +184,10 @@ Object {
"scripted_metric": Object {
"combine_script": "return state",
"init_script": Object {
"id": "ml_hostriskscore_init_script_customSpace",
"id": "ml_hostriskscore_init_script_default",
},
"map_script": Object {
"id": "ml_hostriskscore_map_script_customSpace",
"id": "ml_hostriskscore_map_script_default",
},
"params": Object {
"lookback_time": 72,
@ -215,7 +215,7 @@ Object {
"zeta_constant": 2.612,
},
"reduce_script": Object {
"id": "ml_hostriskscore_reduce_script_customSpace",
"id": "ml_hostriskscore_reduce_script_default",
},
},
},
@ -230,7 +230,7 @@ Object {
},
"source": Object {
"index": Array [
".alerts-security.alerts-customSpace",
".alerts-security.alerts-default",
],
"query": Object {
"bool": Object {
@ -255,9 +255,9 @@ Object {
}
`;
exports[`installRiskScoreModule - host Create script: ml_hostriskscore_init_script_customSpace 1`] = `
exports[`installRiskScoresRoute - host Create script: ml_hostriskscore_init_script_mockSpaceId 1`] = `
Object {
"id": "ml_hostriskscore_init_script_customSpace",
"id": "ml_hostriskscore_init_script_default",
"script": Object {
"lang": "painless",
"source": "state.rule_risk_stats = new HashMap();
@ -268,9 +268,9 @@ state.tactic_ids = new HashSet();",
}
`;
exports[`installRiskScoreModule - host Create script: ml_hostriskscore_levels_script_customSpace 1`] = `
exports[`installRiskScoresRoute - host Create script: ml_hostriskscore_levels_script_mockSpaceId 1`] = `
Object {
"id": "ml_hostriskscore_levels_script_customSpace",
"id": "ml_hostriskscore_levels_script_default",
"script": Object {
"lang": "painless",
"source": "double risk_score = (def)ctx.getByPath(params.risk_score);
@ -293,9 +293,9 @@ else if (risk_score >= 90) {
}
`;
exports[`installRiskScoreModule - host Create script: ml_hostriskscore_map_script_customSpace 1`] = `
exports[`installRiskScoresRoute - host Create script: ml_hostriskscore_map_script_mockSpaceId 1`] = `
Object {
"id": "ml_hostriskscore_map_script_customSpace",
"id": "ml_hostriskscore_map_script_default",
"script": Object {
"lang": "painless",
"source": "// Get the host variant
@ -324,9 +324,9 @@ state.rule_risk_stats.put(rule_name, stats);",
}
`;
exports[`installRiskScoreModule - host Create script: ml_hostriskscore_reduce_script_customSpace 1`] = `
exports[`installRiskScoresRoute - host Create script: ml_hostriskscore_reduce_script_mockSpaceId 1`] = `
Object {
"id": "ml_hostriskscore_reduce_script_customSpace",
"id": "ml_hostriskscore_reduce_script_default",
"script": Object {
"lang": "painless",
"source": "// Consolidating time decayed risks and tactics from across all shards
@ -407,16 +407,9 @@ return [\\"calculated_score_norm\\": final_risk, \\"rule_risks\\": rule_stats, \
}
`;
exports[`installRiskScoreModule - host Start Transforms 1`] = `
Array [
"ml_hostriskscore_pivot_transform_customSpace",
"ml_hostriskscore_latest_transform_customSpace",
]
`;
exports[`installRiskScoreModule - user Create Index: ml_user_risk_score_customSpace 1`] = `
exports[`installRiskScoresRoute - user Create Index: ml_user_risk_score_latest_mockSpaceId 1`] = `
Object {
"index": "ml_user_risk_score_customSpace",
"index": "ml_user_risk_score_latest_default",
"mappings": Object {
"properties": Object {
"@timestamp": Object {
@ -468,9 +461,9 @@ Object {
}
`;
exports[`installRiskScoreModule - user Create Index: ml_user_risk_score_latest_customSpace 1`] = `
exports[`installRiskScoresRoute - user Create Index: ml_user_risk_score_mockSpaceId 1`] = `
Object {
"index": "ml_user_risk_score_latest_customSpace",
"index": "ml_user_risk_score_default",
"mappings": Object {
"properties": Object {
"@timestamp": Object {
@ -522,9 +515,9 @@ Object {
}
`;
exports[`installRiskScoreModule - user Create IngestPipeline: ml_userriskscore_ingest_pipeline_customSpace 1`] = `
exports[`installRiskScoresRoute - user Create IngestPipeline: ml_userriskscore_ingest_pipeline_mockSpaceId 1`] = `
Object {
"name": "ml_userriskscore_ingest_pipeline_customSpace",
"name": "ml_userriskscore_ingest_pipeline_default",
"processors": Array [
Object {
"set": Object {
@ -544,7 +537,7 @@ Object {
},
Object {
"script": Object {
"id": "ml_userriskscore_levels_script_customSpace",
"id": "ml_userriskscore_levels_script_default",
"params": Object {
"risk_score": "user.risk.calculated_score_norm",
},
@ -554,10 +547,10 @@ Object {
}
`;
exports[`installRiskScoreModule - user Create Transform: ml_userriskscore_latest_transform_customSpace 1`] = `
exports[`installRiskScoresRoute - user Create Transform: ml_userriskscore_latest_transform_mockSpaceId 1`] = `
Object {
"dest": Object {
"index": "ml_user_risk_score_latest_customSpace",
"index": "ml_user_risk_score_latest_default",
},
"frequency": "1h",
"latest": Object {
@ -568,7 +561,7 @@ Object {
},
"source": Object {
"index": Array [
"ml_user_risk_score_customSpace",
"ml_user_risk_score_default",
],
},
"sync": Object {
@ -580,11 +573,11 @@ Object {
}
`;
exports[`installRiskScoreModule - user Create Transform: ml_userriskscore_pivot_transform_customSpace 1`] = `
exports[`installRiskScoresRoute - user Create Transform: ml_userriskscore_pivot_transform_mockSpaceId 1`] = `
Object {
"dest": Object {
"index": "ml_user_risk_score_customSpace",
"pipeline": "ml_userriskscore_ingest_pipeline_customSpace",
"index": "ml_user_risk_score_default",
"pipeline": "ml_userriskscore_ingest_pipeline_default",
},
"frequency": "1h",
"pivot": Object {
@ -599,7 +592,7 @@ Object {
"combine_script": "return state",
"init_script": "state.rule_risk_stats = new HashMap();",
"map_script": Object {
"id": "ml_userriskscore_map_script_customSpace",
"id": "ml_userriskscore_map_script_default",
},
"params": Object {
"max_risk": 100,
@ -607,7 +600,7 @@ Object {
"zeta_constant": 2.612,
},
"reduce_script": Object {
"id": "ml_userriskscore_reduce_script_customSpace",
"id": "ml_userriskscore_reduce_script_default",
},
},
},
@ -622,7 +615,7 @@ Object {
},
"source": Object {
"index": Array [
".alerts-security.alerts-customSpace",
".alerts-security.alerts-default",
],
"query": Object {
"bool": Object {
@ -652,9 +645,9 @@ Object {
}
`;
exports[`installRiskScoreModule - user Create script: ml_userriskscore_levels_script_customSpace 1`] = `
exports[`installRiskScoresRoute - user Create script: ml_userriskscore_levels_script_mockSpaceId 1`] = `
Object {
"id": "ml_userriskscore_levels_script_customSpace",
"id": "ml_userriskscore_levels_script_default",
"script": Object {
"lang": "painless",
"source": "double risk_score = (def)ctx.getByPath(params.risk_score);
@ -677,9 +670,9 @@ else if (risk_score >= 90) {
}
`;
exports[`installRiskScoreModule - user Create script: ml_userriskscore_map_script_customSpace 1`] = `
exports[`installRiskScoresRoute - user Create script: ml_userriskscore_map_script_mockSpaceId 1`] = `
Object {
"id": "ml_userriskscore_map_script_customSpace",
"id": "ml_userriskscore_map_script_default",
"script": Object {
"lang": "painless",
"source": "// Get running sum of risk score per rule name per shard\\\\\\\\
@ -691,9 +684,9 @@ state.rule_risk_stats.put(rule_name, stats);",
}
`;
exports[`installRiskScoreModule - user Create script: ml_userriskscore_reduce_script_customSpace 1`] = `
exports[`installRiskScoresRoute - user Create script: ml_userriskscore_reduce_script_mockSpaceId 1`] = `
Object {
"id": "ml_userriskscore_reduce_script_customSpace",
"id": "ml_userriskscore_reduce_script_default",
"script": Object {
"lang": "painless",
"source": "// Consolidating time decayed risks from across all shards
@ -741,10 +734,3 @@ return [\\"calculated_score_norm\\": total_norm_risk, \\"rule_risks\\": rule_sta
},
}
`;
exports[`installRiskScoreModule - user Start Transforms 1`] = `
Array [
"ml_userriskscore_pivot_transform_customSpace",
"ml_userriskscore_latest_transform_customSpace",
]
`;

View file

@ -0,0 +1,178 @@
/*
* 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 {
serverMock,
requestContextMock,
requestMock,
} from '../../../detection_engine/routes/__mocks__';
import { INTERNAL_RISK_SCORE_URL } from '../../../../../common/constants';
import { RiskScoreEntity } from '../../../../../common/search_strategy';
import { installRiskScoresRoute } from './install_risk_scores';
import { createIngestPipeline } from '../helpers/ingest_pipeline';
import { createStoredScript } from '../../stored_scripts/lib/create_script';
import { createIndex } from '../../indices/lib/create_index';
import { createAndStartTransform } from '../../transform/helpers/transforms';
jest.mock('../../stored_scripts/lib/create_script', () => ({
createStoredScript: jest
.fn()
.mockImplementation(({ options }) =>
Promise.resolve({ [options.id]: { success: true, error: null } })
),
}));
jest.mock('../helpers/ingest_pipeline', () => ({
createIngestPipeline: jest
.fn()
.mockImplementation(({ options }) =>
Promise.resolve({ [options.name]: { success: true, error: null } })
),
}));
jest.mock('../../indices/lib/create_index', () => ({
createIndex: jest
.fn()
.mockImplementation(({ options }) =>
Promise.resolve({ [options.index]: { success: true, error: null } })
),
}));
jest.mock('../../transform/helpers/transforms', () => ({
createAndStartTransform: jest
.fn()
.mockImplementation(({ transformId }) =>
Promise.resolve({ [transformId]: { success: true, error: null } })
),
}));
describe(`installRiskScoresRoute - ${RiskScoreEntity.host}`, () => {
let server: ReturnType<typeof serverMock.create>;
let { context } = requestContextMock.createTools();
const logger = { error: jest.fn() } as unknown as Logger;
const security = undefined;
const mockSpaceId = 'mockSpaceId';
beforeAll(async () => {
jest.clearAllMocks();
server = serverMock.create();
({ context } = requestContextMock.createTools());
const request = requestMock.create({
method: 'post',
path: INTERNAL_RISK_SCORE_URL,
body: {
riskScoreEntity: RiskScoreEntity.host,
},
});
installRiskScoresRoute(server.router, logger, security);
await server.inject(request, requestContextMock.convertContext(context));
});
afterAll(() => {
jest.clearAllMocks();
});
it(`Create script: ml_${RiskScoreEntity.host}riskscore_levels_script_${mockSpaceId}`, async () => {
expect((createStoredScript as jest.Mock).mock.calls[0][0].options).toMatchSnapshot();
});
it(`Create IngestPipeline: ml_${RiskScoreEntity.host}riskscore_ingest_pipeline_${mockSpaceId}`, async () => {
expect((createIngestPipeline as jest.Mock).mock.calls[0][0].options).toMatchSnapshot();
});
it(`Create script: ml_${RiskScoreEntity.host}riskscore_init_script_${mockSpaceId}`, async () => {
expect((createStoredScript as jest.Mock).mock.calls[1][0].options).toMatchSnapshot();
});
it(`Create script: ml_${RiskScoreEntity.host}riskscore_map_script_${mockSpaceId}`, async () => {
expect((createStoredScript as jest.Mock).mock.calls[2][0].options).toMatchSnapshot();
});
it(`Create script: ml_${RiskScoreEntity.host}riskscore_reduce_script_${mockSpaceId}`, async () => {
expect((createStoredScript as jest.Mock).mock.calls[3][0].options).toMatchSnapshot();
});
it(`Create Index: ml_${RiskScoreEntity.host}_risk_score_${mockSpaceId}`, async () => {
expect((createIndex as jest.Mock).mock.calls[0][0].options).toMatchSnapshot();
});
it(`Create Index: ml_${RiskScoreEntity.host}_risk_score_latest_${mockSpaceId}`, async () => {
expect((createIndex as jest.Mock).mock.calls[1][0].options).toMatchSnapshot();
});
it(`Create and start Transform: ml_${RiskScoreEntity.host}riskscore_pivot_transform_${mockSpaceId}`, async () => {
expect((createAndStartTransform as jest.Mock).mock.calls[0][0].options).toMatchSnapshot();
});
it(`Create and start Transform: ml_${RiskScoreEntity.host}riskscore_latest_transform_${mockSpaceId}`, async () => {
expect((createAndStartTransform as jest.Mock).mock.calls[1][0].options).toMatchSnapshot();
});
});
describe(`installRiskScoresRoute - ${RiskScoreEntity.user}`, () => {
let server: ReturnType<typeof serverMock.create>;
let { context } = requestContextMock.createTools();
const logger = { error: jest.fn() } as unknown as Logger;
const security = undefined;
const mockSpaceId = 'mockSpaceId';
beforeAll(async () => {
jest.clearAllMocks();
server = serverMock.create();
({ context } = requestContextMock.createTools());
const request = requestMock.create({
method: 'post',
path: INTERNAL_RISK_SCORE_URL,
body: {
riskScoreEntity: RiskScoreEntity.user,
},
});
installRiskScoresRoute(server.router, logger, security);
await server.inject(request, requestContextMock.convertContext(context));
});
afterAll(() => {
jest.clearAllMocks();
});
it(`Create script: ml_${RiskScoreEntity.user}riskscore_levels_script_${mockSpaceId}`, async () => {
expect((createStoredScript as jest.Mock).mock.calls[0][0].options).toMatchSnapshot();
});
it(`Create IngestPipeline: ml_${RiskScoreEntity.user}riskscore_ingest_pipeline_${mockSpaceId}`, async () => {
expect((createIngestPipeline as jest.Mock).mock.calls[0][0].options).toMatchSnapshot();
});
it(`Create script: ml_${RiskScoreEntity.user}riskscore_map_script_${mockSpaceId}`, async () => {
expect((createStoredScript as jest.Mock).mock.calls[1][0].options).toMatchSnapshot();
});
it(`Create script: ml_${RiskScoreEntity.user}riskscore_reduce_script_${mockSpaceId}`, async () => {
expect((createStoredScript as jest.Mock).mock.calls[2][0].options).toMatchSnapshot();
});
it(`Create Index: ml_${RiskScoreEntity.user}_risk_score_${mockSpaceId}`, async () => {
expect((createIndex as jest.Mock).mock.calls[0][0].options).toMatchSnapshot();
});
it(`Create Index: ml_${RiskScoreEntity.user}_risk_score_latest_${mockSpaceId}`, async () => {
expect((createIndex as jest.Mock).mock.calls[1][0].options).toMatchSnapshot();
});
it(`Create Transform: ml_${RiskScoreEntity.user}riskscore_pivot_transform_${mockSpaceId}`, async () => {
expect((createAndStartTransform as jest.Mock).mock.calls[0][0].options).toMatchSnapshot();
});
it(`Create Transform: ml_${RiskScoreEntity.user}riskscore_latest_transform_${mockSpaceId}`, async () => {
expect((createAndStartTransform as jest.Mock).mock.calls[1][0].options).toMatchSnapshot();
});
});

View file

@ -0,0 +1,64 @@
/*
* 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 { transformError } from '@kbn/securitysolution-es-utils';
import type { Logger } from '@kbn/core/server';
import { INTERNAL_RISK_SCORE_URL } from '../../../../../common/constants';
import type { SecuritySolutionPluginRouter } from '../../../../types';
import type { SetupPlugins } from '../../../../plugin';
import { buildSiemResponse } from '../../../detection_engine/routes/utils';
import { installRiskScoreModule } from '../helpers/install_risk_score_module';
import { onboardingRiskScoreSchema } from '../schema';
export const installRiskScoresRoute = (
router: SecuritySolutionPluginRouter,
logger: Logger,
security: SetupPlugins['security']
) => {
router.post(
{
path: INTERNAL_RISK_SCORE_URL,
validate: onboardingRiskScoreSchema,
options: {
tags: ['access:securitySolution'],
},
},
async (context, request, response) => {
const siemResponse = buildSiemResponse(response);
const { riskScoreEntity } = request.body;
try {
const securitySolution = await context.securitySolution;
const spaceId = securitySolution?.getSpaceId();
const { client } = (await context.core).elasticsearch;
const esClient = client.asCurrentUser;
const res = await installRiskScoreModule({
esClient,
logger,
riskScoreEntity,
spaceId,
});
return response.ok({
body: res,
});
} catch (err) {
const error = transformError(err);
return siemResponse.error({
body: error.message,
statusCode: error.statusCode,
});
}
}
);
};

View file

@ -0,0 +1,18 @@
/*
* 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 { schema } from '@kbn/config-schema';
import { RiskScoreEntity } from '../../../../common/search_strategy';
export const onboardingRiskScoreSchema = {
body: schema.object({
riskScoreEntity: schema.oneOf([
schema.literal(RiskScoreEntity.host),
schema.literal(RiskScoreEntity.user),
]),
}),
};

View file

@ -5,29 +5,64 @@
* 2.0.
*/
import type { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server';
import { transformError } from '@kbn/securitysolution-es-utils';
import type { Logger } from '@kbn/core/server';
import uuid from 'uuid';
import { i18n } from '@kbn/i18n';
import { RiskScoreEntity } from '../../../../../common/search_strategy';
import * as savedObjectsToCreate from '../saved_object';
import type { SavedObjectTemplate } from '../types';
import type { BulkCreateSavedObjectsResult, SavedObjectTemplate } from '../types';
import { findOrCreateRiskScoreTag } from './find_or_create_tag';
export const bulkCreateSavedObjects = async ({
export const bulkCreateSavedObjects = async <T = SavedObjectTemplate>({
logger,
savedObjectsClient,
spaceId,
savedObjectTemplate,
}: {
logger: Logger;
savedObjectsClient: SavedObjectsClientContract;
spaceId?: string;
savedObjectTemplate: SavedObjectTemplate;
}) => {
}): Promise<BulkCreateSavedObjectsResult> => {
const regex = /<REPLACE-WITH-SPACE>/g;
const riskScoreEntity =
savedObjectTemplate === 'userRiskScoreDashboards' ? RiskScoreEntity.user : RiskScoreEntity.host;
const tag = await findOrCreateRiskScoreTag({ riskScoreEntity, savedObjectsClient, spaceId });
const tagResponse = await findOrCreateRiskScoreTag({
riskScoreEntity,
logger,
savedObjectsClient,
spaceId,
});
const tagResult = tagResponse?.hostRiskScoreDashboards ?? tagResponse?.userRiskScoreDashboards;
if (!tagResult?.success) {
return tagResponse;
}
const mySavedObjects = savedObjectsToCreate[savedObjectTemplate];
if (!mySavedObjects) {
logger.error(`${savedObjectTemplate} template not found`);
return {
[savedObjectTemplate]: {
success: false,
error: transformError(
new Error(
i18n.translate('xpack.securitySolution.riskScore.savedObjects.templateNotFoundTitle', {
values: { savedObjectTemplate },
defaultMessage: `Failed to import saved objects: {savedObjectTemplate} were not created as template not found`,
})
)
),
},
};
}
const idReplaceMappings: Record<string, string> = {};
mySavedObjects.forEach((so) => {
if (so.id.startsWith('<REPLACE-WITH-ID')) {
@ -42,21 +77,40 @@ export const bulkCreateSavedObjects = async ({
return {
...so,
id: idReplaceMappings[so.id] ?? so.id,
references: [...references, { id: tag.id, name: tag.name, type: tag.type }],
references: [
...references,
{ id: tagResult?.body?.id, name: tagResult?.body?.name, type: tagResult?.body?.type },
],
};
});
const savedObjects = JSON.stringify(mySavedObjectsWithRef);
if (savedObjects == null) {
return new Error('Template not found.');
}
const replacedSO = spaceId ? savedObjects.replace(regex, spaceId) : savedObjects;
const createSO = await savedObjectsClient.bulkCreate(JSON.parse(replacedSO), {
overwrite: true,
});
try {
const result = await savedObjectsClient.bulkCreate<{
title: string;
name: string;
}>(JSON.parse(replacedSO), {
overwrite: true,
});
return createSO;
return {
[savedObjectTemplate]: {
success: true,
error: null,
body: result.saved_objects.map(({ id, type, attributes: { title, name } }) => ({
id,
type,
title,
name,
})),
},
};
} catch (error) {
const err = transformError(error);
logger.error(`Failed to create saved object: ${savedObjectTemplate}: ${err.message}`);
return { [savedObjectTemplate]: { success: false, error: err } };
}
};

View file

@ -4,10 +4,14 @@
* 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 type { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server';
import { transformError } from '@kbn/securitysolution-es-utils';
import { i18n } from '@kbn/i18n';
import type { RiskScoreEntity } from '../../../../../common/search_strategy';
import type { Tag } from './utils';
import { RISK_SCORE_TAG_DESCRIPTION, getRiskScoreTagName } from './utils';
import type { BulkCreateSavedObjectsResult } from '../types';
export const findRiskScoreTag = async ({
savedObjectsClient,
@ -39,15 +43,17 @@ export const findRiskScoreTag = async ({
export const findOrCreateRiskScoreTag = async ({
riskScoreEntity,
logger,
savedObjectsClient,
spaceId = 'default',
}: {
logger: Logger;
riskScoreEntity: RiskScoreEntity;
savedObjectsClient: SavedObjectsClientContract;
spaceId?: string;
}) => {
}): Promise<BulkCreateSavedObjectsResult> => {
const tagName = getRiskScoreTagName(riskScoreEntity, spaceId);
const savedObjectTemplate = `${riskScoreEntity}RiskScoreDashboards`;
const existingRiskScoreTag = await findRiskScoreTag({
savedObjectsClient,
search: tagName,
@ -61,15 +67,59 @@ export const findOrCreateRiskScoreTag = async ({
};
if (existingRiskScoreTag?.id != null) {
return tag;
logger.error(`${savedObjectTemplate} already exists`);
return {
[savedObjectTemplate]: {
success: false,
error: transformError(
new Error(
i18n.translate(
'xpack.securitySolution.riskScore.savedObjects.templateAlreadyExistsTitle',
{
values: { savedObjectTemplate },
defaultMessage: `Failed to import saved objects: {savedObjectTemplate} were not created as already exist`,
}
)
)
),
},
};
} else {
const { id: tagId } = await savedObjectsClient.create('tag', {
name: tagName,
description: RISK_SCORE_TAG_DESCRIPTION,
color: '#6edb7f',
});
try {
const { id: tagId } = await savedObjectsClient.create('tag', {
name: tagName,
description: RISK_SCORE_TAG_DESCRIPTION,
color: '#6edb7f',
});
return { ...tag, id: tagId };
return {
[savedObjectTemplate]: {
success: true,
error: null,
body: { ...tag, id: tagId },
},
};
} catch (e) {
logger.error(
`${savedObjectTemplate} cannot be installed as failed to create the tag: ${tagName}`
);
return {
[savedObjectTemplate]: {
success: false,
error: transformError(
new Error(
i18n.translate(
'xpack.securitySolution.riskScore.savedObjects.failedToCreateTagTitle',
{
values: { savedObjectTemplate, tagName },
defaultMessage: `Failed to import saved objects: {savedObjectTemplate} were not created as failed to create the tag: {tagName}`,
}
)
)
),
},
};
}
}
};

View file

@ -17,7 +17,7 @@ Array [
},
"references": Array [
Object {
"id": "tagID",
"id": "mockTagId",
"name": "my tag",
"type": "tag",
},
@ -136,7 +136,7 @@ Array [
"type": "index-pattern",
},
Object {
"id": "tagID",
"id": "mockTagId",
"name": "my tag",
"type": "tag",
},
@ -159,7 +159,7 @@ Array [
},
"references": Array [
Object {
"id": "tagID",
"id": "mockTagId",
"name": "my tag",
"type": "tag",
},
@ -190,7 +190,7 @@ Array [
"type": "index-pattern",
},
Object {
"id": "tagID",
"id": "mockTagId",
"name": "my tag",
"type": "tag",
},
@ -221,7 +221,7 @@ Array [
"type": "index-pattern",
},
Object {
"id": "tagID",
"id": "mockTagId",
"name": "my tag",
"type": "tag",
},
@ -252,7 +252,7 @@ Array [
"type": "index-pattern",
},
Object {
"id": "tagID",
"id": "mockTagId",
"name": "my tag",
"type": "tag",
},
@ -270,7 +270,7 @@ Array [
"id": "id-7",
"references": Array [
Object {
"id": "tagID",
"id": "mockTagId",
"name": "my tag",
"type": "tag",
},
@ -323,7 +323,7 @@ Array [
"type": "tag",
},
Object {
"id": "tagID",
"id": "mockTagId",
"name": "my tag",
"type": "tag",
},
@ -346,7 +346,7 @@ Array [
},
"references": Array [
Object {
"id": "tagID",
"id": "mockTagId",
"name": "my tag",
"type": "tag",
},
@ -459,7 +459,7 @@ Array [
"type": "index-pattern",
},
Object {
"id": "tagID",
"id": "mockTagId",
"name": "my tag",
"type": "tag",
},
@ -502,7 +502,7 @@ Array [
"type": "tag",
},
Object {
"id": "tagID",
"id": "mockTagId",
"name": "my tag",
"type": "tag",
},
@ -531,7 +531,7 @@ Array [
},
"references": Array [
Object {
"id": "tagID",
"id": "mockTagId",
"name": "my tag",
"type": "tag",
},
@ -649,7 +649,7 @@ Array [
"type": "index-pattern",
},
Object {
"id": "tagID",
"id": "mockTagId",
"name": "my tag",
"type": "tag",
},
@ -673,7 +673,7 @@ Array [
},
"references": Array [
Object {
"id": "tagID",
"id": "mockTagId",
"name": "my tag",
"type": "tag",
},
@ -798,7 +798,7 @@ Array [
"type": "index-pattern",
},
Object {
"id": "tagID",
"id": "mockTagId",
"name": "my tag",
"type": "tag",
},
@ -822,7 +822,7 @@ Array [
},
"references": Array [
Object {
"id": "tagID",
"id": "mockTagId",
"name": "my tag",
"type": "tag",
},
@ -853,7 +853,7 @@ Array [
"type": "index-pattern",
},
Object {
"id": "tagID",
"id": "mockTagId",
"name": "my tag",
"type": "tag",
},
@ -884,7 +884,7 @@ Array [
"type": "index-pattern",
},
Object {
"id": "tagID",
"id": "mockTagId",
"name": "my tag",
"type": "tag",
},
@ -915,7 +915,7 @@ Array [
"type": "index-pattern",
},
Object {
"id": "tagID",
"id": "mockTagId",
"name": "my tag",
"type": "tag",
},
@ -936,7 +936,7 @@ Array [
},
"references": Array [
Object {
"id": "tagID",
"id": "mockTagId",
"name": "my tag",
"type": "tag",
},
@ -995,7 +995,7 @@ Array [
"type": "tag",
},
Object {
"id": "tagID",
"id": "mockTagId",
"name": "my tag",
"type": "tag",
},
@ -1044,7 +1044,7 @@ Array [
"type": "tag",
},
Object {
"id": "tagID",
"id": "mockTagId",
"name": "my tag",
"type": "tag",
},

View file

@ -4,6 +4,7 @@
* 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 type { SecurityPluginSetup } from '@kbn/security-plugin/server';
import { PREBUILT_SAVED_OBJECTS_BULK_CREATE } from '../../../../../common/constants';
import {
@ -13,18 +14,14 @@ import {
requestMock,
} from '../../../detection_engine/routes/__mocks__';
import { getEmptySavedObjectsResponse } from '../../../detection_engine/routes/__mocks__/request_responses';
import { findOrCreateRiskScoreTag } from '../helpers/find_or_create_tag';
import { createPrebuiltSavedObjectsRoute } from './create_prebuilt_saved_objects';
jest.mock('../helpers/find_or_create_tag', () => {
const actual = jest.requireActual('../helpers/find_or_create_tag');
return {
...actual,
findOrCreateRiskScoreTag: jest.fn().mockResolvedValue({
id: 'tagID',
name: 'my tag',
description: 'description',
type: 'tag',
}),
findOrCreateRiskScoreTag: jest.fn(),
};
});
@ -57,6 +54,7 @@ describe('createPrebuiltSavedObjects', () => {
let server: ReturnType<typeof serverMock.create>;
let securitySetup: SecurityPluginSetup;
let { clients, context } = requestContextMock.createTools();
const logger = { error: jest.fn() } as unknown as Logger;
beforeEach(() => {
jest.clearAllMocks();
@ -73,14 +71,26 @@ describe('createPrebuiltSavedObjects', () => {
clients.savedObjectsClient.bulkCreate.mockResolvedValue(getEmptySavedObjectsResponse());
createPrebuiltSavedObjectsRoute(server.router, securitySetup);
createPrebuiltSavedObjectsRoute(server.router, logger, securitySetup);
});
it.each([['hostRiskScoreDashboards'], ['userRiskScoreDashboards']])(
'should create saved objects from given template - %p',
async (object) => {
async (templateName) => {
(findOrCreateRiskScoreTag as jest.Mock).mockResolvedValue({
[templateName]: {
success: true,
error: null,
body: {
id: 'mockTagId',
name: 'my tag',
description: 'description',
type: 'tag',
},
},
});
const response = await server.inject(
createPrebuiltSavedObjectsRequest(object),
createPrebuiltSavedObjectsRequest(templateName),
requestContextMock.convertContext(context)
);

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { transformError } from '@kbn/securitysolution-es-utils';
import type { Logger } from '@kbn/core/server';
import { PREBUILT_SAVED_OBJECTS_BULK_CREATE } from '../../../../../common/constants';
import type { SecuritySolutionPluginRouter } from '../../../../types';
@ -20,6 +20,7 @@ import { createPrebuiltSavedObjectsSchema } from '../schema';
export const createPrebuiltSavedObjectsRoute = (
router: SecuritySolutionPluginRouter,
logger: Logger,
security: SetupPlugins['security']
) => {
router.post(
@ -34,29 +35,24 @@ export const createPrebuiltSavedObjectsRoute = (
const siemResponse = buildSiemResponse(response);
const { template_name: templateName } = request.params;
try {
const securitySolution = await context.securitySolution;
const securitySolution = await context.securitySolution;
const spaceId = securitySolution?.getSpaceId();
const spaceId = securitySolution?.getSpaceId();
const frameworkRequest = await buildFrameworkRequest(context, security, request);
const savedObjectsClient = (await frameworkRequest.context.core).savedObjects.client;
const res = await bulkCreateSavedObjects({
savedObjectsClient,
spaceId,
savedObjectTemplate: templateName,
});
return response.ok({
body: res,
});
} catch (err) {
const error = transformError(err);
return siemResponse.error({
body: error.message,
statusCode: error.statusCode,
});
const frameworkRequest = await buildFrameworkRequest(context, security, request);
const savedObjectsClient = (await frameworkRequest.context.core).savedObjects.client;
const result = await bulkCreateSavedObjects({
savedObjectsClient,
logger,
spaceId,
savedObjectTemplate: templateName,
});
const error =
result?.hostRiskScoreDashboards?.error || result?.userRiskScoreDashboards?.error;
if (error != null) {
return siemResponse.error({ statusCode: error.statusCode, body: error.message });
} else {
return response.ok({ body: result });
}
}
);

View file

@ -5,4 +5,19 @@
* 2.0.
*/
import type { OutputError } from '@kbn/securitysolution-es-utils';
export type SavedObjectTemplate = 'hostRiskScoreDashboards' | 'userRiskScoreDashboards';
export interface BulkCreateSavedObjectsResult {
hostRiskScoreDashboards?: {
success: boolean;
error: OutputError;
body?: { id: string; name: string; type: string };
};
userRiskScoreDashboards?: {
success: boolean;
error: OutputError;
body?: { id: string; name: string; type: string };
};
}

View file

@ -16,3 +16,6 @@ export { createStoredScriptRoute } from '../stored_scripts/create_script_route';
export { deleteStoredScriptRoute } from '../stored_scripts/delete_script_route';
export { getRiskScoreIndexStatusRoute } from '../index_status';
export { installRiskScoresRoute } from '../onboarding/routes/install_risk_scores';
export { restartTransformRoute } from '../transform/restart_transform';

View file

@ -10,7 +10,7 @@
* 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 {
serverMock,
requestContextMock,
@ -19,27 +19,34 @@ import {
import { createStoredScriptRoute } from './create_script_route';
import { RISK_SCORE_CREATE_STORED_SCRIPT } from '../../../../common/constants';
import { createStoredScript } from './lib/create_script';
import { transformError } from '@kbn/securitysolution-es-utils';
const testScriptId = 'test-script';
const testScriptSource =
'if (state.host_variant_set == false) {\n if (doc.containsKey("host.os.full") && doc["host.os.full"].size() != 0) {\n state.host_variant = doc["host.os.full"].value;\n state.host_variant_set = true;\n }\n}\n// Aggregate all the tactics seen on the host\nif (doc.containsKey("signal.rule.threat.tactic.id") && doc["signal.rule.threat.tactic.id"].size() != 0) {\n state.tactic_ids.add(doc["signal.rule.threat.tactic.id"].value);\n}\n// Get running sum of time-decayed risk score per rule name per shard\nString rule_name = doc["signal.rule.name"].value;\ndef stats = state.rule_risk_stats.getOrDefault(rule_name, [0.0,"",false]);\nint time_diff = (int)((System.currentTimeMillis() - doc["@timestamp"].value.toInstant().toEpochMilli()) / (1000.0 * 60.0 * 60.0));\ndouble risk_derate = Math.min(1, Math.exp((params.lookback_time - time_diff) / params.time_decay_constant));\nstats[0] = Math.max(stats[0], doc["signal.rule.risk_score"].value * risk_derate);\nif (stats[2] == false) {\n stats[1] = doc["kibana.alert.rule.uuid"].value;\n stats[2] = true;\n}\nstate.rule_risk_stats.put(rule_name, stats);';
jest.mock('./lib/create_script', () => {
const actualModule = jest.requireActual('./lib/create_script');
return {
...actualModule,
createStoredScript: jest.fn(),
createStoredScript: jest
.fn()
.mockResolvedValue({ [testScriptId]: { success: true, error: null } }),
};
});
describe('createStoredScriptRoute', () => {
let server: ReturnType<typeof serverMock.create>;
let { context } = requestContextMock.createTools();
const logger = { error: jest.fn() } as unknown as Logger;
beforeEach(() => {
jest.resetModules();
jest.resetAllMocks();
jest.clearAllMocks();
server = serverMock.create();
({ context } = requestContextMock.createTools());
createStoredScriptRoute(server.router);
createStoredScriptRoute(server.router, logger);
});
it('Create stored script', async () => {
@ -47,11 +54,10 @@ describe('createStoredScriptRoute', () => {
method: 'put',
path: RISK_SCORE_CREATE_STORED_SCRIPT,
body: {
id: 'test-script',
id: testScriptId,
script: {
lang: 'painless',
source:
'if (state.host_variant_set == false) {\n if (doc.containsKey("host.os.full") && doc["host.os.full"].size() != 0) {\n state.host_variant = doc["host.os.full"].value;\n state.host_variant_set = true;\n }\n}\n// Aggregate all the tactics seen on the host\nif (doc.containsKey("signal.rule.threat.tactic.id") && doc["signal.rule.threat.tactic.id"].size() != 0) {\n state.tactic_ids.add(doc["signal.rule.threat.tactic.id"].value);\n}\n// Get running sum of time-decayed risk score per rule name per shard\nString rule_name = doc["signal.rule.name"].value;\ndef stats = state.rule_risk_stats.getOrDefault(rule_name, [0.0,"",false]);\nint time_diff = (int)((System.currentTimeMillis() - doc["@timestamp"].value.toInstant().toEpochMilli()) / (1000.0 * 60.0 * 60.0));\ndouble risk_derate = Math.min(1, Math.exp((params.lookback_time - time_diff) / params.time_decay_constant));\nstats[0] = Math.max(stats[0], doc["signal.rule.risk_score"].value * risk_derate);\nif (stats[2] == false) {\n stats[1] = doc["kibana.alert.rule.uuid"].value;\n stats[2] = true;\n}\nstate.rule_risk_stats.put(rule_name, stats);',
source: testScriptSource,
},
},
});
@ -70,4 +76,26 @@ describe('createStoredScriptRoute', () => {
expect(result.ok).not.toHaveBeenCalled();
});
it('return error if failed to create stored script', async () => {
(createStoredScript as jest.Mock).mockResolvedValue({
[testScriptId]: { success: false, error: transformError(new Error('unknown error')) },
});
const request = requestMock.create({
method: 'put',
path: RISK_SCORE_CREATE_STORED_SCRIPT,
body: {
id: testScriptId,
script: {
lang: 'painless',
source: testScriptSource,
},
},
});
const response = await server.inject(request, requestContextMock.convertContext(context));
expect(createStoredScript).toHaveBeenCalled();
expect(response.status).toEqual(500);
expect(response.body).toEqual({ message: 'unknown error', status_code: 500 });
});
});

View file

@ -5,13 +5,14 @@
* 2.0.
*/
import { transformError } from '@kbn/securitysolution-es-utils';
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_SCORE_CREATE_STORED_SCRIPT } from '../../../../common/constants';
import type { SecuritySolutionPluginRouter } from '../../../types';
import { createStoredScriptBodySchema, createStoredScript } from './lib/create_script';
export const createStoredScriptRoute = (router: SecuritySolutionPluginRouter) => {
export const createStoredScriptRoute = (router: SecuritySolutionPluginRouter, logger: Logger) => {
router.put(
{
path: RISK_SCORE_CREATE_STORED_SCRIPT,
@ -23,19 +24,25 @@ export const createStoredScriptRoute = (router: SecuritySolutionPluginRouter) =>
async (context, request, response) => {
const siemResponse = buildSiemResponse(response);
const { client } = (await context.core).elasticsearch;
const esClient = client.asCurrentUser;
const options = request.body;
try {
await createStoredScript({
client,
const result = await createStoredScript({
esClient,
logger,
options,
});
return response.ok({ body: options });
} catch (err) {
const error = transformError(err);
return siemResponse.error({
body: error.message,
statusCode: error.statusCode,
});
const error = result[options.id].error;
if (error != null) {
return siemResponse.error({ statusCode: error.statusCode, body: error.message });
} else {
return response.ok({ body: options });
}
} catch (e) {
const error = transformError(e);
return siemResponse.error({ statusCode: error.statusCode, body: error.message });
}
}
);

View file

@ -6,7 +6,8 @@
*/
import type { TypeOf } from '@kbn/config-schema';
import { schema } from '@kbn/config-schema';
import type { IScopedClusterClient } from '@kbn/core-elasticsearch-server';
import type { ElasticsearchClient, Logger } from '@kbn/core/server';
import { transformError } from '@kbn/securitysolution-es-utils';
export const createStoredScriptBodySchema = schema.object({
id: schema.string({ minLength: 1 }),
@ -26,11 +27,20 @@ export const createStoredScriptBodySchema = schema.object({
type CreateStoredScriptBodySchema = TypeOf<typeof createStoredScriptBodySchema>;
export const createStoredScript = async ({
client,
esClient,
logger,
options,
}: {
client: IScopedClusterClient;
esClient: ElasticsearchClient;
logger: Logger;
options: CreateStoredScriptBodySchema;
}) => {
await client.asCurrentUser.putScript(options);
try {
await esClient.putScript(options);
return { [options.id]: { success: true, error: null } };
} catch (error) {
const createScriptError = transformError(error);
logger.error(`Failed to create stored script: ${options.id}: ${createScriptError.message}`);
return { [options.id]: { success: false, error: createScriptError } };
}
};

View file

@ -0,0 +1,241 @@
/*
* 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 { transformError } from '@kbn/securitysolution-es-utils';
import type { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types';
import type { ElasticsearchClient, Logger } from '@kbn/core/server';
import { i18n } from '@kbn/i18n';
export const TRANSFORM_STATE = {
ABORTING: 'aborting',
FAILED: 'failed',
INDEXING: 'indexing',
STARTED: 'started',
STOPPED: 'stopped',
STOPPING: 'stopping',
WAITING: 'waiting',
} as const;
export const createAndStartTransform = ({
esClient,
transformId,
options,
logger,
}: {
esClient: ElasticsearchClient;
transformId: string;
options: string | Omit<TransformPutTransformRequest, 'transform_id'>;
logger: Logger;
}) => {
const transformOptions = typeof options === 'string' ? JSON.parse(options) : options;
const transform = {
transform_id: transformId,
...transformOptions,
};
return createTransformIfNotExists(esClient, transform, logger).then((result) => {
if (result[transform.transform_id].success) {
return startTransformIfNotStarted(esClient, transform.transform_id, logger);
} else {
return result;
}
});
};
/**
* Checks if a transform exists, And if not creates it
* @param transform - the transform to create. If a transform with the same transform_id already exists, nothing is created.
*/
export const createTransformIfNotExists = async (
esClient: ElasticsearchClient,
transform: TransformPutTransformRequest,
logger: Logger
) => {
try {
await esClient.transform.getTransform({
transform_id: transform.transform_id,
});
logger.error(`Transform ${transform.transform_id} already exists`);
return {
[transform.transform_id]: {
success: false,
error: transformError(
new Error(
i18n.translate('xpack.securitySolution.riskScore.transform.transformExistsTitle', {
values: { transformId: transform.transform_id },
defaultMessage: `Failed to create Transform as {transformId} already exists`,
})
)
),
},
};
} catch (existErr) {
const existError = transformError(existErr);
if (existError.statusCode === 404) {
try {
await esClient.transform.putTransform(transform);
return { [transform.transform_id]: { success: true, error: null } };
} catch (createErr) {
const createError = transformError(createErr);
logger.error(
`Failed to create transform ${transform.transform_id}: ${createError.message}`
);
return { [transform.transform_id]: { success: false, error: createError } };
}
} else {
logger.error(
`Failed to check if transform ${transform.transform_id} exists before creation: ${existError.message}`
);
return { [transform.transform_id]: { success: false, error: existError } };
}
}
};
const checkTransformState = async (
esClient: ElasticsearchClient,
transformId: string,
logger: Logger
) => {
try {
const transformStats = await esClient.transform.getTransformStats({
transform_id: transformId,
});
if (transformStats.count <= 0) {
logger.error(`Failed to check ${transformId} state: couldn't find transform`);
return {
[transformId]: {
success: false,
error: transformError(
new Error(
i18n.translate('xpack.securitySolution.riskScore.transform.notFoundTitle', {
values: { transformId },
defaultMessage: `Failed to check Transform state as {transformId} not found`,
})
)
),
},
};
}
return transformStats.transforms[0];
} catch (statsErr) {
const statsError = transformError(statsErr);
logger.error(`Failed to check if transform ${transformId} is started: ${statsError.message}`);
return {
[transformId]: {
success: false,
error: statsErr,
},
};
}
};
export const startTransformIfNotStarted = async (
esClient: ElasticsearchClient,
transformId: string,
logger: Logger
) => {
const fetchedTransformStats = await checkTransformState(esClient, transformId, logger);
if (fetchedTransformStats.state === 'stopped') {
try {
await esClient.transform.startTransform({ transform_id: transformId });
return { [transformId]: { success: true, error: null } };
} catch (startErr) {
const startError = transformError(startErr);
logger.error(`Failed starting transform ${transformId}: ${startError.message}`);
return {
[transformId]: {
success: false,
error: startError,
},
};
}
} else if (
fetchedTransformStats.state === TRANSFORM_STATE.STOPPING ||
fetchedTransformStats.state === TRANSFORM_STATE.ABORTING ||
fetchedTransformStats.state === TRANSFORM_STATE.FAILED
) {
logger.error(
`Not starting transform ${transformId} since it's state is: ${fetchedTransformStats.state}`
);
return {
[transformId]: {
success: false,
error: transformError(
new Error(
i18n.translate('xpack.securitySolution.riskScore.transform.start.stateConflictTitle', {
values: { transformId, state: fetchedTransformStats.state },
defaultMessage: `Not starting transform {transformId} since it's state is: {state}`,
})
)
),
},
};
}
};
const stopTransform = async (
esClient: ElasticsearchClient,
transformId: string,
logger: Logger
) => {
const fetchedTransformStats = await checkTransformState(esClient, transformId, logger);
if (fetchedTransformStats.state) {
try {
await esClient.transform.stopTransform({
transform_id: transformId,
force: fetchedTransformStats.state === TRANSFORM_STATE.FAILED,
wait_for_completion: true,
});
return { [transformId]: { success: true, error: null } };
} catch (startErr) {
const startError = transformError(startErr);
logger.error(`Failed stopping transform ${transformId}: ${startError.message}`);
return {
[transformId]: {
success: false,
error: startError,
},
};
}
} else {
logger.error(
`Not stopping transform ${transformId} since it's state is: ${fetchedTransformStats.state}`
);
return {
[transformId]: {
success: false,
error: transformError(
new Error(
i18n.translate('xpack.securitySolution.riskScore.transform.stop.stateConflictTitle', {
values: { transformId, state: fetchedTransformStats.state },
defaultMessage: `Not stopping transform {transformId} since it's state is: {state}`,
})
)
),
},
};
}
};
export const restartTransform = (
esClient: ElasticsearchClient,
transformId: string,
logger: Logger
) => {
return stopTransform(esClient, transformId, logger).then((result) => {
if (result[transformId].success) {
return startTransformIfNotStarted(esClient, transformId, logger);
} else {
return result;
}
});
};

View file

@ -0,0 +1,77 @@
/*
* 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 { transformError } from '@kbn/securitysolution-es-utils';
import type { Logger } from '@kbn/core/server';
import { schema } from '@kbn/config-schema';
import { RISK_SCORE_RESTART_TRANSFORMS } from '../../../../common/constants';
import type { SecuritySolutionPluginRouter } from '../../../types';
import { buildSiemResponse } from '../../detection_engine/routes/utils';
import { RiskScoreEntity } from '../../../../common/search_strategy';
import { restartTransform } from './helpers/transforms';
import {
getRiskScoreLatestTransformId,
getRiskScorePivotTransformId,
} from '../../../../common/utils/risk_score_modules';
const restartRiskScoreTransformsSchema = {
body: schema.object({
riskScoreEntity: schema.oneOf([
schema.literal(RiskScoreEntity.host),
schema.literal(RiskScoreEntity.user),
]),
}),
};
export const restartTransformRoute = (router: SecuritySolutionPluginRouter, logger: Logger) => {
router.post(
{
path: RISK_SCORE_RESTART_TRANSFORMS,
validate: restartRiskScoreTransformsSchema,
options: {
tags: ['access:securitySolution'],
},
},
async (context, request, response) => {
const siemResponse = buildSiemResponse(response);
const { riskScoreEntity } = request.body;
try {
const securitySolution = await context.securitySolution;
const spaceId = securitySolution?.getSpaceId();
const { client } = (await context.core).elasticsearch;
const esClient = client.asCurrentUser;
const restartPivotTransformResult = await restartTransform(
esClient,
getRiskScorePivotTransformId(riskScoreEntity, spaceId),
logger
);
const restartLatestTransformResult = await restartTransform(
esClient,
getRiskScoreLatestTransformId(riskScoreEntity, spaceId),
logger
);
return response.ok({
body: [restartPivotTransformResult, restartLatestTransformResult],
});
} catch (err) {
const error = transformError(err);
return siemResponse.error({
body: error.message,
statusCode: error.statusCode,
});
}
}
);
};

View file

@ -82,7 +82,9 @@ import {
deletePrebuiltSavedObjectsRoute,
deleteStoredScriptRoute,
getRiskScoreIndexStatusRoute,
installRiskScoresRoute,
readPrebuiltDevToolContentRoute,
restartTransformRoute,
} from '../lib/risk_score/routes';
export const initRoutes = (
router: SecuritySolutionPluginRouter,
@ -185,15 +187,16 @@ export const initRoutes = (
getSourcererDataViewRoute(router, getStartServices);
// risky score module
createEsIndexRoute(router);
createEsIndexRoute(router, logger);
deleteEsIndicesRoute(router);
createStoredScriptRoute(router);
createStoredScriptRoute(router, logger);
deleteStoredScriptRoute(router);
readPrebuiltDevToolContentRoute(router);
createPrebuiltSavedObjectsRoute(router, security);
createPrebuiltSavedObjectsRoute(router, logger, security);
deletePrebuiltSavedObjectsRoute(router, security);
getRiskScoreIndexStatusRoute(router);
installRiskScoresRoute(router, logger, security);
restartTransformRoute(router, logger);
const { previewTelemetryUrlEnabled } = config.experimentalFeatures;
if (previewTelemetryUrlEnabled) {
// telemetry preview endpoint for e2e integration tests only at the moment.