mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
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:
parent
0d2d89d066
commit
45e88fea3e
84 changed files with 1336 additions and 772 deletions
|
@ -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
|
|
@ -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';
|
|
@ -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}`;
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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';
|
||||
|
||||
/**
|
||||
|
|
|
@ -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'];
|
||||
|
||||
|
|
|
@ -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<{}>) => {
|
||||
|
|
|
@ -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'];
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -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 };
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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');
|
||||
}
|
||||
};
|
|
@ -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;
|
|
@ -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';
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
|
@ -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,
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
|
@ -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 {
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
|
@ -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 {
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { CalculateAndPersistScoresResponse } from './types';
|
||||
import type { CalculateAndPersistScoresResponse } from '../types';
|
||||
|
||||
const buildResponseMock = (
|
||||
overrides: Partial<CalculateAndPersistScoresResponse> = {}
|
|
@ -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: {},
|
||||
});
|
||||
|
|
@ -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);
|
|
@ -14,7 +14,7 @@ import type {
|
|||
CalculateRiskScoreAggregations,
|
||||
CalculateScoresResponse,
|
||||
RiskScoreBucket,
|
||||
} from './types';
|
||||
} from '../types';
|
||||
|
||||
const buildRiskScoreBucketMock = (overrides: Partial<RiskScoreBucket> = {}): RiskScoreBucket => ({
|
||||
key: { 'user.name': 'username' },
|
|
@ -38,7 +38,7 @@ import type {
|
|||
CalculateScoresParams,
|
||||
CalculateScoresResponse,
|
||||
RiskScoreBucket,
|
||||
} from './types';
|
||||
} from '../types';
|
||||
|
||||
const bucketToResponse = ({
|
||||
bucket,
|
|
@ -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: {
|
|
@ -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';
|
|
@ -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 };
|
|
@ -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}`
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 }),
|
||||
});
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { riskScoreCalculationRoute } from './risk_score_calculation_route';
|
||||
import { riskScoreCalculationRoute } from './calculation';
|
||||
|
||||
import { loggerMock } from '@kbn/logging-mocks';
|
||||
|
|
@ -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,
|
||||
});
|
||||
|
|
@ -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';
|
|
@ -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');
|
|
@ -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,
|
||||
});
|
||||
|
|
@ -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,
|
|
@ -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,
|
||||
});
|
||||
});
|
|
@ -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;
|
|
@ -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
|
|
@ -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}`;
|
|
@ -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,
|
|
@ -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';
|
||||
|
|
|
@ -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(),
|
||||
})
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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<{
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
});
|
|
@ -6,3 +6,4 @@
|
|||
*/
|
||||
export * from './risk_engine';
|
||||
export * from './get_risk_engine_stats';
|
||||
export * from './asset_criticality';
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue