Asset cricitality init (#171324)

##  Introduce Asset criticality initialisation and refactor risk engine

### Asset criticality 
use `entityAnalyticsAssetCriticalityEnabled` for testing

Added `AssetCriticalityDataClient` which will create index/mappings for
the risk engine.

### Refactor risk engine

As you can see in this
[discussion](https://github.com/elastic/kibana/pull/171324#discussion_r1394461582)
there raised some concerns about `RiskEngineDataClient` has a lot of
responsibilities.

So in this PR, I took out risk scoring functionality from
`RiskEngineDataClient` to `RiskScoreDataClient`.

### Changes inside `entity_analytics` folder

`risk_engine` folder and `RiskEngineDataClient` will be responsible for:
- Init risk engine and installation of all resources like, SO, removing
legacy dashboards, and calling `RiskScoreDataClient` and
`AssetCriticalityDataClient` for installation of corresponding resources
- Getting the status of the risk engine
- Enable / Disable risk engine and start/remove task
- Saved object configuration manipulation

The `risk_score` folder be responsible for:
- Risk score calculation and persistence
- Task methods
- (**_new_**) `RiskScoreDataClient`  will be responsible for: 
    - resource initialisation like:
      - index template and mappings
      - risk score datasream
      - creating the latest index
      - creating transform
   - return writer for risk scores
   - return risk input index
 
The `asset_criticality` and `AssetCriticalityDataClient` folder be
responsible for:
- asset criticality index and mapping creation
- in future CRUD operations for asset criticality

`routes` folders have API routes for risk engine and risk scoring
functionality, there will be also asset criticality routes in the future

`utils` common utils

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Khristinin Nikita 2023-11-28 22:29:54 +01:00 committed by GitHub
parent 0d2d89d066
commit 45e88fea3e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
84 changed files with 1336 additions and 772 deletions

View file

@ -0,0 +1,26 @@
openapi: 3.0.0
info:
version: 1.0.0
title: Asset Criticality Status Schema
paths:
/internal/asset_criticality/status:
get:
summary: Get Asset Criticality Status
responses:
'200':
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/AssetCriticalityStatusResponse'
'400':
description: Invalid request
responses:
components:
schemas:
AssetCriticalityStatusResponse:
type: object
properties:
asset_criticality_resources_installed:
type: boolean

View file

@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export * from './indices';

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.
*/
const indexBase = '.asset-criticality.asset-criticality';
export const getAssetCriticalityIndex = (namespace: string) => `${indexBase}-${namespace}`;

View file

@ -261,6 +261,9 @@ export const RISK_ENGINE_ENABLE_URL = `${RISK_ENGINE_URL}/enable`;
export const RISK_ENGINE_DISABLE_URL = `${RISK_ENGINE_URL}/disable`;
export const RISK_ENGINE_PRIVILEGES_URL = `${RISK_ENGINE_URL}/privileges`;
export const ASSET_CRITICALITY_URL = `/internal/asset_criticality`;
export const ASSET_CRITICALITY_STATUS_URL = `${ASSET_CRITICALITY_URL}/status`;
/**
* Public Risk Score routes
*/

View file

@ -126,6 +126,11 @@ export const allowedExperimentalValues = Object.freeze({
* and associated callout in the UI
*/
riskEnginePrivilegesRouteEnabled: false,
/*
* Enables experimental Entity Analytics Asset Criticality feature
*/
entityAnalyticsAssetCriticalityEnabled: false,
});
type ExperimentalConfigKeys = Array<keyof ExperimentalFeatures>;

View file

@ -22,7 +22,7 @@ import type {
InitRiskEngineResponse,
DisableRiskEngineResponse,
RiskEnginePrivilegesResponse,
} from '../../../server/lib/entity_analytics/risk_engine/types';
} from '../../../server/lib/entity_analytics/types';
import type { RiskScorePreviewRequestSchema } from '../../../common/risk_engine/risk_score_preview/request_schema';
/**

View file

@ -11,7 +11,7 @@ import { useInvalidateRiskEngineStatusQuery } from './use_risk_engine_status';
import type {
EnableRiskEngineResponse,
EnableDisableRiskEngineErrorResponse,
} from '../../../../server/lib/entity_analytics/risk_engine/types';
} from '../../../../server/lib/entity_analytics/types';
export const DISABLE_RISK_ENGINE_MUTATION_KEY = ['POST', 'DISABLE_RISK_ENGINE'];

View file

@ -11,7 +11,7 @@ import { useInvalidateRiskEngineStatusQuery } from './use_risk_engine_status';
import type {
EnableRiskEngineResponse,
EnableDisableRiskEngineErrorResponse,
} from '../../../../server/lib/entity_analytics/risk_engine/types';
} from '../../../../server/lib/entity_analytics/types';
export const ENABLE_RISK_ENGINE_MUTATION_KEY = ['POST', 'ENABLE_RISK_ENGINE'];
export const useEnableRiskEngineMutation = (options?: UseMutationOptions<{}>) => {

View file

@ -11,7 +11,7 @@ import { useInvalidateRiskEngineStatusQuery } from './use_risk_engine_status';
import type {
InitRiskEngineResponse,
InitRiskEngineError,
} from '../../../../server/lib/entity_analytics/risk_engine/types';
} from '../../../../server/lib/entity_analytics/types';
export const INIT_RISK_ENGINE_STATUS_KEY = ['POST', 'INIT_RISK_ENGINE'];

View file

@ -6,7 +6,7 @@
*/
import { useMemo } from 'react';
import type { RiskEnginePrivilegesResponse } from '../../../../server/lib/entity_analytics/risk_engine/types';
import type { RiskEnginePrivilegesResponse } from '../../../../server/lib/entity_analytics/types';
import { useRiskEnginePrivileges } from '../../api/hooks/use_risk_engine_privileges';
import {
RISK_ENGINE_REQUIRED_ES_CLUSTER_PRIVILEGES,

View file

@ -35,6 +35,8 @@ import type {
import { getEndpointAuthzInitialStateMock } from '../../../../../common/endpoint/service/authz/mocks';
import type { EndpointAuthz } from '../../../../../common/endpoint/types/authz';
import { riskEngineDataClientMock } from '../../../entity_analytics/risk_engine/risk_engine_data_client.mock';
import { riskScoreDataClientMock } from '../../../entity_analytics/risk_score/risk_score_data_client.mock';
import { assetCriticalityDataClientMock } from '../../../entity_analytics/asset_criticality/asset_criticality_data_client.mock';
export const createMockClients = () => {
const core = coreMock.createRequestHandlerContext();
@ -63,6 +65,8 @@ export const createMockClients = () => {
detectionEngineHealthClient: detectionEngineHealthClientMock.create(),
ruleExecutionLog: ruleExecutionLogMock.forRoutes.create(),
riskEngineDataClient: riskEngineDataClientMock.create(),
riskScoreDataClient: riskScoreDataClientMock.create(),
assetCriticalityDataClient: assetCriticalityDataClientMock.create(),
};
};
@ -142,6 +146,8 @@ const createSecuritySolutionRequestContextMock = (
throw new Error('Not implemented');
}),
getRiskEngineDataClient: jest.fn(() => clients.riskEngineDataClient),
getRiskScoreDataClient: jest.fn(() => clients.riskScoreDataClient),
getAssetCriticalityDataClient: jest.fn(() => clients.assetCriticalityDataClient),
};
};

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 type { AssetCriticalityDataClient } from './asset_criticality_data_client';
const createAssetCriticalityDataClientMock = () =>
({
doesIndexExist: jest.fn(),
getStatus: jest.fn(),
init: jest.fn(),
} as unknown as jest.Mocked<AssetCriticalityDataClient>);
export const assetCriticalityDataClientMock = { create: createAssetCriticalityDataClientMock };

View file

@ -0,0 +1,60 @@
/*
* 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 { loggingSystemMock, elasticsearchServiceMock } from '@kbn/core/server/mocks';
import { AssetCriticalityDataClient } from './asset_criticality_data_client';
import { createOrUpdateIndex } from '../utils/create_or_update_index';
jest.mock('../utils/create_or_update_index', () => ({
createOrUpdateIndex: jest.fn(),
}));
describe('AssetCriticalityDataClient', () => {
const esClientInternal = elasticsearchServiceMock.createScopedClusterClient().asInternalUser;
const logger = loggingSystemMock.createLogger();
describe('init', () => {
it('ensures the index is available and up to date', async () => {
const assetCriticalityDataClient = new AssetCriticalityDataClient({
esClient: esClientInternal,
logger,
namespace: 'default',
});
await assetCriticalityDataClient.init();
expect(createOrUpdateIndex).toHaveBeenCalledWith({
esClient: esClientInternal,
logger,
options: {
index: '.asset-criticality.asset-criticality-default',
mappings: {
dynamic: 'strict',
properties: {
id_field: {
type: 'keyword',
},
id_value: {
type: 'keyword',
},
criticality_level: {
type: 'keyword',
},
'@timestamp': {
type: 'date',
ignore_malformed: false,
},
updated_at: {
type: 'date',
},
},
},
},
});
});
});
});

View file

@ -0,0 +1,54 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { Logger, ElasticsearchClient } from '@kbn/core/server';
import { mappingFromFieldMap } from '@kbn/alerting-plugin/common';
import { createOrUpdateIndex } from '../utils/create_or_update_index';
import { getAssetCriticalityIndex } from '../../../../common/asset_criticality';
import { assetCriticalityFieldMap } from './configurations';
interface AssetCriticalityClientOpts {
logger: Logger;
esClient: ElasticsearchClient;
namespace: string;
}
export class AssetCriticalityDataClient {
constructor(private readonly options: AssetCriticalityClientOpts) {}
/**
* It will create idex for asset criticality,
* or update mappings if index exists
*/
public async init() {
await createOrUpdateIndex({
esClient: this.options.esClient,
logger: this.options.logger,
options: {
index: getAssetCriticalityIndex(this.options.namespace),
mappings: mappingFromFieldMap(assetCriticalityFieldMap, 'strict'),
},
});
}
public async doesIndexExist() {
try {
const result = await this.options.esClient.indices.exists({
index: getAssetCriticalityIndex(this.options.namespace),
});
return result;
} catch (e) {
return false;
}
}
public async getStatus() {
const isAssetCriticalityResourcesInstalled = await this.doesIndexExist();
return {
isAssetCriticalityResourcesInstalled,
};
}
}

View file

@ -0,0 +1,42 @@
/*
* 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 { loggingSystemMock } from '@kbn/core/server/mocks';
import { requestContextMock } from '../../detection_engine/routes/__mocks__';
import { AssetCriticalityDataClient } from './asset_criticality_data_client';
import { checkAndInitAssetCriticalityResources } from './check_and_init_asset_criticality_resources';
describe('checkAndInitAssetCriticalityResources', () => {
const logger = loggingSystemMock.createLogger();
const { context } = requestContextMock.createTools();
const doesIndexExist = jest.spyOn(AssetCriticalityDataClient.prototype, 'doesIndexExist');
const initAssetCriticality = jest.spyOn(AssetCriticalityDataClient.prototype, 'init');
beforeEach(() => {
doesIndexExist.mockImplementation(() => Promise.resolve(false));
initAssetCriticality.mockImplementation(() => Promise.resolve());
});
afterEach(() => {
doesIndexExist.mockReset();
initAssetCriticality.mockReset();
});
it('should initialise asset criticality resources if they do not exist', async () => {
await checkAndInitAssetCriticalityResources(requestContextMock.convertContext(context), logger);
expect(initAssetCriticality).toHaveBeenCalled();
expect(logger.info).toHaveBeenCalledWith('Asset criticality resources installed');
});
it('should not initialise asset criticality resources if they already exist', async () => {
doesIndexExist.mockImplementationOnce(() => Promise.resolve(true));
await checkAndInitAssetCriticalityResources(requestContextMock.convertContext(context), logger);
expect(initAssetCriticality).not.toHaveBeenCalled();
});
});

View file

@ -0,0 +1,38 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { Logger } from '@kbn/core/server';
import { AssetCriticalityDataClient } from './asset_criticality_data_client';
import type { SecuritySolutionRequestHandlerContext } from '../../../types';
/**
* As internal user we check for existence of asset crititcality resources
* and initialise it if it does not exist
* @param context
* @param logger
*/
export const checkAndInitAssetCriticalityResources = async (
context: SecuritySolutionRequestHandlerContext,
logger: Logger
) => {
const securityContext = await context.securitySolution;
const coreContext = await context.core;
const esClient = coreContext.elasticsearch.client.asInternalUser;
const assetCriticalityDataClient = new AssetCriticalityDataClient({
esClient,
logger,
namespace: securityContext.getSpaceId(),
});
const doesIndexExist = await assetCriticalityDataClient.doesIndexExist();
if (!doesIndexExist) {
logger.info('Asset criticality resources are not installed, initialising...');
await assetCriticalityDataClient.init();
logger.info('Asset criticality resources installed');
}
};

View file

@ -0,0 +1,35 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { FieldMap } from '@kbn/alerts-as-data-utils';
export const assetCriticalityFieldMap: FieldMap = {
'@timestamp': {
type: 'date',
array: false,
required: false,
},
id_field: {
type: 'keyword',
array: false,
required: false,
},
id_value: {
type: 'keyword',
array: false,
required: false,
},
criticality_level: {
type: 'keyword',
array: false,
required: false,
},
updated_at: {
type: 'date',
array: false,
required: false,
},
} as const;

View file

@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export { assetCriticalityStatusRoute } from './status';

View file

@ -0,0 +1,50 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { Logger } from '@kbn/core/server';
import { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils';
import { transformError } from '@kbn/securitysolution-es-utils';
import { ASSET_CRITICALITY_STATUS_URL, APP_ID } from '../../../../../common/constants';
import type { SecuritySolutionPluginRouter } from '../../../../types';
import { checkAndInitAssetCriticalityResources } from '../check_and_init_asset_criticality_resources';
export const assetCriticalityStatusRoute = (
router: SecuritySolutionPluginRouter,
logger: Logger
) => {
router.versioned
.get({
access: 'internal',
path: ASSET_CRITICALITY_STATUS_URL,
options: {
tags: ['access:securitySolution', `access:${APP_ID}-entity-analytics`],
},
})
.addVersion({ version: '1', validate: {} }, async (context, request, response) => {
const siemResponse = buildSiemResponse(response);
try {
await checkAndInitAssetCriticalityResources(context, logger);
const securitySolution = await context.securitySolution;
const assetCriticalityClient = securitySolution.getAssetCriticalityDataClient();
const result = await assetCriticalityClient.getStatus();
return response.ok({
body: {
asset_criticality_resources_installed: result.isAssetCriticalityResourcesInstalled,
},
});
} catch (e) {
const error = transformError(e);
return siemResponse.error({
statusCode: error.statusCode,
body: { message: error.message, full_error: JSON.stringify(e) },
bypassErrorFormat: true,
});
}
});
};

View file

@ -7,7 +7,7 @@
import type { KibanaRequest } from '@kbn/core/server';
import type { SecurityPluginStart } from '@kbn/security-plugin/server';
import type { RiskEnginePrivilegesResponse } from './types';
import type { RiskEnginePrivilegesResponse } from '../types';
import {
RISK_ENGINE_REQUIRED_ES_CLUSTER_PRIVILEGES,
RISK_ENGINE_REQUIRED_ES_INDEX_PRIVILEGES,

View file

@ -13,11 +13,8 @@ const createRiskEngineDataClientMock = () =>
disableRiskEngine: jest.fn(),
enableRiskEngine: jest.fn(),
getConfiguration: jest.fn(),
getRiskInputsIndex: jest.fn(),
getStatus: jest.fn(),
getWriter: jest.fn(),
init: jest.fn(),
initializeResources: jest.fn(),
} as unknown as jest.Mocked<RiskEngineDataClient>);
export const riskEngineDataClientMock = { create: createRiskEngineDataClientMock };

View file

@ -5,10 +5,6 @@
* 2.0.
*/
import {
createOrUpdateComponentTemplate,
createOrUpdateIndexTemplate,
} from '@kbn/alerting-plugin/server';
import {
loggingSystemMock,
elasticsearchServiceMock,
@ -17,11 +13,11 @@ import {
import { taskManagerMock } from '@kbn/task-manager-plugin/server/mocks';
import type { SavedObject } from '@kbn/core/server';
import { RiskEngineDataClient } from './risk_engine_data_client';
import type { RiskEngineConfiguration } from './types';
import { createDataStream } from './utils/create_datastream';
import { RiskScoreDataClient } from '../risk_score/risk_score_data_client';
import type { RiskEngineConfiguration } from '../types';
import * as savedObjectConfig from './utils/saved_object_configuration';
import * as transforms from './utils/transforms';
import { createIndex } from './utils/create_index';
import * as transforms from '../utils/transforms';
import { riskScoreDataClientMock } from '../risk_score/risk_score_data_client.mock';
const getSavedObjectConfiguration = (attributes = {}) => ({
page: 1,
@ -47,32 +43,17 @@ const getSavedObjectConfiguration = (attributes = {}) => ({
],
});
const transformsMock = {
count: 1,
transforms: [
{
id: 'ml_hostriskscore_pivot_transform_default',
dest: { index: '' },
source: { index: '' },
},
],
};
jest.mock('@kbn/alerting-plugin/server', () => ({
createOrUpdateComponentTemplate: jest.fn(),
createOrUpdateIndexTemplate: jest.fn(),
}));
jest.mock('./utils/create_datastream', () => ({
jest.mock('../utils/create_datastream', () => ({
createDataStream: jest.fn(),
}));
jest.mock('../../risk_score/transform/helpers/transforms', () => ({
createAndStartTransform: jest.fn(),
}));
jest.mock('./utils/create_index', () => ({
createIndex: jest.fn(),
jest.mock('../utils/create_or_update_index', () => ({
createOrUpdateIndex: jest.fn(),
}));
jest.spyOn(transforms, 'createTransform').mockResolvedValue(Promise.resolve());
@ -87,7 +68,6 @@ describe('RiskEngineDataClient', () => {
let mockSavedObjectClient: ReturnType<typeof savedObjectsClientMock.create>;
let logger: ReturnType<typeof loggingSystemMock.createLogger>;
const esClient = elasticsearchServiceMock.createScopedClusterClient().asCurrentUser;
const totalFieldsLimit = 1000;
beforeEach(() => {
logger = loggingSystemMock.createLogger();
@ -106,484 +86,6 @@ describe('RiskEngineDataClient', () => {
jest.clearAllMocks();
});
describe('getWriter', () => {
it('should return a writer object', async () => {
const writer = await riskEngineDataClient.getWriter({ namespace: 'default' });
expect(writer).toBeDefined();
expect(typeof writer?.bulk).toBe('function');
});
it('should cache and return the same writer for the same namespace', async () => {
const writer1 = await riskEngineDataClient.getWriter({ namespace: 'default' });
const writer2 = await riskEngineDataClient.getWriter({ namespace: 'default' });
const writer3 = await riskEngineDataClient.getWriter({ namespace: 'space-1' });
expect(writer1).toEqual(writer2);
expect(writer2).not.toEqual(writer3);
});
});
describe('initializeResources success', () => {
it('should initialize risk engine resources', async () => {
await riskEngineDataClient.initializeResources({ namespace: 'default' });
expect(createOrUpdateComponentTemplate).toHaveBeenCalledWith(
expect.objectContaining({
logger,
esClient,
template: expect.objectContaining({
name: '.risk-score-mappings',
_meta: {
managed: true,
},
}),
totalFieldsLimit: 1000,
})
);
expect((createOrUpdateComponentTemplate as jest.Mock).mock.lastCall[0].template.template)
.toMatchInlineSnapshot(`
Object {
"mappings": Object {
"dynamic": "strict",
"properties": Object {
"@timestamp": Object {
"ignore_malformed": false,
"type": "date",
},
"host": Object {
"properties": Object {
"name": Object {
"type": "keyword",
},
"risk": Object {
"properties": Object {
"calculated_level": Object {
"type": "keyword",
},
"calculated_score": Object {
"type": "float",
},
"calculated_score_norm": Object {
"type": "float",
},
"category_1_count": Object {
"type": "long",
},
"category_1_score": Object {
"type": "float",
},
"id_field": Object {
"type": "keyword",
},
"id_value": Object {
"type": "keyword",
},
"inputs": Object {
"properties": Object {
"category": Object {
"type": "keyword",
},
"description": Object {
"type": "keyword",
},
"id": Object {
"type": "keyword",
},
"index": Object {
"type": "keyword",
},
"risk_score": Object {
"type": "float",
},
"timestamp": Object {
"type": "date",
},
},
"type": "object",
},
"notes": Object {
"type": "keyword",
},
},
"type": "object",
},
},
},
"user": Object {
"properties": Object {
"name": Object {
"type": "keyword",
},
"risk": Object {
"properties": Object {
"calculated_level": Object {
"type": "keyword",
},
"calculated_score": Object {
"type": "float",
},
"calculated_score_norm": Object {
"type": "float",
},
"category_1_count": Object {
"type": "long",
},
"category_1_score": Object {
"type": "float",
},
"id_field": Object {
"type": "keyword",
},
"id_value": Object {
"type": "keyword",
},
"inputs": Object {
"properties": Object {
"category": Object {
"type": "keyword",
},
"description": Object {
"type": "keyword",
},
"id": Object {
"type": "keyword",
},
"index": Object {
"type": "keyword",
},
"risk_score": Object {
"type": "float",
},
"timestamp": Object {
"type": "date",
},
},
"type": "object",
},
"notes": Object {
"type": "keyword",
},
},
"type": "object",
},
},
},
},
},
"settings": Object {},
}
`);
expect(createOrUpdateIndexTemplate).toHaveBeenCalledWith({
logger,
esClient,
template: {
name: '.risk-score.risk-score-default-index-template',
body: {
data_stream: { hidden: true },
index_patterns: ['risk-score.risk-score-default'],
composed_of: ['.risk-score-mappings'],
template: {
lifecycle: {},
settings: {
'index.mapping.total_fields.limit': totalFieldsLimit,
},
mappings: {
dynamic: false,
_meta: {
kibana: {
version: '8.9.0',
},
managed: true,
namespace: 'default',
},
},
},
_meta: {
kibana: {
version: '8.9.0',
},
managed: true,
namespace: 'default',
},
},
},
});
expect(createDataStream).toHaveBeenCalledWith({
logger,
esClient,
totalFieldsLimit,
indexPatterns: {
template: `.risk-score.risk-score-default-index-template`,
alias: `risk-score.risk-score-default`,
},
});
expect(createIndex).toHaveBeenCalledWith({
logger,
esClient,
options: {
index: `risk-score.risk-score-latest-default`,
mappings: {
dynamic: 'strict',
properties: {
'@timestamp': {
ignore_malformed: false,
type: 'date',
},
host: {
properties: {
name: {
type: 'keyword',
},
risk: {
properties: {
calculated_level: {
type: 'keyword',
},
calculated_score: {
type: 'float',
},
calculated_score_norm: {
type: 'float',
},
category_1_count: {
type: 'long',
},
category_1_score: {
type: 'float',
},
id_field: {
type: 'keyword',
},
id_value: {
type: 'keyword',
},
inputs: {
properties: {
category: {
type: 'keyword',
},
description: {
type: 'keyword',
},
id: {
type: 'keyword',
},
index: {
type: 'keyword',
},
risk_score: {
type: 'float',
},
timestamp: {
type: 'date',
},
},
type: 'object',
},
notes: {
type: 'keyword',
},
},
type: 'object',
},
},
},
user: {
properties: {
name: {
type: 'keyword',
},
risk: {
properties: {
calculated_level: {
type: 'keyword',
},
calculated_score: {
type: 'float',
},
calculated_score_norm: {
type: 'float',
},
category_1_count: {
type: 'long',
},
category_1_score: {
type: 'float',
},
id_field: {
type: 'keyword',
},
id_value: {
type: 'keyword',
},
inputs: {
properties: {
category: {
type: 'keyword',
},
description: {
type: 'keyword',
},
id: {
type: 'keyword',
},
index: {
type: 'keyword',
},
risk_score: {
type: 'float',
},
timestamp: {
type: 'date',
},
},
type: 'object',
},
notes: {
type: 'keyword',
},
},
type: 'object',
},
},
},
},
},
},
});
expect(transforms.createTransform).toHaveBeenCalledWith({
logger,
esClient,
transform: {
dest: {
index: 'risk-score.risk-score-latest-default',
},
frequency: '1h',
latest: {
sort: '@timestamp',
unique_key: ['host.name', 'user.name'],
},
source: {
index: ['risk-score.risk-score-default'],
},
sync: {
time: {
delay: '2s',
field: '@timestamp',
},
},
transform_id: 'risk_score_latest_transform_default',
},
});
});
});
describe('initializeResources error', () => {
it('should handle errors during initialization', async () => {
const error = new Error('There error');
(createOrUpdateIndexTemplate as jest.Mock).mockRejectedValueOnce(error);
try {
await riskEngineDataClient.initializeResources({ namespace: 'default' });
} catch (e) {
expect(logger.error).toHaveBeenCalledWith(
`Error initializing risk engine resources: ${error.message}`
);
}
});
});
describe('getStatus', () => {
it('should return initial status', async () => {
const status = await riskEngineDataClient.getStatus({
namespace: 'default',
});
expect(status).toEqual({
isMaxAmountOfRiskEnginesReached: false,
riskEngineStatus: 'NOT_INSTALLED',
legacyRiskEngineStatus: 'NOT_INSTALLED',
});
});
describe('saved object exists and transforms not', () => {
beforeEach(() => {
mockSavedObjectClient.find.mockResolvedValue(getSavedObjectConfiguration());
});
it('should return status with enabled true', async () => {
mockSavedObjectClient.find.mockResolvedValue(
getSavedObjectConfiguration({
enabled: true,
})
);
const status = await riskEngineDataClient.getStatus({
namespace: 'default',
});
expect(status).toEqual({
isMaxAmountOfRiskEnginesReached: true,
riskEngineStatus: 'ENABLED',
legacyRiskEngineStatus: 'NOT_INSTALLED',
});
});
it('should return status with enabled false', async () => {
mockSavedObjectClient.find.mockResolvedValue(getSavedObjectConfiguration());
const status = await riskEngineDataClient.getStatus({
namespace: 'default',
});
expect(status).toEqual({
isMaxAmountOfRiskEnginesReached: false,
riskEngineStatus: 'DISABLED',
legacyRiskEngineStatus: 'NOT_INSTALLED',
});
});
});
describe('legacy transforms', () => {
it('should fetch transforms', async () => {
await riskEngineDataClient.getStatus({
namespace: 'default',
});
expect(esClient.transform.getTransform).toHaveBeenCalledTimes(4);
expect(esClient.transform.getTransform).toHaveBeenNthCalledWith(1, {
transform_id: 'ml_hostriskscore_pivot_transform_default',
});
expect(esClient.transform.getTransform).toHaveBeenNthCalledWith(2, {
transform_id: 'ml_hostriskscore_latest_transform_default',
});
expect(esClient.transform.getTransform).toHaveBeenNthCalledWith(3, {
transform_id: 'ml_userriskscore_pivot_transform_default',
});
expect(esClient.transform.getTransform).toHaveBeenNthCalledWith(4, {
transform_id: 'ml_userriskscore_latest_transform_default',
});
});
it('should return that legacy transform enabled if at least on transform exist', async () => {
esClient.transform.getTransform.mockResolvedValueOnce(transformsMock);
const status = await riskEngineDataClient.getStatus({
namespace: 'default',
});
expect(status).toEqual({
isMaxAmountOfRiskEnginesReached: false,
riskEngineStatus: 'NOT_INSTALLED',
legacyRiskEngineStatus: 'ENABLED',
});
esClient.transform.getTransformStats.mockReset();
});
});
});
describe('#getConfiguration', () => {
it('retrieves configuration from the saved object', async () => {
mockSavedObjectClient.find.mockResolvedValueOnce(getSavedObjectConfiguration());
@ -703,10 +205,7 @@ describe('RiskEngineDataClient', () => {
describe('init', () => {
let mockTaskManagerStart: ReturnType<typeof taskManagerMock.createStart>;
const initializeResourcesMock = jest.spyOn(
RiskEngineDataClient.prototype,
'initializeResources'
);
const initRiskScore = jest.spyOn(RiskScoreDataClient.prototype, 'init');
const enableRiskEngineMock = jest.spyOn(RiskEngineDataClient.prototype, 'enableRiskEngine');
const disableLegacyRiskEngineMock = jest.spyOn(
@ -717,7 +216,7 @@ describe('RiskEngineDataClient', () => {
mockTaskManagerStart = taskManagerMock.createStart();
disableLegacyRiskEngineMock.mockImplementation(() => Promise.resolve(true));
initializeResourcesMock.mockImplementation(() => {
initRiskScore.mockImplementation(() => {
return Promise.resolve();
});
@ -731,7 +230,7 @@ describe('RiskEngineDataClient', () => {
});
afterEach(() => {
initializeResourcesMock.mockReset();
initRiskScore.mockReset();
enableRiskEngineMock.mockReset();
disableLegacyRiskEngineMock.mockReset();
});
@ -740,6 +239,7 @@ describe('RiskEngineDataClient', () => {
const initResult = await riskEngineDataClient.init({
namespace: 'default',
taskManager: mockTaskManagerStart,
riskScoreDataClient: riskScoreDataClientMock.create(),
});
expect(initResult).toEqual({
@ -758,6 +258,7 @@ describe('RiskEngineDataClient', () => {
const initResult = await riskEngineDataClient.init({
namespace: 'default',
taskManager: mockTaskManagerStart,
riskScoreDataClient: riskScoreDataClientMock.create(),
});
expect(initResult).toEqual({
@ -777,6 +278,7 @@ describe('RiskEngineDataClient', () => {
const initResult = await riskEngineDataClient.init({
namespace: 'default',
taskManager: mockTaskManagerStart,
riskScoreDataClient: riskScoreDataClientMock.create(),
});
expect(initResult).toEqual({
@ -789,17 +291,19 @@ describe('RiskEngineDataClient', () => {
});
it('should catch error for initializeResources and stop', async () => {
initializeResourcesMock.mockImplementationOnce(() => {
throw new Error('Error initializeResourcesMock');
const riskScoreDataClient = riskScoreDataClientMock.create();
riskScoreDataClient.init.mockImplementationOnce(() => {
throw new Error('Error riskScoreDataClient');
});
const initResult = await riskEngineDataClient.init({
namespace: 'default',
taskManager: mockTaskManagerStart,
riskScoreDataClient,
});
expect(initResult).toEqual({
errors: ['Error initializeResourcesMock'],
errors: ['Error riskScoreDataClient'],
legacyRiskEngineDisabled: true,
riskEngineConfigurationCreated: false,
riskEngineEnabled: false,
@ -815,6 +319,7 @@ describe('RiskEngineDataClient', () => {
const initResult = await riskEngineDataClient.init({
namespace: 'default',
taskManager: mockTaskManagerStart,
riskScoreDataClient: riskScoreDataClientMock.create(),
});
expect(initResult).toEqual({
@ -834,6 +339,7 @@ describe('RiskEngineDataClient', () => {
const initResult = await riskEngineDataClient.init({
namespace: 'default',
taskManager: mockTaskManagerStart,
riskScoreDataClient: riskScoreDataClientMock.create(),
});
expect(initResult).toEqual({

View file

@ -5,58 +5,29 @@
* 2.0.
*/
import type { Metadata } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import type { ClusterPutComponentTemplateRequest } from '@elastic/elasticsearch/lib/api/types';
import {
createOrUpdateComponentTemplate,
createOrUpdateIndexTemplate,
} from '@kbn/alerting-plugin/server';
import { mappingFromFieldMap } from '@kbn/alerting-plugin/common';
import { DEFAULT_NAMESPACE_STRING } from '@kbn/core-saved-objects-utils-server';
import type { Logger, ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/server';
import type { TaskManagerStartContract } from '@kbn/task-manager-plugin/server';
import {
riskScoreFieldMap,
getIndexPatternDataStream,
totalFieldsLimit,
mappingComponentName,
getTransformOptions,
} from './configurations';
import { createDataStream } from './utils/create_datastream';
import type { RiskEngineDataWriter as Writer } from './risk_engine_data_writer';
import { RiskEngineDataWriter } from './risk_engine_data_writer';
import type { InitRiskEngineResult } from '../../../../common/risk_engine';
import {
RiskEngineStatus,
getRiskScoreLatestIndex,
MAX_SPACES_COUNT,
RiskScoreEntity,
} from '../../../../common/risk_engine';
import {
getLegacyTransforms,
getLatestTransformId,
removeLegacyTransforms,
createTransform,
} from './utils/transforms';
import { removeLegacyTransforms, getLegacyTransforms } from '../utils/transforms';
import {
updateSavedObjectAttribute,
getConfiguration,
initSavedObjects,
getEnabledRiskEngineAmount,
} from './utils/saved_object_configuration';
import { getRiskInputsIndex } from './get_risk_inputs_index';
import { removeRiskScoringTask, startRiskScoringTask } from './tasks';
import { createIndex } from './utils/create_index';
import { bulkDeleteSavedObjects } from '../../risk_score/prebuilt_saved_objects/helpers/bulk_delete_saved_objects';
import type { RiskScoreDataClient } from '../risk_score/risk_score_data_client';
import { removeRiskScoringTask, startRiskScoringTask } from '../risk_score/tasks';
interface InitOpts {
namespace: string;
taskManager: TaskManagerStartContract;
}
interface InitializeRiskEngineResourcesOpts {
namespace?: string;
riskScoreDataClient: RiskScoreDataClient;
}
interface RiskEngineDataClientOpts {
@ -68,10 +39,9 @@ interface RiskEngineDataClientOpts {
}
export class RiskEngineDataClient {
private writerCache: Map<string, Writer> = new Map();
constructor(private readonly options: RiskEngineDataClientOpts) {}
public async init({ namespace, taskManager }: InitOpts) {
public async init({ namespace, taskManager, riskScoreDataClient }: InitOpts) {
const result: InitRiskEngineResult = {
legacyRiskEngineDisabled: false,
riskEngineResourcesInstalled: false,
@ -88,7 +58,7 @@ export class RiskEngineDataClient {
}
try {
await this.initializeResources({ namespace });
await riskScoreDataClient.init();
result.riskEngineResourcesInstalled = true;
} catch (e) {
result.errors.push(e.message);
@ -106,6 +76,7 @@ export class RiskEngineDataClient {
return result;
}
// should be the last step, after all resources are installed
try {
await this.enableRiskEngine({ taskManager });
result.riskEngineEnabled = true;
@ -117,39 +88,11 @@ export class RiskEngineDataClient {
return result;
}
public async getWriter({ namespace }: { namespace: string }): Promise<Writer> {
if (this.writerCache.get(namespace)) {
return this.writerCache.get(namespace) as Writer;
}
const indexPatterns = getIndexPatternDataStream(namespace);
await this.initializeWriter(namespace, indexPatterns.alias);
return this.writerCache.get(namespace) as Writer;
}
private async initializeWriter(namespace: string, index: string): Promise<Writer> {
const writer = new RiskEngineDataWriter({
esClient: this.options.esClient,
namespace,
index,
logger: this.options.logger,
});
this.writerCache.set(namespace, writer);
return writer;
}
public getConfiguration = () =>
getConfiguration({
savedObjectsClient: this.options.soClient,
});
public getRiskInputsIndex = ({ dataViewId }: { dataViewId: string }) =>
getRiskInputsIndex({
dataViewId,
logger: this.options.logger,
soClient: this.options.soClient,
});
public async getStatus({ namespace }: { namespace: string }) {
const riskEngineStatus = await this.getCurrentStatus();
const legacyRiskEngineStatus = await this.getLegacyStatus({ namespace });
@ -258,96 +201,4 @@ export class RiskEngineDataClient {
return RiskEngineStatus.ENABLED;
}
public async initializeResources({
namespace = DEFAULT_NAMESPACE_STRING,
}: InitializeRiskEngineResourcesOpts) {
try {
const esClient = this.options.esClient;
const indexPatterns = getIndexPatternDataStream(namespace);
const indexMetadata: Metadata = {
kibana: {
version: this.options.kibanaVersion,
},
managed: true,
namespace,
};
await Promise.all([
createOrUpdateComponentTemplate({
logger: this.options.logger,
esClient,
template: {
name: mappingComponentName,
_meta: {
managed: true,
},
template: {
settings: {},
mappings: mappingFromFieldMap(riskScoreFieldMap, 'strict'),
},
} as ClusterPutComponentTemplateRequest,
totalFieldsLimit,
}),
]);
await createOrUpdateIndexTemplate({
logger: this.options.logger,
esClient,
template: {
name: indexPatterns.template,
body: {
data_stream: { hidden: true },
index_patterns: [indexPatterns.alias],
composed_of: [mappingComponentName],
template: {
lifecycle: {},
settings: {
'index.mapping.total_fields.limit': totalFieldsLimit,
},
mappings: {
dynamic: false,
_meta: indexMetadata,
},
},
_meta: indexMetadata,
},
},
});
await createDataStream({
logger: this.options.logger,
esClient,
totalFieldsLimit,
indexPatterns,
});
await createIndex({
esClient,
logger: this.options.logger,
options: {
index: getRiskScoreLatestIndex(namespace),
mappings: mappingFromFieldMap(riskScoreFieldMap, 'strict'),
},
});
const transformId = getLatestTransformId(namespace);
await createTransform({
esClient,
logger: this.options.logger,
transform: {
transform_id: transformId,
...getTransformOptions({
dest: getRiskScoreLatestIndex(namespace),
source: [indexPatterns.alias],
}),
},
});
} catch (error) {
this.options.logger.error(`Error initializing risk engine resources: ${error.message}`);
throw error;
}
}
}

View file

@ -6,7 +6,7 @@
*/
import { taskManagerMock } from '@kbn/task-manager-plugin/server/mocks';
import { riskEngineDisableRoute } from './risk_engine_disable_route';
import { riskEngineDisableRoute } from './disable';
import { RISK_ENGINE_DISABLE_URL } from '../../../../../common/constants';
import {

View file

@ -6,7 +6,7 @@
*/
import { taskManagerMock } from '@kbn/task-manager-plugin/server/mocks';
import { riskEngineEnableRoute } from './risk_engine_enable_route';
import { riskEngineEnableRoute } from './enable';
import { RISK_ENGINE_ENABLE_URL } from '../../../../../common/constants';
import {

View file

@ -5,9 +5,8 @@
* 2.0.
*/
export { riskScorePreviewRoute } from './risk_score_preview_route';
export { riskEngineInitRoute } from './risk_engine_init_route';
export { riskEngineEnableRoute } from './risk_engine_enable_route';
export { riskEngineDisableRoute } from './risk_engine_disable_route';
export { riskEngineStatusRoute } from './risk_engine_status_route';
export { riskEnginePrivilegesRoute } from './risk_engine_privileges_route';
export { riskEngineInitRoute } from './init';
export { riskEngineEnableRoute } from './enable';
export { riskEngineDisableRoute } from './disable';
export { riskEngineStatusRoute } from './status';
export { riskEnginePrivilegesRoute } from './privileges';

View file

@ -12,6 +12,7 @@ import { RISK_ENGINE_INIT_URL, APP_ID } from '../../../../../common/constants';
import type { StartPlugins } from '../../../../plugin';
import { TASK_MANAGER_UNAVAILABLE_ERROR } from './translations';
import type { SecuritySolutionPluginRouter } from '../../../../types';
import type { InitRiskEngineResultResponse } from '../../types';
export const riskEngineInitRoute = (
router: SecuritySolutionPluginRouter,
@ -30,6 +31,7 @@ export const riskEngineInitRoute = (
const securitySolution = await context.securitySolution;
const [_, { taskManager }] = await getStartServices();
const riskEngineDataClient = securitySolution.getRiskEngineDataClient();
const riskScoreDataClient = securitySolution.getRiskScoreDataClient();
const spaceId = securitySolution.getSpaceId();
try {
@ -43,9 +45,10 @@ export const riskEngineInitRoute = (
const initResult = await riskEngineDataClient.init({
taskManager,
namespace: spaceId,
riskScoreDataClient,
});
const initResultResponse = {
const initResultResponse: InitRiskEngineResultResponse = {
risk_engine_enabled: initResult.riskEngineEnabled,
risk_engine_resources_installed: initResult.riskEngineResourcesInstalled,
risk_engine_configuration_created: initResult.riskEngineConfigurationCreated,

View file

@ -7,7 +7,7 @@
import type { SavedObject, SavedObjectsClientContract } from '@kbn/core/server';
import { getAlertsIndex } from '../../../../../common/utils/risk_score_modules';
import type { RiskEngineConfiguration } from '../types';
import type { RiskEngineConfiguration } from '../../types';
import { riskEngineConfigurationTypeName } from '../saved_object';
export interface SavedObjectsClientArg {

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import type { CalculateAndPersistScoresResponse } from './types';
import type { CalculateAndPersistScoresResponse } from '../types';
const buildResponseMock = (
overrides: Partial<CalculateAndPersistScoresResponse> = {}

View file

@ -41,7 +41,7 @@ describe('calculateAndPersistRiskScores', () => {
range: { start: 'now - 15d', end: 'now' },
spaceId: 'default',
// @ts-expect-error not relevant for this test
riskEngineDataClient: { getWriter: jest.fn() },
riskScoreDataClient: { getWriter: jest.fn() },
runtimeMappings: {},
});

View file

@ -7,8 +7,8 @@
import type { ElasticsearchClient, Logger } from '@kbn/core/server';
import type { RiskEngineDataClient } from './risk_engine_data_client';
import type { CalculateAndPersistScoresParams, CalculateAndPersistScoresResponse } from './types';
import type { RiskScoreDataClient } from './risk_score_data_client';
import type { CalculateAndPersistScoresParams, CalculateAndPersistScoresResponse } from '../types';
import { calculateRiskScores } from './calculate_risk_scores';
export const calculateAndPersistRiskScores = async (
@ -16,11 +16,11 @@ export const calculateAndPersistRiskScores = async (
esClient: ElasticsearchClient;
logger: Logger;
spaceId: string;
riskEngineDataClient: RiskEngineDataClient;
riskScoreDataClient: RiskScoreDataClient;
}
): Promise<CalculateAndPersistScoresResponse> => {
const { riskEngineDataClient, spaceId, ...rest } = params;
const writer = await riskEngineDataClient.getWriter({
const { riskScoreDataClient, spaceId, ...rest } = params;
const writer = await riskScoreDataClient.getWriter({
namespace: spaceId,
});
const { after_keys: afterKeys, scores } = await calculateRiskScores(rest);

View file

@ -14,7 +14,7 @@ import type {
CalculateRiskScoreAggregations,
CalculateScoresResponse,
RiskScoreBucket,
} from './types';
} from '../types';
const buildRiskScoreBucketMock = (overrides: Partial<RiskScoreBucket> = {}): RiskScoreBucket => ({
key: { 'user.name': 'username' },

View file

@ -38,7 +38,7 @@ import type {
CalculateScoresParams,
CalculateScoresResponse,
RiskScoreBucket,
} from './types';
} from '../types';
const bucketToResponse = ({
bucket,

View file

@ -7,7 +7,7 @@
import type { FieldMap } from '@kbn/alerts-as-data-utils';
import type { IdentifierType } from '../../../../common/risk_engine';
import { RiskScoreEntity, riskScoreBaseIndexName } from '../../../../common/risk_engine';
import type { IIndexPatternString } from './utils/create_datastream';
import type { IIndexPatternString } from '../utils/create_datastream';
const commonRiskFields: FieldMap = {
id_field: {

View file

@ -6,7 +6,7 @@
*/
import type { AfterKey, AfterKeys, IdentifierType } from '../../../../common/risk_engine';
import type { CalculateAndPersistScoresResponse } from './types';
import type { CalculateAndPersistScoresResponse } from '../types';
export const getFieldForIdentifierAgg = (identifierType: IdentifierType): string =>
identifierType === 'host' ? 'host.name' : 'user.name';

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 type { RiskScoreDataClient } from './risk_score_data_client';
const createRiskScoreDataClientMock = () =>
({
getWriter: jest.fn(),
init: jest.fn(),
getRiskInputsIndex: jest.fn(),
} as unknown as jest.Mocked<RiskScoreDataClient>);
export const riskScoreDataClientMock = { create: createRiskScoreDataClientMock };

View file

@ -0,0 +1,455 @@
/*
* 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 {
createOrUpdateComponentTemplate,
createOrUpdateIndexTemplate,
} from '@kbn/alerting-plugin/server';
import {
loggingSystemMock,
elasticsearchServiceMock,
savedObjectsClientMock,
} from '@kbn/core/server/mocks';
import { RiskScoreDataClient } from './risk_score_data_client';
import { createDataStream } from '../utils/create_datastream';
import * as transforms from '../utils/transforms';
import { createOrUpdateIndex } from '../utils/create_or_update_index';
jest.mock('@kbn/alerting-plugin/server', () => ({
createOrUpdateComponentTemplate: jest.fn(),
createOrUpdateIndexTemplate: jest.fn(),
}));
jest.mock('../utils/create_datastream', () => ({
createDataStream: jest.fn(),
}));
jest.mock('../utils/create_or_update_index', () => ({
createOrUpdateIndex: jest.fn(),
}));
jest.spyOn(transforms, 'createTransform').mockResolvedValue(Promise.resolve());
jest.spyOn(transforms, 'startTransform').mockResolvedValue(Promise.resolve());
describe('RiskScoreDataClient', () => {
let riskScoreDataClient: RiskScoreDataClient;
let mockSavedObjectClient: ReturnType<typeof savedObjectsClientMock.create>;
let logger: ReturnType<typeof loggingSystemMock.createLogger>;
const esClient = elasticsearchServiceMock.createScopedClusterClient().asCurrentUser;
const totalFieldsLimit = 1000;
beforeEach(() => {
logger = loggingSystemMock.createLogger();
mockSavedObjectClient = savedObjectsClientMock.create();
const options = {
logger,
kibanaVersion: '8.9.0',
esClient,
soClient: mockSavedObjectClient,
namespace: 'default',
};
riskScoreDataClient = new RiskScoreDataClient(options);
});
afterEach(() => {
jest.clearAllMocks();
});
describe('getWriter', () => {
it('should return a writer object', async () => {
const writer = await riskScoreDataClient.getWriter({ namespace: 'default' });
expect(writer).toBeDefined();
expect(typeof writer?.bulk).toBe('function');
});
it('should cache and return the same writer for the same namespace', async () => {
const writer1 = await riskScoreDataClient.getWriter({ namespace: 'default' });
const writer2 = await riskScoreDataClient.getWriter({ namespace: 'default' });
const writer3 = await riskScoreDataClient.getWriter({ namespace: 'space-1' });
expect(writer1).toEqual(writer2);
expect(writer2).not.toEqual(writer3);
});
});
describe('init success', () => {
it('should initialize risk engine resources', async () => {
await riskScoreDataClient.init();
expect(createOrUpdateComponentTemplate).toHaveBeenCalledWith(
expect.objectContaining({
logger,
esClient,
template: expect.objectContaining({
name: '.risk-score-mappings',
_meta: {
managed: true,
},
}),
totalFieldsLimit: 1000,
})
);
expect((createOrUpdateComponentTemplate as jest.Mock).mock.lastCall[0].template.template)
.toMatchInlineSnapshot(`
Object {
"mappings": Object {
"dynamic": "strict",
"properties": Object {
"@timestamp": Object {
"ignore_malformed": false,
"type": "date",
},
"host": Object {
"properties": Object {
"name": Object {
"type": "keyword",
},
"risk": Object {
"properties": Object {
"calculated_level": Object {
"type": "keyword",
},
"calculated_score": Object {
"type": "float",
},
"calculated_score_norm": Object {
"type": "float",
},
"category_1_count": Object {
"type": "long",
},
"category_1_score": Object {
"type": "float",
},
"id_field": Object {
"type": "keyword",
},
"id_value": Object {
"type": "keyword",
},
"inputs": Object {
"properties": Object {
"category": Object {
"type": "keyword",
},
"description": Object {
"type": "keyword",
},
"id": Object {
"type": "keyword",
},
"index": Object {
"type": "keyword",
},
"risk_score": Object {
"type": "float",
},
"timestamp": Object {
"type": "date",
},
},
"type": "object",
},
"notes": Object {
"type": "keyword",
},
},
"type": "object",
},
},
},
"user": Object {
"properties": Object {
"name": Object {
"type": "keyword",
},
"risk": Object {
"properties": Object {
"calculated_level": Object {
"type": "keyword",
},
"calculated_score": Object {
"type": "float",
},
"calculated_score_norm": Object {
"type": "float",
},
"category_1_count": Object {
"type": "long",
},
"category_1_score": Object {
"type": "float",
},
"id_field": Object {
"type": "keyword",
},
"id_value": Object {
"type": "keyword",
},
"inputs": Object {
"properties": Object {
"category": Object {
"type": "keyword",
},
"description": Object {
"type": "keyword",
},
"id": Object {
"type": "keyword",
},
"index": Object {
"type": "keyword",
},
"risk_score": Object {
"type": "float",
},
"timestamp": Object {
"type": "date",
},
},
"type": "object",
},
"notes": Object {
"type": "keyword",
},
},
"type": "object",
},
},
},
},
},
"settings": Object {},
}
`);
expect(createOrUpdateIndexTemplate).toHaveBeenCalledWith({
logger,
esClient,
template: {
name: '.risk-score.risk-score-default-index-template',
body: {
data_stream: { hidden: true },
index_patterns: ['risk-score.risk-score-default'],
composed_of: ['.risk-score-mappings'],
template: {
lifecycle: {},
settings: {
'index.mapping.total_fields.limit': totalFieldsLimit,
},
mappings: {
dynamic: false,
_meta: {
kibana: {
version: '8.9.0',
},
managed: true,
namespace: 'default',
},
},
},
_meta: {
kibana: {
version: '8.9.0',
},
managed: true,
namespace: 'default',
},
},
},
});
expect(createDataStream).toHaveBeenCalledWith({
logger,
esClient,
totalFieldsLimit,
indexPatterns: {
template: `.risk-score.risk-score-default-index-template`,
alias: `risk-score.risk-score-default`,
},
});
expect(createOrUpdateIndex).toHaveBeenCalledWith({
logger,
esClient,
options: {
index: `risk-score.risk-score-latest-default`,
mappings: {
dynamic: 'strict',
properties: {
'@timestamp': {
ignore_malformed: false,
type: 'date',
},
host: {
properties: {
name: {
type: 'keyword',
},
risk: {
properties: {
calculated_level: {
type: 'keyword',
},
calculated_score: {
type: 'float',
},
calculated_score_norm: {
type: 'float',
},
category_1_count: {
type: 'long',
},
category_1_score: {
type: 'float',
},
id_field: {
type: 'keyword',
},
id_value: {
type: 'keyword',
},
inputs: {
properties: {
category: {
type: 'keyword',
},
description: {
type: 'keyword',
},
id: {
type: 'keyword',
},
index: {
type: 'keyword',
},
risk_score: {
type: 'float',
},
timestamp: {
type: 'date',
},
},
type: 'object',
},
notes: {
type: 'keyword',
},
},
type: 'object',
},
},
},
user: {
properties: {
name: {
type: 'keyword',
},
risk: {
properties: {
calculated_level: {
type: 'keyword',
},
calculated_score: {
type: 'float',
},
calculated_score_norm: {
type: 'float',
},
category_1_count: {
type: 'long',
},
category_1_score: {
type: 'float',
},
id_field: {
type: 'keyword',
},
id_value: {
type: 'keyword',
},
inputs: {
properties: {
category: {
type: 'keyword',
},
description: {
type: 'keyword',
},
id: {
type: 'keyword',
},
index: {
type: 'keyword',
},
risk_score: {
type: 'float',
},
timestamp: {
type: 'date',
},
},
type: 'object',
},
notes: {
type: 'keyword',
},
},
type: 'object',
},
},
},
},
},
},
});
expect(transforms.createTransform).toHaveBeenCalledWith({
logger,
esClient,
transform: {
dest: {
index: 'risk-score.risk-score-latest-default',
},
frequency: '1h',
latest: {
sort: '@timestamp',
unique_key: ['host.name', 'user.name'],
},
source: {
index: ['risk-score.risk-score-default'],
},
sync: {
time: {
delay: '2s',
field: '@timestamp',
},
},
transform_id: 'risk_score_latest_transform_default',
},
});
});
});
describe('init error', () => {
it('should handle errors during initialization', async () => {
const error = new Error('There error');
(createOrUpdateIndexTemplate as jest.Mock).mockRejectedValueOnce(error);
try {
await riskScoreDataClient.init();
} catch (e) {
expect(logger.error).toHaveBeenCalledWith(
`Error initializing risk engine resources: ${error.message}`
);
}
});
});
});

View file

@ -0,0 +1,164 @@
/*
* 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 { Metadata } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import type { ClusterPutComponentTemplateRequest } from '@elastic/elasticsearch/lib/api/types';
import {
createOrUpdateComponentTemplate,
createOrUpdateIndexTemplate,
} from '@kbn/alerting-plugin/server';
import { mappingFromFieldMap } from '@kbn/alerting-plugin/common';
import type { Logger, ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/server';
import {
riskScoreFieldMap,
getIndexPatternDataStream,
totalFieldsLimit,
mappingComponentName,
getTransformOptions,
} from './configurations';
import { createDataStream } from '../utils/create_datastream';
import type { RiskEngineDataWriter as Writer } from './risk_engine_data_writer';
import { RiskEngineDataWriter } from './risk_engine_data_writer';
import { getRiskScoreLatestIndex } from '../../../../common/risk_engine';
import { getLatestTransformId, createTransform } from '../utils/transforms';
import { getRiskInputsIndex } from './get_risk_inputs_index';
import { createOrUpdateIndex } from '../utils/create_or_update_index';
interface RiskScoringDataClientOpts {
logger: Logger;
kibanaVersion: string;
esClient: ElasticsearchClient;
namespace: string;
soClient: SavedObjectsClientContract;
}
export class RiskScoreDataClient {
private writerCache: Map<string, Writer> = new Map();
constructor(private readonly options: RiskScoringDataClientOpts) {}
public async getWriter({ namespace }: { namespace: string }): Promise<Writer> {
if (this.writerCache.get(namespace)) {
return this.writerCache.get(namespace) as Writer;
}
const indexPatterns = getIndexPatternDataStream(namespace);
await this.initializeWriter(namespace, indexPatterns.alias);
return this.writerCache.get(namespace) as Writer;
}
private async initializeWriter(namespace: string, index: string): Promise<Writer> {
const writer = new RiskEngineDataWriter({
esClient: this.options.esClient,
namespace,
index,
logger: this.options.logger,
});
this.writerCache.set(namespace, writer);
return writer;
}
public getRiskInputsIndex = ({ dataViewId }: { dataViewId: string }) =>
getRiskInputsIndex({
dataViewId,
logger: this.options.logger,
soClient: this.options.soClient,
});
public async init() {
const namespace = this.options.namespace;
try {
const esClient = this.options.esClient;
const indexPatterns = getIndexPatternDataStream(namespace);
const indexMetadata: Metadata = {
kibana: {
version: this.options.kibanaVersion,
},
managed: true,
namespace,
};
await Promise.all([
createOrUpdateComponentTemplate({
logger: this.options.logger,
esClient,
template: {
name: mappingComponentName,
_meta: {
managed: true,
},
template: {
settings: {},
mappings: mappingFromFieldMap(riskScoreFieldMap, 'strict'),
},
} as ClusterPutComponentTemplateRequest,
totalFieldsLimit,
}),
]);
await createOrUpdateIndexTemplate({
logger: this.options.logger,
esClient,
template: {
name: indexPatterns.template,
body: {
data_stream: { hidden: true },
index_patterns: [indexPatterns.alias],
composed_of: [mappingComponentName],
template: {
lifecycle: {},
settings: {
'index.mapping.total_fields.limit': totalFieldsLimit,
},
mappings: {
dynamic: false,
_meta: indexMetadata,
},
},
_meta: indexMetadata,
},
},
});
await createDataStream({
logger: this.options.logger,
esClient,
totalFieldsLimit,
indexPatterns,
});
await createOrUpdateIndex({
esClient,
logger: this.options.logger,
options: {
index: getRiskScoreLatestIndex(namespace),
mappings: mappingFromFieldMap(riskScoreFieldMap, 'strict'),
},
});
const transformId = getLatestTransformId(namespace);
await createTransform({
esClient,
logger: this.options.logger,
transform: {
transform_id: transformId,
...getTransformOptions({
dest: getRiskScoreLatestIndex(namespace),
source: [indexPatterns.alias],
}),
},
});
} catch (error) {
this.options.logger.error(`Error initializing risk engine resources: ${error.message}`);
throw error;
}
}
}

View file

@ -12,12 +12,13 @@ import type {
CalculateScoresParams,
CalculateScoresResponse,
RiskEngineConfiguration,
} from './types';
} from '../types';
import { calculateRiskScores } from './calculate_risk_scores';
import { calculateAndPersistRiskScores } from './calculate_and_persist_risk_scores';
import type { RiskEngineDataClient } from './risk_engine_data_client';
import type { RiskEngineDataClient } from '../risk_engine/risk_engine_data_client';
import type { RiskScoreDataClient } from './risk_score_data_client';
import type { RiskInputsIndexResponse } from './get_risk_inputs_index';
import { scheduleLatestTransformNow } from './utils/transforms';
import { scheduleLatestTransformNow } from '../utils/transforms';
export interface RiskScoreService {
calculateScores: (params: CalculateScoresParams) => Promise<CalculateScoresResponse>;
@ -33,6 +34,7 @@ export interface RiskScoreServiceFactoryParams {
esClient: ElasticsearchClient;
logger: Logger;
riskEngineDataClient: RiskEngineDataClient;
riskScoreDataClient: RiskScoreDataClient;
spaceId: string;
}
@ -40,12 +42,13 @@ export const riskScoreServiceFactory = ({
esClient,
logger,
riskEngineDataClient,
riskScoreDataClient,
spaceId,
}: RiskScoreServiceFactoryParams): RiskScoreService => ({
calculateScores: (params) => calculateRiskScores({ ...params, esClient, logger }),
calculateAndPersistScores: (params) =>
calculateAndPersistRiskScores({ ...params, esClient, logger, riskEngineDataClient, spaceId }),
calculateAndPersistRiskScores({ ...params, esClient, logger, riskScoreDataClient, spaceId }),
getConfiguration: async () => riskEngineDataClient.getConfiguration(),
getRiskInputsIndex: async (params) => riskEngineDataClient.getRiskInputsIndex(params),
getRiskInputsIndex: async (params) => riskScoreDataClient.getRiskInputsIndex(params),
scheduleLatestTransformNow: () => scheduleLatestTransformNow({ namespace: spaceId, esClient }),
});

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { riskScoreCalculationRoute } from './risk_score_calculation_route';
import { riskScoreCalculationRoute } from './calculation';
import { loggerMock } from '@kbn/logging-mocks';

View file

@ -41,11 +41,13 @@ export const riskScoreCalculationRoute = (router: SecuritySolutionPluginRouter,
const soClient = coreContext.savedObjects.client;
const spaceId = securityContext.getSpaceId();
const riskEngineDataClient = securityContext.getRiskEngineDataClient();
const riskScoreDataClient = securityContext.getRiskScoreDataClient();
const riskScoreService = riskScoreServiceFactory({
esClient,
logger,
riskEngineDataClient,
riskScoreDataClient,
spaceId,
});

View file

@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export { riskScorePreviewRoute } from './preview';

View file

@ -17,7 +17,7 @@ import {
import { getRiskInputsIndex } from '../get_risk_inputs_index';
import { riskScoreServiceFactory } from '../risk_score_service';
import { riskScoreServiceMock } from '../risk_score_service.mock';
import { riskScorePreviewRoute } from './risk_score_preview_route';
import { riskScorePreviewRoute } from './preview';
jest.mock('../risk_score_service');
jest.mock('../get_risk_inputs_index');

View file

@ -42,11 +42,13 @@ export const riskScorePreviewRoute = (router: SecuritySolutionPluginRouter, logg
const soClient = coreContext.savedObjects.client;
const spaceId = securityContext.getSpaceId();
const riskEngineDataClient = securityContext.getRiskEngineDataClient();
const riskScoreDataClient = securityContext.getRiskScoreDataClient();
const riskScoreService = riskScoreServiceFactory({
esClient,
logger,
riskEngineDataClient,
riskScoreDataClient,
spaceId,
});

View file

@ -14,7 +14,7 @@ import type { AnalyticsServiceSetup } from '@kbn/core/public';
import type { RiskScoreService } from '../risk_score_service';
import { riskScoreServiceMock } from '../risk_score_service.mock';
import { riskScoringTaskMock } from './risk_scoring_task.mock';
import { riskEngineDataClientMock } from '../risk_engine_data_client.mock';
import { riskEngineDataClientMock } from '../../risk_engine/risk_engine_data_client.mock';
import {
registerRiskScoringTask,
startRiskScoringTask,

View file

@ -21,7 +21,8 @@ import type { AnalyticsServiceSetup } from '@kbn/core-analytics-server';
import type { AfterKeys, IdentifierType } from '../../../../../common/risk_engine';
import type { StartPlugins } from '../../../../plugin';
import { type RiskScoreService, riskScoreServiceFactory } from '../risk_score_service';
import { RiskEngineDataClient } from '../risk_engine_data_client';
import { RiskEngineDataClient } from '../../risk_engine/risk_engine_data_client';
import { RiskScoreDataClient } from '../risk_score_data_client';
import { isRiskScoreCalculationComplete } from '../helpers';
import {
defaultState,
@ -77,11 +78,19 @@ export const registerRiskScoringTask = ({
namespace,
soClient,
});
const riskScoreDataClient = new RiskScoreDataClient({
logger,
kibanaVersion,
esClient,
namespace,
soClient,
});
return riskScoreServiceFactory({
esClient,
logger,
riskEngineDataClient,
riskScoreDataClient,
spaceId: namespace,
});
});

View file

@ -14,7 +14,7 @@ import type {
Range,
RiskEngineStatus,
RiskScore,
} from '../../../../common/risk_engine';
} from '../../../common/risk_engine';
export interface CalculateScoresParams {
afterKeys: AfterKeys;
@ -64,7 +64,7 @@ export interface GetRiskEngineStatusResponse {
is_max_amount_of_risk_engines_reached: boolean;
}
interface InitRiskEngineResultResponse {
export interface InitRiskEngineResultResponse {
risk_engine_enabled: boolean;
risk_engine_resources_installed: boolean;
risk_engine_configuration_created: boolean;

View file

@ -9,9 +9,9 @@
// original function create index instead of datastream, and their have plan to use datastream in the future
// so we probably should remove this file and use the original when datastream will be supported
import { get } from 'lodash';
import type { IndicesSimulateIndexTemplateResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import type { Logger, ElasticsearchClient } from '@kbn/core/server';
import { get } from 'lodash';
import { retryTransientEsErrors } from './retry_transient_es_errors';
export interface IIndexPatternString {
@ -19,33 +19,33 @@ export interface IIndexPatternString {
alias: string;
}
interface ConcreteIndexInfo {
index: string;
alias: string;
isWriteIndex: boolean;
interface CreateConcreteWriteIndexOpts {
logger: Logger;
esClient: ElasticsearchClient;
totalFieldsLimit: number;
indexPatterns: IIndexPatternString;
}
interface UpdateIndexMappingsOpts {
logger: Logger;
esClient: ElasticsearchClient;
totalFieldsLimit: number;
concreteIndices: ConcreteIndexInfo[];
totalFieldsLimit?: number;
indices: string[];
}
interface UpdateIndexOpts {
logger: Logger;
esClient: ElasticsearchClient;
totalFieldsLimit: number;
concreteIndexInfo: ConcreteIndexInfo;
totalFieldsLimit?: number;
index: string;
}
const updateTotalFieldLimitSetting = async ({
logger,
esClient,
totalFieldsLimit,
concreteIndexInfo,
index,
}: UpdateIndexOpts) => {
const { index, alias } = concreteIndexInfo;
try {
await retryTransientEsErrors(
() =>
@ -57,22 +57,17 @@ const updateTotalFieldLimitSetting = async ({
);
} catch (err) {
logger.error(
`Failed to PUT index.mapping.total_fields.limit settings for alias ${alias}: ${err.message}`
`Failed to PUT index.mapping.total_fields.limit settings for index ${index}: ${err.message}`
);
throw err;
}
};
// This will update the mappings of backing indices but *not* the settings. This
// This will update the mappings of indices but *not* the settings. This
// is due to the fact settings can be classed as dynamic and static, and static
// updates will fail on an index that isn't closed. New settings *will* be applied as part
// of the ILM policy rollovers. More info: https://github.com/elastic/kibana/pull/113389#issuecomment-940152654
const updateUnderlyingMapping = async ({
logger,
esClient,
concreteIndexInfo,
}: UpdateIndexOpts) => {
const { index, alias } = concreteIndexInfo;
const updateUnderlyingMapping = async ({ logger, esClient, index }: UpdateIndexOpts) => {
let simulatedIndexMapping: IndicesSimulateIndexTemplateResponse;
try {
simulatedIndexMapping = await retryTransientEsErrors(
@ -81,7 +76,7 @@ const updateUnderlyingMapping = async ({
);
} catch (err) {
logger.error(
`Ignored PUT mappings for alias ${alias}; error generating simulated mappings: ${err.message}`
`Ignored PUT mappings for index ${index}; error generating simulated mappings: ${err.message}`
);
return;
}
@ -89,7 +84,7 @@ const updateUnderlyingMapping = async ({
const simulatedMapping = get(simulatedIndexMapping, ['template', 'mappings']);
if (simulatedMapping == null) {
logger.error(`Ignored PUT mappings for alias ${alias}; simulated mappings were empty`);
logger.error(`Ignored PUT mappings for index ${index}; simulated mappings were empty`);
return;
}
@ -98,44 +93,37 @@ const updateUnderlyingMapping = async ({
() => esClient.indices.putMapping({ index, body: simulatedMapping }),
{ logger }
);
logger.info(`Update mappings for ${index}`);
} catch (err) {
logger.error(`Failed to PUT mapping for alias ${alias}: ${err.message}`);
logger.error(`Failed to PUT mapping for index ${index}: ${err.message}`);
throw err;
}
};
/**
* Updates the underlying mapping for any existing concrete indices
*/
const updateIndexMappings = async ({
export const updateIndexMappings = async ({
logger,
esClient,
totalFieldsLimit,
concreteIndices,
indices,
}: UpdateIndexMappingsOpts) => {
logger.debug(`Updating underlying mappings for ${concreteIndices.length} indices.`);
logger.info(`Updating underlying mappings for ${indices.length} indices.`);
// Update total field limit setting of found indices
// Other index setting changes are not updated at this time
await Promise.all(
concreteIndices.map((index) =>
updateTotalFieldLimitSetting({ logger, esClient, totalFieldsLimit, concreteIndexInfo: index })
)
);
if (totalFieldsLimit) {
// Update total field limit setting of found indices
// Other index setting changes are not updated at this time
await Promise.all(
indices.map((index) =>
updateTotalFieldLimitSetting({ logger, esClient, totalFieldsLimit, index })
)
);
}
// Update mappings of the found indices.
await Promise.all(
concreteIndices.map((index) =>
updateUnderlyingMapping({ logger, esClient, totalFieldsLimit, concreteIndexInfo: index })
)
);
await Promise.all(indices.map((index) => updateUnderlyingMapping({ logger, esClient, index })));
};
interface CreateConcreteWriteIndexOpts {
logger: Logger;
esClient: ElasticsearchClient;
totalFieldsLimit: number;
indexPatterns: IIndexPatternString;
}
/**
* Create a data stream
*/
@ -148,7 +136,7 @@ export const createDataStream = async ({
logger.info(`Creating data stream - ${indexPatterns.alias}`);
// check if a datastream already exists
let dataStreams: ConcreteIndexInfo[] = [];
let dataStreams: string[] = [];
try {
// Specify both the index pattern for the backing indices and their aliases
// The alias prevents the request from finding other namespaces that could match the -* pattern
@ -157,11 +145,7 @@ export const createDataStream = async ({
{ logger }
);
dataStreams = response.data_streams.map((dataStream) => ({
index: dataStream.name,
alias: dataStream.name,
isWriteIndex: true,
}));
dataStreams = response.data_streams.map((dataStream) => dataStream.name);
logger.debug(
`Found ${dataStreams.length} concrete indices for ${indexPatterns.alias} - ${JSON.stringify(
@ -182,7 +166,7 @@ export const createDataStream = async ({
// if a concrete write datastream already exists, update the underlying mapping
if (dataStreams.length > 0) {
await updateIndexMappings({ logger, esClient, totalFieldsLimit, concreteIndices: dataStreams });
await updateIndexMappings({ logger, esClient, totalFieldsLimit, indices: dataStreams });
}
// check if a concrete write datastream already exists

View file

@ -10,8 +10,13 @@ import type {
IndicesCreateRequest,
IndicesCreateResponse,
} from '@elastic/elasticsearch/lib/api/types';
import { retryTransientEsErrors } from './retry_transient_es_errors';
export const createIndex = async ({
/**
* It's check for index existatnce, and create index
* or update existing index mappings
*/
export const createOrUpdateIndex = async ({
esClient,
logger,
options,
@ -25,11 +30,29 @@ export const createIndex = async ({
index: options.index,
});
if (isIndexExist) {
const response = await esClient.indices.get({
index: options.index,
});
const indices = Object.keys(response ?? {});
logger.info(`${options.index} already exist`);
return;
if (options.mappings) {
await Promise.all(
indices.map(async (index) => {
try {
await retryTransientEsErrors(
() => esClient.indices.putMapping({ index, body: options.mappings }),
{ logger }
);
logger.info(`Update mappings for ${index}`);
} catch (err) {
logger.error(`Failed to PUT mapping for index ${index}: ${err.message}`);
}
})
);
}
} else {
return esClient.indices.create(options);
}
return esClient.indices.create(options);
} catch (err) {
const error = transformError(err);
const fullErrorMessage = `Failed to create index: ${options.index}: ${error.message}`;

View file

@ -14,11 +14,11 @@ import type {
TransformPutTransformRequest,
TransformGetTransformStatsTransformStats,
} from '@elastic/elasticsearch/lib/api/types';
import { RiskScoreEntity } from '../../../../../common/search_strategy';
import { RiskScoreEntity } from '../../../../common/search_strategy';
import {
getRiskScorePivotTransformId,
getRiskScoreLatestTransformId,
} from '../../../../../common/utils/risk_score_modules';
} from '../../../../common/utils/risk_score_modules';
export const getLegacyTransforms = async ({
namespace,

View file

@ -107,7 +107,7 @@ import {
} from '../common/endpoint/constants';
import { AppFeaturesService } from './lib/app_features_service/app_features_service';
import { registerRiskScoringTask } from './lib/entity_analytics/risk_engine/tasks/risk_scoring_task';
import { registerRiskScoringTask } from './lib/entity_analytics/risk_score/tasks/risk_scoring_task';
import { registerProtectionUpdatesNoteRoutes } from './endpoint/routes/protection_updates_note';
import { latestRiskScoreIndexPattern, allRiskScoreIndexPattern } from '../common/risk_engine';
import { isEndpointPackageV2 } from '../common/endpoint/utils/package_v2';

View file

@ -26,6 +26,8 @@ import type { Immutable } from '../common/endpoint/types';
import type { EndpointAuthz } from '../common/endpoint/types/authz';
import type { EndpointAppContextService } from './endpoint/endpoint_app_context_services';
import { RiskEngineDataClient } from './lib/entity_analytics/risk_engine/risk_engine_data_client';
import { RiskScoreDataClient } from './lib/entity_analytics/risk_score/risk_score_data_client';
import { AssetCriticalityDataClient } from './lib/entity_analytics/asset_criticality/asset_criticality_data_client';
export interface IRequestContextFactory {
create(
@ -141,6 +143,24 @@ export class RequestContextFactory implements IRequestContextFactory {
namespace: getSpaceId(),
})
),
getRiskScoreDataClient: memoize(
() =>
new RiskScoreDataClient({
logger: options.logger,
kibanaVersion: options.kibanaVersion,
esClient: coreContext.elasticsearch.client.asCurrentUser,
soClient: coreContext.savedObjects.client,
namespace: getSpaceId(),
})
),
getAssetCriticalityDataClient: memoize(
() =>
new AssetCriticalityDataClient({
logger: options.logger,
esClient: coreContext.elasticsearch.client.asCurrentUser,
namespace: getSpaceId(),
})
),
};
}
}

View file

@ -75,14 +75,15 @@ import { registerDashboardsRoutes } from '../lib/dashboards/routes';
import { registerTagsRoutes } from '../lib/tags/routes';
import { setAlertTagsRoute } from '../lib/detection_engine/routes/signals/set_alert_tags_route';
import {
riskScorePreviewRoute,
riskEngineDisableRoute,
riskEngineInitRoute,
riskEngineEnableRoute,
riskEngineStatusRoute,
riskEnginePrivilegesRoute,
} from '../lib/entity_analytics/risk_engine/routes';
import { riskScoreCalculationRoute } from '../lib/entity_analytics/risk_engine/routes/risk_score_calculation_route';
import { riskScoreCalculationRoute } from '../lib/entity_analytics/risk_score/routes/calculation';
import { riskScorePreviewRoute } from '../lib/entity_analytics/risk_score/routes/preview';
import { assetCriticalityStatusRoute } from '../lib/entity_analytics/asset_criticality/routes';
export const initRoutes = (
router: SecuritySolutionPluginRouter,
@ -192,4 +193,7 @@ export const initRoutes = (
riskEnginePrivilegesRoute(router, getStartServices);
}
}
if (config.experimentalFeatures.entityAnalyticsAssetCriticalityEnabled) {
assetCriticalityStatusRoute(router, logger);
}
};

View file

@ -30,7 +30,8 @@ import type { FrameworkRequest } from './lib/framework';
import type { EndpointAuthz } from '../common/endpoint/types/authz';
import type { EndpointInternalFleetServicesInterface } from './endpoint/services/fleet';
import type { RiskEngineDataClient } from './lib/entity_analytics/risk_engine/risk_engine_data_client';
import type { RiskScoreDataClient } from './lib/entity_analytics/risk_score/risk_score_data_client';
import type { AssetCriticalityDataClient } from './lib/entity_analytics/asset_criticality/asset_criticality_data_client';
export { AppClient };
export interface SecuritySolutionApiRequestHandlerContext {
@ -48,6 +49,8 @@ export interface SecuritySolutionApiRequestHandlerContext {
getExceptionListClient: () => ExceptionListClient | null;
getInternalFleetServices: () => EndpointInternalFleetServicesInterface;
getRiskEngineDataClient: () => RiskEngineDataClient;
getRiskScoreDataClient: () => RiskScoreDataClient;
getAssetCriticalityDataClient: () => AssetCriticalityDataClient;
}
export type SecuritySolutionRequestHandlerContext = CustomRequestHandlerContext<{

View file

@ -81,7 +81,6 @@ export function createTestConfig(options: CreateTestConfigOptions, testFiles?: s
'previewTelemetryUrlEnabled',
'riskScoringPersistence',
'riskScoringRoutesEnabled',
'riskEnginePrivilegesRouteEnabled',
])}`,
'--xpack.task_manager.poll_interval=1000',
`--xpack.actions.preconfigured=${JSON.stringify({

View file

@ -0,0 +1,85 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import expect from '@kbn/expect';
import {
cleanRiskEngine,
cleanAssetCriticality,
assetCriticalityRouteHelpersFactory,
} from '../../utils';
import { FtrProviderContext } from '../../../../ftr_provider_context';
export default ({ getService }: FtrProviderContext) => {
const es = getService('es');
const kibanaServer = getService('kibanaServer');
const log = getService('log');
const supertest = getService('supertest');
const assetCriticalityRoutes = assetCriticalityRouteHelpersFactory(supertest);
describe('@ess @serverless @skipInQA asset_criticality Asset Criticality APIs', () => {
beforeEach(async () => {
await cleanRiskEngine({ kibanaServer, es, log });
await cleanAssetCriticality({ log, es });
});
afterEach(async () => {
await cleanRiskEngine({ kibanaServer, es, log });
await cleanAssetCriticality({ log, es });
});
describe('initialisation of resources', () => {
it('should has index installed on status api call', async () => {
const assetCriticalityIndex = '.asset-criticality.asset-criticality-default';
let assetCriticalityIndexExist;
try {
assetCriticalityIndexExist = await es.indices.exists({
index: assetCriticalityIndex,
});
} catch (e) {
assetCriticalityIndexExist = false;
}
expect(assetCriticalityIndexExist).to.eql(false);
const statusResponse = await assetCriticalityRoutes.status();
expect(statusResponse.body).to.eql({
asset_criticality_resources_installed: true,
});
const assetCriticalityIndexResult = await es.indices.get({
index: assetCriticalityIndex,
});
expect(
assetCriticalityIndexResult['.asset-criticality.asset-criticality-default']?.mappings
).to.eql({
dynamic: 'strict',
properties: {
'@timestamp': {
type: 'date',
},
criticality_level: {
type: 'keyword',
},
id_field: {
type: 'keyword',
},
id_value: {
type: 'keyword',
},
updated_at: {
type: 'date',
},
},
});
});
});
});
};

View file

@ -13,6 +13,16 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
return {
...functionalConfig.getAll(),
kbnTestServer: {
...functionalConfig.get('kbnTestServer'),
serverArgs: [
...functionalConfig.get('kbnTestServer.serverArgs'),
`--xpack.securitySolution.enableExperimental=${JSON.stringify([
'entityAnalyticsAssetCriticalityEnabled',
'riskEnginePrivilegesRouteEnabled',
])}`,
],
},
testFiles: [require.resolve('..')],
junit: {
reportName: 'Entity Analytics API Integration Tests - ESS - Risk Engine',

View file

@ -8,6 +8,12 @@
import { createTestConfig } from '../../../../../config/serverless/config.base';
export default createTestConfig({
kbnTestServerArgs: [
`--xpack.securitySolution.enableExperimental=${JSON.stringify([
'entityAnalyticsAssetCriticalityEnabled',
'riskEnginePrivilegesRouteEnabled',
])}`,
],
testFiles: [require.resolve('..')],
junit: {
reportName: 'Entity Analytics API Integration Tests - Serverless - Risk Engine',

View file

@ -16,5 +16,6 @@ export default function ({ loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./risk_scoring_task/task_execution_nondefault_spaces'));
loadTestFile(require.resolve('./telemetry_usage'));
loadTestFile(require.resolve('./risk_engine_privileges'));
loadTestFile(require.resolve('./asset_criticality'));
});
}

View file

@ -0,0 +1,50 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import SuperTest from 'supertest';
import {
ELASTIC_HTTP_VERSION_HEADER,
X_ELASTIC_INTERNAL_ORIGIN_REQUEST,
} from '@kbn/core-http-common';
import { ASSET_CRITICALITY_STATUS_URL } from '@kbn/security-solution-plugin/common/constants';
import type { Client } from '@elastic/elasticsearch';
import type { ToolingLog } from '@kbn/tooling-log';
import { routeWithNamespace } from '../../detections_response/utils';
export const cleanAssetCriticality = async ({
log,
es,
namespace = 'default',
}: {
log: ToolingLog;
es: Client;
namespace?: string;
}) => {
try {
await Promise.allSettled([
es.indices.delete({
index: [`.asset-criticality.asset-criticality-${namespace}`],
}),
]);
} catch (e) {
log.warning(`Error deleting asset criticality index: ${e.message}`);
}
};
export const assetCriticalityRouteHelpersFactory = (
supertest: SuperTest.SuperTest<SuperTest.Test>,
namespace?: string
) => ({
status: async () =>
await supertest
.get(routeWithNamespace(ASSET_CRITICALITY_STATUS_URL, namespace))
.set('kbn-xsrf', 'true')
.set(ELASTIC_HTTP_VERSION_HEADER, '1')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send()
.expect(200),
});

View file

@ -6,3 +6,4 @@
*/
export * from './risk_engine';
export * from './get_risk_engine_stats';
export * from './asset_criticality';