Skip to content

Commit

Permalink
[Security Solution] Create Entity Store index (elastic#175025)
Browse files Browse the repository at this point in the history
**This PR is going to be merged to the
[entity-store-poc](https://github.com/elastic/kibana/tree/security/feature/entity-store-poc)
feature branch; it won't impact the main branch.**

## Summary

* Create `entity_store/init` route that creates the Entity Store index.
* Create FTR tests.
### Out of scope
  * User fields are out of scope.
  * API privileges are out of scope.

### How to test it?
* Call API
```
 KIBANA_URL="http://localhost:5601"
 USER_PASS="{USER}:{PASSWORD}"

curl "$KIBANA_URL/internal/entity_store/init" \
  -H 'kbn-xsrf:bleh' \
  --user "$USER_PASS"\
  -X 'POST' \
  -H 'elastic-api-version: 1'
```
* Open the console and check if the index `.entities.entities-default`
exists

#### Run tests
**serverless**
`yarn run initialize-server:ea:default entity_store serverless`
`yarn run run-tests:ea:default entity_store serverless serverlessEnv`

**ess**
`yarn run initialize-server:ea:default entity_store ess`
`yarn run run-tests:ea:default entity_store ess essEnv`

### Checklist

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

---------

Co-authored-by: Kibana Machine <[email protected]>
  • Loading branch information
2 people authored and hop-dev committed Apr 17, 2024
1 parent 805217e commit c66a373
Show file tree
Hide file tree
Showing 18 changed files with 500 additions and 3 deletions.
2 changes: 2 additions & 0 deletions x-pack/plugins/security_solution/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,8 @@ export const DETECTION_ENGINE_RULES_BULK_UPDATE =
`${DETECTION_ENGINE_RULES_URL}/_bulk_update` as const;

export * from './entity_analytics/constants';
export const INTERNAL_ENTITY_STORE_URL = '/internal/entity_store' as const;
export const ENTITY_STORE_INIT_URL = `${INTERNAL_ENTITY_STORE_URL}/init`;

export const INTERNAL_DASHBOARDS_URL = `/internal/dashboards` as const;
export const INTERNAL_TAGS_URL = `/internal/tags`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,10 @@ export const allowedExperimentalValues = Object.freeze({
*/
alertSuppressionForNonSequenceEqlRuleEnabled: false,

/**
* Enables Entity Store POC
*/
entityStoreEnabled: true,
/**
* Enables experimental Experimental S1 integration data to be available in Analyzer
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ 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';
import { entityStoreDataClientMock } from '../../../entity_analytics/entity_store/entity_store_data_client.mock';

export const createMockClients = () => {
const core = coreMock.createRequestHandlerContext();
Expand Down Expand Up @@ -67,6 +68,7 @@ export const createMockClients = () => {
riskEngineDataClient: riskEngineDataClientMock.create(),
riskScoreDataClient: riskScoreDataClientMock.create(),
assetCriticalityDataClient: assetCriticalityDataClientMock.create(),
entityStoreyDataClient: entityStoreDataClientMock.create(),
};
};

Expand Down Expand Up @@ -148,6 +150,7 @@ const createSecuritySolutionRequestContextMock = (
getRiskEngineDataClient: jest.fn(() => clients.riskEngineDataClient),
getRiskScoreDataClient: jest.fn(() => clients.riskScoreDataClient),
getAssetCriticalityDataClient: jest.fn(() => clients.assetCriticalityDataClient),
getEntityStoreDataClient: jest.fn(() => clients.entityStoreyDataClient),
};
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*
* 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 entityStoreFieldMap: FieldMap = {
'@timestamp': {
type: 'date',
array: false,
required: false,
},
// user or host
entity_type: {
type: 'keyword',
array: false,
required: true,
},
// HOST
'host.architecture': {
type: 'keyword',
required: false,
array: true,
},
'host.id': {
type: 'keyword',
required: false,
array: true,
},
'host.ip': {
type: 'ip',
required: false,
array: true,
},
'host.name': {
type: 'keyword',
required: true,
array: false,
},
'host.os.platform': {
type: 'keyword',
required: false,
array: true,
},
'host.os.version': {
type: 'keyword',
required: false,
array: true,
},
// AGENT
'agent.type': {
type: 'keyword',
required: false,
array: true,
},
'agent.id': {
type: 'keyword',
required: false,
array: true,
},
// CLOUD
'cloud.provider': {
type: 'keyword',
required: false,
array: true,
},
'cloud.region': {
type: 'keyword',
required: false,
array: true,
},
// RISK SCORE
'host.risk.calculated_level': {
type: 'keyword',
array: false,
required: false,
},
'host.risk.calculated_score': {
type: 'float',
array: false,
required: false,
},
'host.risk.calculated_score_norm': {
type: 'float',
array: false,
required: false,
},
// ASSET CRITICALITY
'host.asset.criticality': {
type: 'keyword',
array: false,
required: false,
},
} as const;
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import type { EntityStoreDataClient } from './entity_store_data_client';

const createEntityStoreDataClientMock = () =>
({
doesIndexExist: jest.fn(),
getStatus: jest.fn(),
init: jest.fn(),
search: jest.fn(),
} as unknown as jest.Mocked<EntityStoreDataClient>);

export const entityStoreDataClientMock = { create: createEntityStoreDataClientMock };
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
* 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 { createOrUpdateIndex } from '../utils/create_or_update_index';
import { EntityStoreDataClient } from './entity_store_data_client';

jest.mock('../utils/create_or_update_index', () => ({
createOrUpdateIndex: jest.fn(),
}));

describe('EntityStoreDataClient', () => {
let entityStoreDataClient: EntityStoreDataClient;
let logger: ReturnType<typeof loggingSystemMock.createLogger>;
const esClient = elasticsearchServiceMock.createScopedClusterClient().asCurrentUser;

beforeEach(() => {
logger = loggingSystemMock.createLogger();
const options = {
logger,
esClient,
namespace: 'default',
};
entityStoreDataClient = new EntityStoreDataClient(options);
});

afterEach(() => {
jest.clearAllMocks();
});

it('should initialize entity store resources successfully', async () => {
await entityStoreDataClient.init();

expect(createOrUpdateIndex).toHaveBeenCalledWith({
logger,
esClient,
options: {
index: '.entities.entities-default',
mappings: {
dynamic: 'strict',
properties: {
'@timestamp': {
ignore_malformed: false,
type: 'date',
},
agent: {
properties: {
id: {
type: 'keyword',
},
type: {
type: 'keyword',
},
},
},
cloud: {
properties: {
provider: {
type: 'keyword',
},
region: {
type: 'keyword',
},
},
},
entity_type: {
type: 'keyword',
},
host: {
properties: {
architecture: {
type: 'keyword',
},
asset: {
properties: {
criticality: {
type: 'keyword',
},
},
},
id: {
type: 'keyword',
},
ip: {
type: 'ip',
},
name: {
type: 'keyword',
},
os: {
properties: {
platform: {
type: 'keyword',
},
version: {
type: 'keyword',
},
},
},
risk: {
properties: {
calculated_level: {
type: 'keyword',
},
calculated_score: {
type: 'float',
},
calculated_score_norm: {
type: 'float',
},
},
},
},
},
},
},
},
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { Logger, ElasticsearchClient } from '@kbn/core/server';
import { mappingFromFieldMap } from '@kbn/alerting-plugin/common';
import { getEntityStoreIndex } from '../../../../common/entity_analytics/entity_store';
import { createOrUpdateIndex } from '../utils/create_or_update_index';
import { entityStoreFieldMap } from './constants';

interface EntityStoreClientOpts {
logger: Logger;
esClient: ElasticsearchClient;
namespace: string;
}

export class EntityStoreDataClient {
constructor(private readonly options: EntityStoreClientOpts) {}
/**
* It creates the entity store index or update mappings if index exists
*/
public async init() {
await createOrUpdateIndex({
esClient: this.options.esClient,
logger: this.options.logger,
options: {
index: getEntityStoreIndex(this.options.namespace),
mappings: mappingFromFieldMap(entityStoreFieldMap, 'strict'),
},
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,4 @@
* 2.0.
*/

// 🚧 TODO: make the entity store

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

import { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils';
import { transformError } from '@kbn/securitysolution-es-utils';
import { ENTITY_STORE_INIT_URL } from '../../../../../common/constants';
import type { SecuritySolutionPluginRouter } from '../../../../types';
export const entityStoreInitRoute = (router: SecuritySolutionPluginRouter) => {
router.versioned
.post({
access: 'internal',
path: ENTITY_STORE_INIT_URL,
options: {
tags: ['access:securitySolution'], // TODO entity store access `access:${APP_ID}-entity-analytics`
},
})
.addVersion(
{ version: '1', validate: {} },
// TODO Implement entity store privileges like `withRiskEnginePrivilegeCheck` in risk_engine_privileges.ts
async (context, request, response) => {
const siemResponse = buildSiemResponse(response);
const securitySolution = await context.securitySolution;
const entityStoreDataClient = securitySolution.getEntityStoreDataClient();

try {
await entityStoreDataClient.init();

return response.ok({
body: {
result: {
entity_store_created: true,
errors: [],
},
},
});
} catch (e) {
const error = transformError(e);

return siemResponse.error({
statusCode: error.statusCode,
body: { message: error.message, full_error: JSON.stringify(e) },
bypassErrorFormat: true,
});
}
}
);
};
Loading

0 comments on commit c66a373

Please sign in to comment.